diff --git a/Program.cs b/Program.cs index 43ad560..06d1c6d 100644 --- a/Program.cs +++ b/Program.cs @@ -6,56 +6,56 @@ namespace redux { - class Program - { - private const string Version = "0.2.6"; - private const string logSrc = "REDUX"; - static void Main(string[] args) - { - if (args.Length == 1 && ( - args[0].Equals("-ver", StringComparison.OrdinalIgnoreCase) || - args[0].Equals("--version", StringComparison.OrdinalIgnoreCase) || - args[0].Equals("-v", StringComparison.OrdinalIgnoreCase) || - args[0].Equals("-help", StringComparison.OrdinalIgnoreCase) || - args[0].Equals("-h", StringComparison.OrdinalIgnoreCase))) - { - ShowHelp(); - return; - } + class Program + { + private const string Version = "0.2.6"; + private const string logSrc = "REDUX"; + static void Main(string[] args) + { + if (args.Length == 1 && ( + args[0].Equals("-ver", StringComparison.OrdinalIgnoreCase) || + args[0].Equals("--version", StringComparison.OrdinalIgnoreCase) || + args[0].Equals("-v", StringComparison.OrdinalIgnoreCase) || + args[0].Equals("-help", StringComparison.OrdinalIgnoreCase) || + args[0].Equals("-h", StringComparison.OrdinalIgnoreCase))) + { + ShowHelp(); + return; + } - string inputFile = null; - string outFormatArg = null; + string inputFile = null; + string outFormatArg = null; - // Parse arguments: - for (int i = 0; i < args.Length; i++) - { - if ((args[i].Equals("-input", StringComparison.OrdinalIgnoreCase) || - args[i].Equals("-i", StringComparison.OrdinalIgnoreCase)) && - i + 1 < args.Length) - { - inputFile = args[++i]; - } - else if ((args[i].Equals("-outformat", StringComparison.OrdinalIgnoreCase) || - args[i].Equals("-out", StringComparison.OrdinalIgnoreCase) || - args[i].Equals("-format", StringComparison.OrdinalIgnoreCase) || - args[i].Equals("-output", StringComparison.OrdinalIgnoreCase)) && - i + 1 < args.Length) - { - outFormatArg = args[++i]; - } - else if (args[i].Equals("-loglevel", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length) - { - string levelStr = args[++i].ToLowerInvariant(); - Config.Verbosity = levelStr switch - { - "debug" or "0" => Config.LogLevel.Debug, - "dev" or "1" => Config.LogLevel.Dev, - "info" or "2" => Config.LogLevel.Info, - "warn" or "3" => Config.LogLevel.Warn, - "error" or "4" => Config.LogLevel.Error, - _ => Config.LogLevel.Info - }; - } + // Parse arguments: + for (int i = 0; i < args.Length; i++) + { + if ((args[i].Equals("-input", StringComparison.OrdinalIgnoreCase) || + args[i].Equals("-i", StringComparison.OrdinalIgnoreCase)) && + i + 1 < args.Length) + { + inputFile = args[++i]; + } + else if ((args[i].Equals("-outformat", StringComparison.OrdinalIgnoreCase) || + args[i].Equals("-out", StringComparison.OrdinalIgnoreCase) || + args[i].Equals("-format", StringComparison.OrdinalIgnoreCase) || + args[i].Equals("-output", StringComparison.OrdinalIgnoreCase)) && + i + 1 < args.Length) + { + outFormatArg = args[++i]; + } + else if (args[i].Equals("-loglevel", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length) + { + string levelStr = args[++i].ToLowerInvariant(); + Config.Verbosity = levelStr switch + { + "debug" or "0" => Config.LogLevel.Debug, + "dev" or "1" => Config.LogLevel.Dev, + "info" or "2" => Config.LogLevel.Info, + "warn" or "3" => Config.LogLevel.Warn, + "error" or "4" => Config.LogLevel.Error, + _ => Config.LogLevel.Info + }; + } else if (args[i].Equals("-geomirror", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length) { string axis = args[++i].Trim().ToUpperInvariant(); @@ -70,380 +70,380 @@ static void Main(string[] args) Logger.Warn(logSrc, $"Unknown axis '{axis}' for -geomirror. Valid: X, Y, Z."); } else if (args[i].Equals("-itemswap", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length) - { - Config.ReplacementItemName = args[++i]; - } - else if (args[i].Equals("-coronaswap", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length) - { - Config.CoronaClutterName = args[++i]; - } - else - { - bool IsBoolArg(string name) => - args[i].Equals(name, StringComparison.OrdinalIgnoreCase); + { + Config.ReplacementItemName = args[++i]; + } + else if (args[i].Equals("-coronaswap", StringComparison.OrdinalIgnoreCase) && i + 1 < args.Length) + { + Config.CoronaClutterName = args[++i]; + } + else + { + bool IsBoolArg(string name) => + args[i].Equals(name, StringComparison.OrdinalIgnoreCase); - bool TryGetBoolArg(int index, out bool value) - { - if (index + 1 < args.Length) - { - string val = args[index + 1].ToLowerInvariant(); - if (val == "1" || val == "true") { value = true; return true; } - if (val == "0" || val == "false") { value = false; return true; } - } - value = true; // default to true if no explicit value - return false; - } + bool TryGetBoolArg(int index, out bool value) + { + if (index + 1 < args.Length) + { + string val = args[index + 1].ToLowerInvariant(); + if (val == "1" || val == "true") { value = true; return true; } + if (val == "0" || val == "false") { value = false; return true; } + } + value = true; // default to true if no explicit value + return false; + } - if (IsBoolArg("-brushes")) - { - if (TryGetBoolArg(i, out var val)) i++; - Config.ParseBrushSectionInstead = val; - } - else if (IsBoolArg("-simplenames")) - { - if (TryGetBoolArg(i, out var val)) i++; - Config.SimpleBrushNames = val; - } - else if (IsBoolArg("-textranslate")) - { - if (TryGetBoolArg(i, out var val)) i++; - Config.TranslateRF2Textures = val; - } - else if (IsBoolArg("-texprefix")) - { - if (TryGetBoolArg(i, out var val)) i++; - Config.InsertRF2TexturePrefix = val; - } - else if (IsBoolArg("-ngons")) - { - if (TryGetBoolArg(i, out var val)) i++; - Config.TriangulatePolygons = !val; - } - else if (IsBoolArg("-flipnormals")) - { - if (TryGetBoolArg(i, out var val)) i++; - Config.FlipNormals = !val; - } - else if (IsBoolArg("-geonodetail")) - { - if (TryGetBoolArg(i, out var val)) i++; - Config.SetRF2GeoableNonDetail = val; - } - else if (IsBoolArg("-portalfaces")) - { - if (TryGetBoolArg(i, out var val)) i++; - Config.IncludePortalFaces = val; - } - else if (IsBoolArg("-detailfaces")) - { - if (TryGetBoolArg(i, out var val)) i++; - Config.IncludeDetailFaces = val; - } - else if (IsBoolArg("-alphafaces")) - { - if (TryGetBoolArg(i, out var val)) i++; - Config.IncludeAlphaFaces = val; - } - else if (IsBoolArg("-holefaces")) - { - if (TryGetBoolArg(i, out var val)) i++; - Config.IncludeHoleFaces = val; - } - else if (IsBoolArg("-liquidfaces")) - { - if (TryGetBoolArg(i, out var val)) i++; - Config.IncludeLiquidFaces = val; - } - else if (IsBoolArg("-skyfaces")) - { - if (TryGetBoolArg(i, out var val)) i++; - Config.IncludeSkyFaces = val; - } - else if (IsBoolArg("-invisiblefaces")) - { - if (TryGetBoolArg(i, out var val)) i++; - Config.IncludeInvisibleFaces = val; - } - } - } + if (IsBoolArg("-brushes")) + { + if (TryGetBoolArg(i, out var val)) i++; + Config.ParseBrushSectionInstead = val; + } + else if (IsBoolArg("-simplenames")) + { + if (TryGetBoolArg(i, out var val)) i++; + Config.SimpleBrushNames = val; + } + else if (IsBoolArg("-textranslate")) + { + if (TryGetBoolArg(i, out var val)) i++; + Config.TranslateRF2Textures = val; + } + else if (IsBoolArg("-texprefix")) + { + if (TryGetBoolArg(i, out var val)) i++; + Config.InsertRF2TexturePrefix = val; + } + else if (IsBoolArg("-ngons")) + { + if (TryGetBoolArg(i, out var val)) i++; + Config.TriangulatePolygons = !val; + } + else if (IsBoolArg("-flipnormals")) + { + if (TryGetBoolArg(i, out var val)) i++; + Config.FlipNormals = !val; + } + else if (IsBoolArg("-geonodetail")) + { + if (TryGetBoolArg(i, out var val)) i++; + Config.SetRF2GeoableNonDetail = val; + } + else if (IsBoolArg("-portalfaces")) + { + if (TryGetBoolArg(i, out var val)) i++; + Config.IncludePortalFaces = val; + } + else if (IsBoolArg("-detailfaces")) + { + if (TryGetBoolArg(i, out var val)) i++; + Config.IncludeDetailFaces = val; + } + else if (IsBoolArg("-alphafaces")) + { + if (TryGetBoolArg(i, out var val)) i++; + Config.IncludeAlphaFaces = val; + } + else if (IsBoolArg("-holefaces")) + { + if (TryGetBoolArg(i, out var val)) i++; + Config.IncludeHoleFaces = val; + } + else if (IsBoolArg("-liquidfaces")) + { + if (TryGetBoolArg(i, out var val)) i++; + Config.IncludeLiquidFaces = val; + } + else if (IsBoolArg("-skyfaces")) + { + if (TryGetBoolArg(i, out var val)) i++; + Config.IncludeSkyFaces = val; + } + else if (IsBoolArg("-invisiblefaces")) + { + if (TryGetBoolArg(i, out var val)) i++; + Config.IncludeInvisibleFaces = val; + } + } + } - if (string.IsNullOrEmpty(inputFile) || string.IsNullOrEmpty(outFormatArg)) - { - Logger.Error(logSrc, "Usage: redux.exe -input -outformat [options]"); - return; - } + if (string.IsNullOrEmpty(inputFile) || string.IsNullOrEmpty(outFormatArg)) + { + Logger.Error(logSrc, "Usage: redux.exe -input -outformat [options]"); + return; + } - if (!File.Exists(inputFile)) - { - Logger.Error(logSrc, $"Input file not found: {inputFile}"); - return; - } + if (!File.Exists(inputFile)) + { + Logger.Error(logSrc, $"Input file not found: {inputFile}"); + return; + } - // Normalize extensions/formats - string inExt = Path.GetExtension(inputFile).ToLowerInvariant(); - if (inExt.Length == 0) - { - Logger.Error(logSrc, "Input file has no extension; cannot determine format."); - return; - } + // Normalize extensions/formats + string inExt = Path.GetExtension(inputFile).ToLowerInvariant(); + if (inExt.Length == 0) + { + Logger.Error(logSrc, "Input file has no extension; cannot determine format."); + return; + } - // Build a canonical “desired extension” (with leading dot) from outFormatArg: - outFormatArg = outFormatArg.ToLowerInvariant(); - string desiredExt; - bool extractTextures = false; // used for tga/png + // Build a canonical “desired extension” (with leading dot) from outFormatArg: + outFormatArg = outFormatArg.ToLowerInvariant(); + string desiredExt; + bool extractTextures = false; // used for tga/png - switch (outFormatArg) - { - case "peg": - desiredExt = ".peg"; - break; - case "rfg": - desiredExt = ".rfg"; - break; - case "v3m": - desiredExt = ".v3m"; - break; - case "obj": - desiredExt = ".obj"; - break; - /*case "gltf": - desiredExt = ".gltf"; - break;*/ - case "tga": - Config.ExportImageFormat = Config.ImageFormat.tga; - extractTextures = true; - desiredExt = outFormatArg == "png" ? ".png" : ".tga"; - break; - case "png": - extractTextures = true; - desiredExt = outFormatArg == "png" ? ".png" : ".tga"; - break; - default: - Logger.Error(logSrc, $"Unknown outformat “{outFormatArg}”. Valid: peg, rfg, v3m, obj, tga, png."); - return; - } + switch (outFormatArg) + { + case "peg": + desiredExt = ".peg"; + break; + case "rfg": + desiredExt = ".rfg"; + break; + case "v3m": + desiredExt = ".v3m"; + break; + case "obj": + desiredExt = ".obj"; + break; + /*case "gltf": + desiredExt = ".gltf"; + break;*/ + case "tga": + Config.ExportImageFormat = Config.ImageFormat.tga; + extractTextures = true; + desiredExt = outFormatArg == "png" ? ".png" : ".tga"; + break; + case "png": + extractTextures = true; + desiredExt = outFormatArg == "png" ? ".png" : ".tga"; + break; + default: + Logger.Error(logSrc, $"Unknown outformat “{outFormatArg}”. Valid: peg, rfg, v3m, obj, tga, png."); + return; + } - // Construct the output filename by replacing the input’s extension with desiredExt - string inputFolder = Path.GetDirectoryName(inputFile) ?? ""; - string inputBase = Path.GetFileNameWithoutExtension(inputFile); - string outputFile = Path.Combine(inputFolder, inputBase + desiredExt); + // Construct the output filename by replacing the input’s extension with desiredExt + string inputFolder = Path.GetDirectoryName(inputFile) ?? ""; + string inputBase = Path.GetFileNameWithoutExtension(inputFile); + string outputFile = Path.Combine(inputFolder, inputBase + desiredExt); - Logger.Info(logSrc, $"Converting {inExt} → {desiredExt}: {inputFile} → {outputFile}"); + Logger.Info(logSrc, $"Converting {inExt} → {desiredExt}: {inputFile} → {outputFile}"); - // Dispatch based on (input extension, desired format) - switch (inExt) - { - case ".peg": - if (extractTextures) - { - PegParser.ExtractPeg(inputFile); - } - else - { - if (desiredExt is ".rfg" or ".v3m" or ".obj") - { - Logger.Error(logSrc, $".peg → {desiredExt} is not a valid conversion."); - } - else - { - Logger.Warn(logSrc, "Input and output formats are both PEG; nothing to do."); - } - } - break; + // Dispatch based on (input extension, desired format) + switch (inExt) + { + case ".peg": + if (extractTextures) + { + PegParser.ExtractPeg(inputFile); + } + else + { + if (desiredExt is ".rfg" or ".v3m" or ".obj") + { + Logger.Error(logSrc, $".peg → {desiredExt} is not a valid conversion."); + } + else + { + Logger.Warn(logSrc, "Input and output formats are both PEG; nothing to do."); + } + } + break; - case ".rfg": - if (desiredExt == ".rfg") - { - var mesh = RfgParser.ReadRfg(inputFile); - RfgExporter.ExportRfg(mesh, outputFile); - } - else if (desiredExt == ".obj") - { - var mesh = RfgParser.ReadRfg(inputFile); - ObjExporter.ExportObj(mesh, outputFile); - } - else if (desiredExt == ".v3m") - { - var mesh = RfgParser.ReadRfg(inputFile); - V3mExporter.ExportV3m(mesh, outputFile); - } - else - { - Logger.Error(logSrc, $".rfg → {desiredExt} is not a valid conversion."); - } - break; + case ".rfg": + if (desiredExt == ".rfg") + { + var mesh = RfgParser.ReadRfg(inputFile); + RfgExporter.ExportRfg(mesh, outputFile); + } + else if (desiredExt == ".obj") + { + var mesh = RfgParser.ReadRfg(inputFile); + ObjExporter.ExportObj(mesh, outputFile); + } + else if (desiredExt == ".v3m") + { + var mesh = RfgParser.ReadRfg(inputFile); + V3mExporter.ExportV3m(mesh, outputFile); + } + else + { + Logger.Error(logSrc, $".rfg → {desiredExt} is not a valid conversion."); + } + break; - case ".v3c": - case ".v3m": - if (desiredExt == ".v3m") - { - var mesh = V3mParser.ReadV3mAsRflMesh(inputFile); - V3mExporter.ExportV3m(mesh, outputFile); - } - /*else if (desiredExt == ".gltf") - { - var mesh = V3mParser.ReadV3mAsRflMesh(inputFile); - GltfExporter.ExportGltf(mesh, outputFile); - }*/ - else if (desiredExt == ".obj") - { - var mesh = V3mParser.ReadV3mAsRflMesh(inputFile); - ObjExporter.ExportObj(mesh, outputFile); - } - else if (desiredExt == ".rfg") - { - var mesh = V3mParser.ReadV3mAsRflMesh(inputFile); - RfgExporter.ExportRfg(mesh, outputFile); - } - else - { - Logger.Error(logSrc, $".v3m → {desiredExt} is not a valid conversion."); - } - break; + case ".v3c": + case ".v3m": + if (desiredExt == ".v3m") + { + var mesh = V3mParser.ReadV3mAsRflMesh(inputFile); + V3mExporter.ExportV3m(mesh, outputFile); + } + /*else if (desiredExt == ".gltf") + { + var mesh = V3mParser.ReadV3mAsRflMesh(inputFile); + GltfExporter.ExportGltf(mesh, outputFile); + }*/ + else if (desiredExt == ".obj") + { + var mesh = V3mParser.ReadV3mAsRflMesh(inputFile); + ObjExporter.ExportObj(mesh, outputFile); + } + else if (desiredExt == ".rfg") + { + var mesh = V3mParser.ReadV3mAsRflMesh(inputFile); + RfgExporter.ExportRfg(mesh, outputFile); + } + else + { + Logger.Error(logSrc, $".v3m → {desiredExt} is not a valid conversion."); + } + break; - case ".rfc": - case ".rfm": - if (desiredExt == ".v3m") - { - var mesh = RfmParser.ReadRfm(inputFile); - V3mExporter.ExportV3m(mesh, outputFile); - } - /*else if (desiredExt == ".gltf") - { - var mesh = RfmParser.ReadRfm(inputFile); - GltfExporter.ExportGltf(mesh, outputFile); - }*/ - else if (desiredExt == ".obj") - { - var mesh = RfmParser.ReadRfm(inputFile); - ObjExporter.ExportObj(mesh, outputFile); - } - else if (desiredExt == ".rfg") - { - var mesh = RfmParser.ReadRfm(inputFile); - RfgExporter.ExportRfg(mesh, outputFile); - } - else - { - Logger.Error(logSrc, $".rfm → {desiredExt} is not a valid conversion."); - } - break; + case ".rfc": + case ".rfm": + if (desiredExt == ".v3m") + { + var mesh = RfmParser.ReadRfm(inputFile); + V3mExporter.ExportV3m(mesh, outputFile); + } + /*else if (desiredExt == ".gltf") + { + var mesh = RfmParser.ReadRfm(inputFile); + GltfExporter.ExportGltf(mesh, outputFile); + }*/ + else if (desiredExt == ".obj") + { + var mesh = RfmParser.ReadRfm(inputFile); + ObjExporter.ExportObj(mesh, outputFile); + } + else if (desiredExt == ".rfg") + { + var mesh = RfmParser.ReadRfm(inputFile); + RfgExporter.ExportRfg(mesh, outputFile); + } + else + { + Logger.Error(logSrc, $".rfm → {desiredExt} is not a valid conversion."); + } + break; - case ".obj": - if (desiredExt == ".obj") - { - var mesh = ObjParser.ReadObj(inputFile); - ObjExporter.ExportObj(mesh, outputFile); - } - else if (desiredExt == ".rfg") - { - var mesh = ObjParser.ReadObj(inputFile); - RfgExporter.ExportRfg(mesh, outputFile); - } - else if (desiredExt == ".v3m") - { - var mesh = ObjParser.ReadObj(inputFile); - V3mExporter.ExportV3m(mesh, outputFile); - } - else - { - Logger.Error(logSrc, $".obj → {desiredExt} is not a valid conversion."); - } - break; + case ".obj": + if (desiredExt == ".obj") + { + var mesh = ObjParser.ReadObj(inputFile); + ObjExporter.ExportObj(mesh, outputFile); + } + else if (desiredExt == ".rfg") + { + var mesh = ObjParser.ReadObj(inputFile); + RfgExporter.ExportRfg(mesh, outputFile); + } + else if (desiredExt == ".v3m") + { + var mesh = ObjParser.ReadObj(inputFile); + V3mExporter.ExportV3m(mesh, outputFile); + } + else + { + Logger.Error(logSrc, $".obj → {desiredExt} is not a valid conversion."); + } + break; - case ".rfl": - if (desiredExt == ".obj") - { - var mesh = RflParser.ReadRfl(inputFile); - ObjExporter.ExportObj(mesh, outputFile); - } - else if (desiredExt == ".rfg") - { - var mesh = RflParser.ReadRfl(inputFile); - RfgExporter.ExportRfg(mesh, outputFile); - } - else if (desiredExt == ".v3m") - { - var mesh = RflParser.ReadRfl(inputFile); - V3mExporter.ExportV3m(mesh, outputFile); - } - else - { - Logger.Error(logSrc, $".rfl → {desiredExt} is not a valid conversion."); - } - break; + case ".rfl": + if (desiredExt == ".obj") + { + var mesh = RflParser.ReadRfl(inputFile); + ObjExporter.ExportObj(mesh, outputFile); + } + else if (desiredExt == ".rfg") + { + var mesh = RflParser.ReadRfl(inputFile); + RfgExporter.ExportRfg(mesh, outputFile); + } + else if (desiredExt == ".v3m") + { + var mesh = RflParser.ReadRfl(inputFile); + V3mExporter.ExportV3m(mesh, outputFile); + } + else + { + Logger.Error(logSrc, $".rfl → {desiredExt} is not a valid conversion."); + } + break; - case ".rfa": - if (desiredExt == ".rfa") - { - RfaParser.ReadRfa(inputFile); - } - else - { - Logger.Error(logSrc, $".rfa → {desiredExt} is not a valid conversion."); - } - break; + case ".rfa": + if (desiredExt == ".rfa") + { + RfaParser.ReadRfa(inputFile); + } + else + { + Logger.Error(logSrc, $".rfa → {desiredExt} is not a valid conversion."); + } + break; - default: - Logger.Error(logSrc, $"Unsupported input format “{inExt}.”"); - break; - } - } + default: + Logger.Error(logSrc, $"Unsupported input format “{inExt}.”"); + break; + } + } - private static void ShowHelp() - { - Console.WriteLine(); - Console.WriteLine($"======================================================="); - Console.WriteLine($"======================================================="); - Console.WriteLine($"====== RED UX Toolkit by Goober - Version {Version} ======="); - Console.WriteLine($"======================================================="); - Console.WriteLine($"======================================================="); - Console.WriteLine(); - Console.WriteLine("Usage:"); - Console.WriteLine(" redux.exe -input -outformat [options]"); - Console.WriteLine(); - Console.WriteLine("Supported input formats:"); - Console.WriteLine(" .rfl (RF1/RF2 level)"); - Console.WriteLine(" .rfg (RF group)"); - Console.WriteLine(" .v3m (RF static mesh)"); - Console.WriteLine(" .obj (Wavefront OBJ)"); - Console.WriteLine(" .peg (RF2 texture packfile)"); - Console.WriteLine(" .rfa (RF animation - read only)"); - Console.WriteLine(); - Console.WriteLine("Supported output formats (specify with -outformat):"); - Console.WriteLine(" rfg (RF group)"); - Console.WriteLine(" v3m (RF static mesh)"); - Console.WriteLine(" obj (Wavefront OBJ)"); - Console.WriteLine(" tga (extract textures from a .peg into .tga)"); - Console.WriteLine(" png (extract textures from a .peg into .png)"); - Console.WriteLine(); - Console.WriteLine("Example:"); - Console.WriteLine(" redux.exe -input dm02.rfl -outformat obj"); - Console.WriteLine(" redux.exe -input dmc11.peg -outformat png"); - Console.WriteLine(); - Console.WriteLine("Other options (boolean flags):"); - Console.WriteLine(" -loglevel - Set logging verbosity level (default info)"); - Console.WriteLine(" -swapitem - Replace all items in exported .rfg with the specified class"); - Console.WriteLine(" -brushes - Export brush data from .rfl instead of static geometry (default false)"); - Console.WriteLine(" -ngons - Allow n-sided polygons in output (default false)"); - Console.WriteLine(" -flipnormals - Flip face normals during conversion (default false)"); + private static void ShowHelp() + { + Console.WriteLine(); + Console.WriteLine($"======================================================="); + Console.WriteLine($"======================================================="); + Console.WriteLine($"====== RED UX Toolkit by Goober - Version {Version} ======="); + Console.WriteLine($"======================================================="); + Console.WriteLine($"======================================================="); + Console.WriteLine(); + Console.WriteLine("Usage:"); + Console.WriteLine(" redux.exe -input -outformat [options]"); + Console.WriteLine(); + Console.WriteLine("Supported input formats:"); + Console.WriteLine(" .rfl (RF1/RF2 level)"); + Console.WriteLine(" .rfg (RF group)"); + Console.WriteLine(" .v3m (RF static mesh)"); + Console.WriteLine(" .obj (Wavefront OBJ)"); + Console.WriteLine(" .peg (RF2 texture packfile)"); + Console.WriteLine(" .rfa (RF animation - read only)"); + Console.WriteLine(); + Console.WriteLine("Supported output formats (specify with -outformat):"); + Console.WriteLine(" rfg (RF group)"); + Console.WriteLine(" v3m (RF static mesh)"); + Console.WriteLine(" obj (Wavefront OBJ)"); + Console.WriteLine(" tga (extract textures from a .peg into .tga)"); + Console.WriteLine(" png (extract textures from a .peg into .png)"); + Console.WriteLine(); + Console.WriteLine("Example:"); + Console.WriteLine(" redux.exe -input dm02.rfl -outformat obj"); + Console.WriteLine(" redux.exe -input dmc11.peg -outformat png"); + Console.WriteLine(); + Console.WriteLine("Other options (boolean flags):"); + Console.WriteLine(" -loglevel - Set logging verbosity level (default info)"); + Console.WriteLine(" -swapitem - Replace all items in exported .rfg with the specified class"); + Console.WriteLine(" -brushes - Export brush data from .rfl instead of static geometry (default false)"); + Console.WriteLine(" -ngons - Allow n-sided polygons in output (default false)"); + Console.WriteLine(" -flipnormals - Flip face normals during conversion (default false)"); Console.WriteLine(" -geomirror - Mirror geometry across the given global axis (RFG export only)"); Console.WriteLine(" -simplenames - Use simple Brush_UID names in exports (default false)"); - Console.WriteLine(" -textranslate - RF2 -> RF1 texture name translation (default false)"); - Console.WriteLine(" -geonodetail - Remove detail flag from RF2 geoable brushes (default false)"); - Console.WriteLine(" -portalfaces - Include portal faces (default true)"); - Console.WriteLine(" -detailfaces - Include detail faces (default true)"); - Console.WriteLine(" -alphafaces - Include faces with alpha textures (default true)"); - Console.WriteLine(" -holefaces - Include faces with shoot-through alpha textures (default true)"); - Console.WriteLine(" -liquidfaces - Include liquid faces (default false)"); - Console.WriteLine(" -skyfaces - Include show sky faces (default true)"); - Console.WriteLine(" -invisiblefaces - Include invisible faces (default true)"); - Console.WriteLine(" -ver / -help - Print version/usage/help information"); - Console.WriteLine(); - Console.WriteLine("Notes:"); - Console.WriteLine(" – defaults to true if no explicit value is given."); - Console.WriteLine(" - references class names from in the corresponding .tbl file. Use quotation marks if it has spaces."); - Console.WriteLine(" – If you supply “-input dmc11.peg -outformat png,” textures will be extracted as .png files in a folder named “dmc11/”."); - } - } + Console.WriteLine(" -textranslate - RF2 -> RF1 texture name translation (default false)"); + Console.WriteLine(" -geonodetail - Remove detail flag from RF2 geoable brushes (default false)"); + Console.WriteLine(" -portalfaces - Include portal faces (default true)"); + Console.WriteLine(" -detailfaces - Include detail faces (default true)"); + Console.WriteLine(" -alphafaces - Include faces with alpha textures (default true)"); + Console.WriteLine(" -holefaces - Include faces with shoot-through alpha textures (default true)"); + Console.WriteLine(" -liquidfaces - Include liquid faces (default false)"); + Console.WriteLine(" -skyfaces - Include show sky faces (default true)"); + Console.WriteLine(" -invisiblefaces - Include invisible faces (default true)"); + Console.WriteLine(" -ver / -help - Print version/usage/help information"); + Console.WriteLine(); + Console.WriteLine("Notes:"); + Console.WriteLine(" – defaults to true if no explicit value is given."); + Console.WriteLine(" - references class names from in the corresponding .tbl file. Use quotation marks if it has spaces."); + Console.WriteLine(" – If you supply “-input dmc11.peg -outformat png,” textures will be extracted as .png files in a folder named “dmc11/”."); + } + } } diff --git a/exporters/ObjExporter.cs b/exporters/ObjExporter.cs index 38bbafd..d55372f 100644 --- a/exporters/ObjExporter.cs +++ b/exporters/ObjExporter.cs @@ -8,411 +8,411 @@ namespace redux.exporters { - public static class ObjExporter - { - private const string logSrc = "ObjExporter"; - - public static void ExportObj(Mesh mesh, string objPath) - { - var objDir = Path.GetDirectoryName(objPath)!; - var baseName = Path.GetFileNameWithoutExtension(objPath); - var mtlName = baseName + ".mtl"; - var mtlPath = Path.Combine(objDir, mtlName); - - Logger.Info(logSrc, $"Writing OBJ to {objPath}"); - Logger.Info(logSrc, $"Writing MTL to {mtlPath}"); - - using var obj = new StreamWriter(objPath); - using var mtl = new StreamWriter(mtlPath); - - obj.WriteLine($"mtllib {mtlName}"); - Logger.Dev(logSrc, $"Added index for material {mtlPath}"); - - var writtenMats = new HashSet(); - int vOffset = 1, vtOffset = 1, vnOffset = 1; - - foreach (var brush in mesh.Brushes) - { - if (brush.Vertices.Count == 0) continue; - - var name = $"Brush_{brush.UID}"; - - // Add brush flags to the name - // Used to track flags between conversions - if (!Config.SimpleBrushNames) - { - bool isAir = (brush.Solid.Flags & (uint)SolidFlags.Air) != 0; - bool isDetail = (brush.Solid.Flags & (uint)SolidFlags.Detail) != 0; - bool isPortal = (brush.Solid.Flags & (uint)SolidFlags.Portal) != 0; - bool isEmitsSteam = (brush.Solid.Flags & (uint)SolidFlags.EmitsSteam) != 0; - bool isGeoable = (brush.Solid.Flags & (uint)SolidFlags.Geoable) != 0; - - name = $"Brush_{brush.UID}_{(isAir ? "A" : "S")}_{(isDetail ? "D" : "nD")}_{(isPortal ? "P" : "nP")}_{(isEmitsSteam ? "ES" : "nES")}_{(isGeoable ? "G" : "nG")}"; - } - obj.WriteLine($"o {name}"); - - // Merge verts by position in world space only - var posPool = new Dictionary< - (int x, int y, int z), - int // vi - >(); - - var V = new List(); // verts in world space - - // Merge verts by quantized UVs (u, v) per vertex index (vi): - var uvPool = new Dictionary< - (int vi, int u, int v), - int // vti - >(); - - var VT = new List(); // vert UVs - - var VN = new List(); // vert normals - - // Group indices per material - var facesByMat = new Dictionary>>(); - - foreach (var face in brush.Solid.Faces) - { - // Compute a flat normal for the face in world space - var w0 = Vector3.Transform( - brush.Vertices[face.Vertices[0]], - brush.RotationMatrix - ) + brush.Position; - - Vector3 flatNorm = Vector3.Zero; - for (int i = 1; i + 1 < face.Vertices.Count; i++) - { - var w1 = Vector3.Transform( - brush.Vertices[face.Vertices[i]], - brush.RotationMatrix - ) + brush.Position; - - var w2 = Vector3.Transform( - brush.Vertices[face.Vertices[i + 1]], - brush.RotationMatrix - ) + brush.Position; - - flatNorm += Vector3.Cross(w1 - w0, w2 - w0); - } - flatNorm = Vector3.Normalize(flatNorm); - - // Get the texture for the face - string tex = (face.TextureIndex >= 0 && face.TextureIndex < brush.Solid.Textures.Count) - ? brush.Solid.Textures[face.TextureIndex] - : "missing_texture.tga"; - string mat = Path.GetFileNameWithoutExtension(tex); - - if (!facesByMat.TryGetValue(mat, out var listOfFaces)) - facesByMat[mat] = listOfFaces = new List>(); - - // Compute the vertex indices (vi, vti, vni) for the face - var idxTriples = new List<(int vi, int vti, int vni)>(); - - for (int i = 0; i < face.Vertices.Count; i++) - { - // World space position of the vertex - var p = Vector3.Transform( - brush.Vertices[face.Vertices[i]], - brush.RotationMatrix - ) + brush.Position; - - // Quantize world space position to integer by 0.001 - var pKey = ( - x: (int)Math.Round(p.X * 1000f), - y: (int)Math.Round(p.Y * 1000f), - z: (int)Math.Round(p.Z * 1000f) - ); - - // If this is a new position, assign a new vi - if (!posPool.TryGetValue(pKey, out int vi)) - { - vi = V.Count; - posPool[pKey] = vi; - V.Add(p); - } - - // UV of the vertex - var uv = (i < face.UVs.Count) - ? face.UVs[i] - : Vector2.Zero; - - // Quantize UV to integer by 0.001 - var uvKey = ( - u: (int)Math.Round(uv.X * 1000f), - v: (int)Math.Round(uv.Y * 1000f) - ); - - // Make key for the uvPool (vi, quantized_u, quantized_v) - var fullUVKey = (vi: vi, u: uvKey.u, v: uvKey.v); - - // If this is a new UV for this vertex, assign a new vti - if (!uvPool.TryGetValue(fullUVKey, out int vti)) - { - vti = VT.Count; - uvPool[fullUVKey] = vti; - VT.Add(uv); - VN.Add(flatNorm); // store this face’s normal for (vi, vti) - } - - idxTriples.Add((vi, vti, vti)); - // Use the same index for vni so that vn[vti] is the flat normal - } - - // Triganulate if needed before adding the face - if (Config.TriangulatePolygons && idxTriples.Count > 3) - { - for (int i = 1; i + 1 < idxTriples.Count; i++) - listOfFaces.Add(new List<(int, int, int)> { - idxTriples[0], - idxTriples[i], - idxTriples[i + 1] - }); - } - else - { - listOfFaces.Add(idxTriples); - } - } - - // Write unique positions (V) - // OBJ format uses right-handed coordinate system, so flip X to match RF (left-handed) - foreach (var p in V) - obj.WriteLine( - $"v {(-p.X).ToString(CultureInfo.InvariantCulture)} " + - $"{p.Y.ToString(CultureInfo.InvariantCulture)} " + - $"{p.Z.ToString(CultureInfo.InvariantCulture)}" - ); - - // Write unique UVs (VT) - foreach (var uv in VT) - obj.WriteLine( - $"vt {uv.X.ToString(CultureInfo.InvariantCulture)} " + - $"{(1 - uv.Y).ToString(CultureInfo.InvariantCulture)}" - ); - - // Write unique normals (VN) - foreach (var n in VN) - obj.WriteLine( - $"vn {n.X.ToString(CultureInfo.InvariantCulture)} " + - $"{n.Y.ToString(CultureInfo.InvariantCulture)} " + - $"{n.Z.ToString(CultureInfo.InvariantCulture)}" - ); - - // Write faces grouped by material - // Use pooled indices and global offsets - foreach (var kv in facesByMat) - { - var mat = kv.Key; - if (writtenMats.Add(mat)) - { - mtl.WriteLine($"newmtl {mat}"); - mtl.WriteLine("Ka 1.000 1.000 1.000"); - mtl.WriteLine("Kd 1.000 1.000 1.000"); - mtl.WriteLine("Ks 0.000 0.000 0.000"); - mtl.WriteLine("d 1.0"); - mtl.WriteLine("illum 1"); - mtl.WriteLine($"map_Kd {mat}.tga"); - mtl.WriteLine(); - } - - obj.WriteLine($"usemtl {mat}"); - foreach (var face in kv.Value) - { - face.Reverse(); // correct winding so face normals point outward - var faceTokens = face - .Select(t => $"{t.vi + vOffset}/{t.vti + vtOffset}/{t.vni + vnOffset}") - .ToArray(); - obj.WriteLine("f " + string.Join(" ", faceTokens)); - } - } - - // Increment global vertex offsets before parsing next brush - vOffset += V.Count; - vtOffset += VT.Count; - vnOffset += VN.Count; - - foreach (var prop in brush.PropPoints) - { - // Prop object name: Prop__ - string safeName = Path.GetInvalidFileNameChars() - .Aggregate(prop.Name, (s, c) => s.Replace(c, '=')); - string objName = $"RFPP={brush.UID}={safeName}"; - obj.WriteLine($"o {objName}"); - - // Compute world‐space position of the PropPoint: - var wp = Vector3.Transform(prop.Position, brush.RotationMatrix) - + brush.Position; - - // Compute the “forward” direction from the quaternion, then flip it: - var forward = Vector3.Transform(new Vector3(0, 0, 1), prop.Orientation); - forward = Vector3.Normalize(forward); - forward = -forward; // flip direction - - float arrowLength = 1.0f; // shaft length - float wingLength = 0.25f; // arrowhead wing length - - // Tip of arrow = base + flipped_forward * arrowLength - var tip = wp + forward * arrowLength; - - // Choose a “right” vector perpendicular to flipped forward: - Vector3 worldUp = Vector3.UnitY; - Vector3 right = Vector3.Cross(forward, worldUp); - if (right.LengthSquared() < 1e-6f) - right = Vector3.Cross(forward, Vector3.UnitX); - right = Vector3.Normalize(right); - - // Arrowhead wings: - var wingBase = tip - forward * wingLength; - var wing1 = wingBase + right * wingLength; - var wing2 = wingBase - right * wingLength; - - // 1) Write base vertex: - obj.WriteLine( - $"v {(-wp.X).ToString(CultureInfo.InvariantCulture)} " + - $"{wp.Y.ToString(CultureInfo.InvariantCulture)} " + - $"{wp.Z.ToString(CultureInfo.InvariantCulture)}" - ); - - // 2) Write tip vertex: - obj.WriteLine( - $"v {(-tip.X).ToString(CultureInfo.InvariantCulture)} " + - $"{tip.Y.ToString(CultureInfo.InvariantCulture)} " + - $"{tip.Z.ToString(CultureInfo.InvariantCulture)}" - ); - - // 3) Write wing1 vertex: - obj.WriteLine( - $"v {(-wing1.X).ToString(CultureInfo.InvariantCulture)} " + - $"{wing1.Y.ToString(CultureInfo.InvariantCulture)} " + - $"{wing1.Z.ToString(CultureInfo.InvariantCulture)}" - ); - - // 4) Write wing2 vertex: - obj.WriteLine( - $"v {(-wing2.X).ToString(CultureInfo.InvariantCulture)} " + - $"{wing2.Y.ToString(CultureInfo.InvariantCulture)} " + - $"{wing2.Z.ToString(CultureInfo.InvariantCulture)}" - ); - - // 5) Draw shaft: line from base (index=vOffset) to tip (index=vOffset+1) - obj.WriteLine($"l {vOffset} {vOffset + 1}"); - - // 6) Draw arrowhead wings: - obj.WriteLine($"l {vOffset + 1} {vOffset + 2}"); // tip → wing1 - obj.WriteLine($"l {vOffset + 1} {vOffset + 3}"); // tip → wing2 - - // 7) (Optional) Write orientation quaternion as a comment - obj.WriteLine( - "# quat " + - $"{prop.Orientation.X.ToString(CultureInfo.InvariantCulture)} " + - $"{prop.Orientation.Y.ToString(CultureInfo.InvariantCulture)} " + - $"{prop.Orientation.Z.ToString(CultureInfo.InvariantCulture)} " + - $"{prop.Orientation.W.ToString(CultureInfo.InvariantCulture)}" - ); - - // 8) (Optional) Write parent‐index as a comment - obj.WriteLine($"# parent {prop.ParentIndex}"); - - Logger.Dev(logSrc, $"Wrote prop point {prop.Name} with parent {prop.ParentIndex}, " + - $"vertices vIndices={vOffset},{vOffset + 1},{vOffset + 2},{vOffset + 3}"); - - // 9) We used four “v” lines → bump vOffset by 4 - vOffset += 4; - } - - Logger.Dev(logSrc, $"Wrote brush {brush.UID}"); - } - - int stacks = 6, slices = 6; - for (int i = 0; i < mesh.CollisionSpheres.Count; i++) - { - var cs = mesh.CollisionSpheres[i]; - string safeName = Path.GetInvalidFileNameChars() - .Aggregate(cs.Name, (s, c) => s.Replace(c, '_')); - string objName = $"CSphere_{i}_{safeName}"; - obj.WriteLine($"o {objName}"); - - var center = cs.Position; - float radius = cs.Radius; - - // Generate (stacks+1)×slices vertices on sphere - // iLat = 0..stacks, iLon = 0..slices-1 - for (int iLat = 0; iLat <= stacks; iLat++) - { - float theta = (float)Math.PI * iLat / stacks; // 0..π - float sinT = (float)Math.Sin(theta); - float cosT = (float)Math.Cos(theta); - - for (int iLon = 0; iLon < slices; iLon++) - { - float phi = 2f * (float)Math.PI * iLon / slices; // 0..2π - float sinP = (float)Math.Sin(phi); - float cosP = (float)Math.Cos(phi); - - // Base sphere point (before scaling/translating): - // x = sinθ·cosφ, y = cosθ, z = sinθ·sinφ - var p = new Vector3( - sinT * cosP, - cosT, - sinT * sinP - ); - - // Scale by radius, translate by center - p = center + p * radius; - - // Flip X for OBJ right‐handed - obj.WriteLine( - $"v {(-p.X).ToString(CultureInfo.InvariantCulture)} " + - $"{p.Y.ToString(CultureInfo.InvariantCulture)} " + - $"{p.Z.ToString(CultureInfo.InvariantCulture)}" - ); - } - } - - // Build faces (triangles) between these vertices - // Indexing: baseIndex = vOffset, then - // index(iLat, iLon) = baseIndex + iLat*slices + iLon - int baseIndex = vOffset; - for (int iLat = 0; iLat < stacks; iLat++) - { - for (int iLon = 0; iLon < slices; iLon++) - { - int nextLon = (iLon + 1) % slices; - int i0 = baseIndex + iLat * slices + iLon; - int i1 = baseIndex + iLat * slices + nextLon; - int i2 = baseIndex + (iLat + 1) * slices + iLon; - int i3 = baseIndex + (iLat + 1) * slices + nextLon; - - if (iLat == 0) - { - // Top cap: one triangle (i0, i2, i3) - obj.WriteLine($"f {i0} {i2} {i3}"); - } - else if (iLat == stacks - 1) - { - // Bottom cap: one triangle (i0, i2, i1) - obj.WriteLine($"f {i0} {i2} {i1}"); - } - else - { - // Middle: two triangles - obj.WriteLine($"f {i0} {i2} {i1}"); - obj.WriteLine($"f {i1} {i2} {i3}"); - } - } - } - - // Write radius comment for reconstruction - obj.WriteLine($"# radius {radius.ToString(CultureInfo.InvariantCulture)}"); - - Logger.Dev(logSrc, $"Wrote CSphere '{cs.Name}' as sphere with center ({center.X},{center.Y},{center.Z}), " + - $"radius={radius}, vIndices={vOffset}..{vOffset + (stacks + 1) * slices - 1}"); - - vOffset += (stacks + 1) * slices; - } - - Logger.Info(logSrc, $"{objPath} and {mtlPath} written successfully."); - } - } + public static class ObjExporter + { + private const string logSrc = "ObjExporter"; + + public static void ExportObj(Mesh mesh, string objPath) + { + var objDir = Path.GetDirectoryName(objPath)!; + var baseName = Path.GetFileNameWithoutExtension(objPath); + var mtlName = baseName + ".mtl"; + var mtlPath = Path.Combine(objDir, mtlName); + + Logger.Info(logSrc, $"Writing OBJ to {objPath}"); + Logger.Info(logSrc, $"Writing MTL to {mtlPath}"); + + using var obj = new StreamWriter(objPath); + using var mtl = new StreamWriter(mtlPath); + + obj.WriteLine($"mtllib {mtlName}"); + Logger.Dev(logSrc, $"Added index for material {mtlPath}"); + + var writtenMats = new HashSet(); + int vOffset = 1, vtOffset = 1, vnOffset = 1; + + foreach (var brush in mesh.Brushes) + { + if (brush.Vertices.Count == 0) continue; + + var name = $"Brush_{brush.UID}"; + + // Add brush flags to the name + // Used to track flags between conversions + if (!Config.SimpleBrushNames) + { + bool isAir = (brush.Solid.Flags & (uint)SolidFlags.Air) != 0; + bool isDetail = (brush.Solid.Flags & (uint)SolidFlags.Detail) != 0; + bool isPortal = (brush.Solid.Flags & (uint)SolidFlags.Portal) != 0; + bool isEmitsSteam = (brush.Solid.Flags & (uint)SolidFlags.EmitsSteam) != 0; + bool isGeoable = (brush.Solid.Flags & (uint)SolidFlags.Geoable) != 0; + + name = $"Brush_{brush.UID}_{(isAir ? "A" : "S")}_{(isDetail ? "D" : "nD")}_{(isPortal ? "P" : "nP")}_{(isEmitsSteam ? "ES" : "nES")}_{(isGeoable ? "G" : "nG")}"; + } + obj.WriteLine($"o {name}"); + + // Merge verts by position in world space only + var posPool = new Dictionary< + (int x, int y, int z), + int // vi + >(); + + var V = new List(); // verts in world space + + // Merge verts by quantized UVs (u, v) per vertex index (vi): + var uvPool = new Dictionary< + (int vi, int u, int v), + int // vti + >(); + + var VT = new List(); // vert UVs + + var VN = new List(); // vert normals + + // Group indices per material + var facesByMat = new Dictionary>>(); + + foreach (var face in brush.Solid.Faces) + { + // Compute a flat normal for the face in world space + var w0 = Vector3.Transform( + brush.Vertices[face.Vertices[0]], + brush.RotationMatrix + ) + brush.Position; + + Vector3 flatNorm = Vector3.Zero; + for (int i = 1; i + 1 < face.Vertices.Count; i++) + { + var w1 = Vector3.Transform( + brush.Vertices[face.Vertices[i]], + brush.RotationMatrix + ) + brush.Position; + + var w2 = Vector3.Transform( + brush.Vertices[face.Vertices[i + 1]], + brush.RotationMatrix + ) + brush.Position; + + flatNorm += Vector3.Cross(w1 - w0, w2 - w0); + } + flatNorm = Vector3.Normalize(flatNorm); + + // Get the texture for the face + string tex = (face.TextureIndex >= 0 && face.TextureIndex < brush.Solid.Textures.Count) + ? brush.Solid.Textures[face.TextureIndex] + : "missing_texture.tga"; + string mat = Path.GetFileNameWithoutExtension(tex); + + if (!facesByMat.TryGetValue(mat, out var listOfFaces)) + facesByMat[mat] = listOfFaces = new List>(); + + // Compute the vertex indices (vi, vti, vni) for the face + var idxTriples = new List<(int vi, int vti, int vni)>(); + + for (int i = 0; i < face.Vertices.Count; i++) + { + // World space position of the vertex + var p = Vector3.Transform( + brush.Vertices[face.Vertices[i]], + brush.RotationMatrix + ) + brush.Position; + + // Quantize world space position to integer by 0.001 + var pKey = ( + x: (int)Math.Round(p.X * 1000f), + y: (int)Math.Round(p.Y * 1000f), + z: (int)Math.Round(p.Z * 1000f) + ); + + // If this is a new position, assign a new vi + if (!posPool.TryGetValue(pKey, out int vi)) + { + vi = V.Count; + posPool[pKey] = vi; + V.Add(p); + } + + // UV of the vertex + var uv = (i < face.UVs.Count) + ? face.UVs[i] + : Vector2.Zero; + + // Quantize UV to integer by 0.001 + var uvKey = ( + u: (int)Math.Round(uv.X * 1000f), + v: (int)Math.Round(uv.Y * 1000f) + ); + + // Make key for the uvPool (vi, quantized_u, quantized_v) + var fullUVKey = (vi: vi, u: uvKey.u, v: uvKey.v); + + // If this is a new UV for this vertex, assign a new vti + if (!uvPool.TryGetValue(fullUVKey, out int vti)) + { + vti = VT.Count; + uvPool[fullUVKey] = vti; + VT.Add(uv); + VN.Add(flatNorm); // store this face’s normal for (vi, vti) + } + + idxTriples.Add((vi, vti, vti)); + // Use the same index for vni so that vn[vti] is the flat normal + } + + // Triganulate if needed before adding the face + if (Config.TriangulatePolygons && idxTriples.Count > 3) + { + for (int i = 1; i + 1 < idxTriples.Count; i++) + listOfFaces.Add(new List<(int, int, int)> { + idxTriples[0], + idxTriples[i], + idxTriples[i + 1] + }); + } + else + { + listOfFaces.Add(idxTriples); + } + } + + // Write unique positions (V) + // OBJ format uses right-handed coordinate system, so flip X to match RF (left-handed) + foreach (var p in V) + obj.WriteLine( + $"v {(-p.X).ToString(CultureInfo.InvariantCulture)} " + + $"{p.Y.ToString(CultureInfo.InvariantCulture)} " + + $"{p.Z.ToString(CultureInfo.InvariantCulture)}" + ); + + // Write unique UVs (VT) + foreach (var uv in VT) + obj.WriteLine( + $"vt {uv.X.ToString(CultureInfo.InvariantCulture)} " + + $"{(1 - uv.Y).ToString(CultureInfo.InvariantCulture)}" + ); + + // Write unique normals (VN) + foreach (var n in VN) + obj.WriteLine( + $"vn {n.X.ToString(CultureInfo.InvariantCulture)} " + + $"{n.Y.ToString(CultureInfo.InvariantCulture)} " + + $"{n.Z.ToString(CultureInfo.InvariantCulture)}" + ); + + // Write faces grouped by material + // Use pooled indices and global offsets + foreach (var kv in facesByMat) + { + var mat = kv.Key; + if (writtenMats.Add(mat)) + { + mtl.WriteLine($"newmtl {mat}"); + mtl.WriteLine("Ka 1.000 1.000 1.000"); + mtl.WriteLine("Kd 1.000 1.000 1.000"); + mtl.WriteLine("Ks 0.000 0.000 0.000"); + mtl.WriteLine("d 1.0"); + mtl.WriteLine("illum 1"); + mtl.WriteLine($"map_Kd {mat}.tga"); + mtl.WriteLine(); + } + + obj.WriteLine($"usemtl {mat}"); + foreach (var face in kv.Value) + { + face.Reverse(); // correct winding so face normals point outward + var faceTokens = face + .Select(t => $"{t.vi + vOffset}/{t.vti + vtOffset}/{t.vni + vnOffset}") + .ToArray(); + obj.WriteLine("f " + string.Join(" ", faceTokens)); + } + } + + // Increment global vertex offsets before parsing next brush + vOffset += V.Count; + vtOffset += VT.Count; + vnOffset += VN.Count; + + foreach (var prop in brush.PropPoints) + { + // Prop object name: Prop__ + string safeName = Path.GetInvalidFileNameChars() + .Aggregate(prop.Name, (s, c) => s.Replace(c, '=')); + string objName = $"RFPP={brush.UID}={safeName}"; + obj.WriteLine($"o {objName}"); + + // Compute world‐space position of the PropPoint: + var wp = Vector3.Transform(prop.Position, brush.RotationMatrix) + + brush.Position; + + // Compute the “forward” direction from the quaternion, then flip it: + var forward = Vector3.Transform(new Vector3(0, 0, 1), prop.Orientation); + forward = Vector3.Normalize(forward); + forward = -forward; // flip direction + + float arrowLength = 1.0f; // shaft length + float wingLength = 0.25f; // arrowhead wing length + + // Tip of arrow = base + flipped_forward * arrowLength + var tip = wp + forward * arrowLength; + + // Choose a “right” vector perpendicular to flipped forward: + Vector3 worldUp = Vector3.UnitY; + Vector3 right = Vector3.Cross(forward, worldUp); + if (right.LengthSquared() < 1e-6f) + right = Vector3.Cross(forward, Vector3.UnitX); + right = Vector3.Normalize(right); + + // Arrowhead wings: + var wingBase = tip - forward * wingLength; + var wing1 = wingBase + right * wingLength; + var wing2 = wingBase - right * wingLength; + + // 1) Write base vertex: + obj.WriteLine( + $"v {(-wp.X).ToString(CultureInfo.InvariantCulture)} " + + $"{wp.Y.ToString(CultureInfo.InvariantCulture)} " + + $"{wp.Z.ToString(CultureInfo.InvariantCulture)}" + ); + + // 2) Write tip vertex: + obj.WriteLine( + $"v {(-tip.X).ToString(CultureInfo.InvariantCulture)} " + + $"{tip.Y.ToString(CultureInfo.InvariantCulture)} " + + $"{tip.Z.ToString(CultureInfo.InvariantCulture)}" + ); + + // 3) Write wing1 vertex: + obj.WriteLine( + $"v {(-wing1.X).ToString(CultureInfo.InvariantCulture)} " + + $"{wing1.Y.ToString(CultureInfo.InvariantCulture)} " + + $"{wing1.Z.ToString(CultureInfo.InvariantCulture)}" + ); + + // 4) Write wing2 vertex: + obj.WriteLine( + $"v {(-wing2.X).ToString(CultureInfo.InvariantCulture)} " + + $"{wing2.Y.ToString(CultureInfo.InvariantCulture)} " + + $"{wing2.Z.ToString(CultureInfo.InvariantCulture)}" + ); + + // 5) Draw shaft: line from base (index=vOffset) to tip (index=vOffset+1) + obj.WriteLine($"l {vOffset} {vOffset + 1}"); + + // 6) Draw arrowhead wings: + obj.WriteLine($"l {vOffset + 1} {vOffset + 2}"); // tip → wing1 + obj.WriteLine($"l {vOffset + 1} {vOffset + 3}"); // tip → wing2 + + // 7) (Optional) Write orientation quaternion as a comment + obj.WriteLine( + "# quat " + + $"{prop.Orientation.X.ToString(CultureInfo.InvariantCulture)} " + + $"{prop.Orientation.Y.ToString(CultureInfo.InvariantCulture)} " + + $"{prop.Orientation.Z.ToString(CultureInfo.InvariantCulture)} " + + $"{prop.Orientation.W.ToString(CultureInfo.InvariantCulture)}" + ); + + // 8) (Optional) Write parent‐index as a comment + obj.WriteLine($"# parent {prop.ParentIndex}"); + + Logger.Dev(logSrc, $"Wrote prop point {prop.Name} with parent {prop.ParentIndex}, " + + $"vertices vIndices={vOffset},{vOffset + 1},{vOffset + 2},{vOffset + 3}"); + + // 9) We used four “v” lines → bump vOffset by 4 + vOffset += 4; + } + + Logger.Dev(logSrc, $"Wrote brush {brush.UID}"); + } + + int stacks = 6, slices = 6; + for (int i = 0; i < mesh.CollisionSpheres.Count; i++) + { + var cs = mesh.CollisionSpheres[i]; + string safeName = Path.GetInvalidFileNameChars() + .Aggregate(cs.Name, (s, c) => s.Replace(c, '_')); + string objName = $"CSphere_{i}_{safeName}"; + obj.WriteLine($"o {objName}"); + + var center = cs.Position; + float radius = cs.Radius; + + // Generate (stacks+1)×slices vertices on sphere + // iLat = 0..stacks, iLon = 0..slices-1 + for (int iLat = 0; iLat <= stacks; iLat++) + { + float theta = (float)Math.PI * iLat / stacks; // 0..π + float sinT = (float)Math.Sin(theta); + float cosT = (float)Math.Cos(theta); + + for (int iLon = 0; iLon < slices; iLon++) + { + float phi = 2f * (float)Math.PI * iLon / slices; // 0..2π + float sinP = (float)Math.Sin(phi); + float cosP = (float)Math.Cos(phi); + + // Base sphere point (before scaling/translating): + // x = sinθ·cosφ, y = cosθ, z = sinθ·sinφ + var p = new Vector3( + sinT * cosP, + cosT, + sinT * sinP + ); + + // Scale by radius, translate by center + p = center + p * radius; + + // Flip X for OBJ right‐handed + obj.WriteLine( + $"v {(-p.X).ToString(CultureInfo.InvariantCulture)} " + + $"{p.Y.ToString(CultureInfo.InvariantCulture)} " + + $"{p.Z.ToString(CultureInfo.InvariantCulture)}" + ); + } + } + + // Build faces (triangles) between these vertices + // Indexing: baseIndex = vOffset, then + // index(iLat, iLon) = baseIndex + iLat*slices + iLon + int baseIndex = vOffset; + for (int iLat = 0; iLat < stacks; iLat++) + { + for (int iLon = 0; iLon < slices; iLon++) + { + int nextLon = (iLon + 1) % slices; + int i0 = baseIndex + iLat * slices + iLon; + int i1 = baseIndex + iLat * slices + nextLon; + int i2 = baseIndex + (iLat + 1) * slices + iLon; + int i3 = baseIndex + (iLat + 1) * slices + nextLon; + + if (iLat == 0) + { + // Top cap: one triangle (i0, i2, i3) + obj.WriteLine($"f {i0} {i2} {i3}"); + } + else if (iLat == stacks - 1) + { + // Bottom cap: one triangle (i0, i2, i1) + obj.WriteLine($"f {i0} {i2} {i1}"); + } + else + { + // Middle: two triangles + obj.WriteLine($"f {i0} {i2} {i1}"); + obj.WriteLine($"f {i1} {i2} {i3}"); + } + } + } + + // Write radius comment for reconstruction + obj.WriteLine($"# radius {radius.ToString(CultureInfo.InvariantCulture)}"); + + Logger.Dev(logSrc, $"Wrote CSphere '{cs.Name}' as sphere with center ({center.X},{center.Y},{center.Z}), " + + $"radius={radius}, vIndices={vOffset}..{vOffset + (stacks + 1) * slices - 1}"); + + vOffset += (stacks + 1) * slices; + } + + Logger.Info(logSrc, $"{objPath} and {mtlPath} written successfully."); + } + } } diff --git a/exporters/RfgExporter.cs b/exporters/RfgExporter.cs index eeef23e..92cb671 100644 --- a/exporters/RfgExporter.cs +++ b/exporters/RfgExporter.cs @@ -7,9 +7,9 @@ namespace redux.exporters { - public static class RfgExporter - { - private const string logSrc = "RfgExporter"; + public static class RfgExporter + { + private const string logSrc = "RfgExporter"; // Map RF "world axis" for positions/geometry so it matches rotation mirroring. // If you're seeing X behave like Z for positions, swap X<->Z here. @@ -85,38 +85,38 @@ private static Matrix4x4 MirrorRotationAboutOrigin(Matrix4x4 R, Config.MirrorAxi } public static void ExportRfg(Mesh mesh, string outputPath) - { - Logger.Info(logSrc, $"Writing RFG to {outputPath}"); - - using var stream = File.Create(outputPath); - using var writer = new BinaryWriter(stream); + { + Logger.Info(logSrc, $"Writing RFG to {outputPath}"); + + using var stream = File.Create(outputPath); + using var writer = new BinaryWriter(stream); - writer.Write(0xD43DD00D); // Magic - writer.Write(0x0000012C); // Version - //writer.Write(1); // Num groups + writer.Write(0xD43DD00D); // Magic + writer.Write(0x0000012C); // Version + //writer.Write(1); // Num groups - int totalGroups = 1 + mesh.Groups.Count; - writer.Write(totalGroups); + int totalGroups = 1 + mesh.Groups.Count; + writer.Write(totalGroups); - //Utils.WriteVString(writer, "static_group"); - //writer.Write((byte)0); // is_moving = false + //Utils.WriteVString(writer, "static_group"); + //writer.Write((byte)0); // is_moving = false - var movingBrushUIDs = new HashSet( - mesh.Groups.SelectMany(g => g.Brushes) - ); - var staticBrushes = mesh.Brushes - .Where(b => !movingBrushUIDs.Contains(b.UID)) - .ToList(); + var movingBrushUIDs = new HashSet( + mesh.Groups.SelectMany(g => g.Brushes) + ); + var staticBrushes = mesh.Brushes + .Where(b => !movingBrushUIDs.Contains(b.UID)) + .ToList(); - // write the “static_group” header - Utils.WriteVString(writer, "static_group"); - writer.Write((byte)0); // is_moving = false + // write the “static_group” header + Utils.WriteVString(writer, "static_group"); + writer.Write((byte)0); // is_moving = false - // Fold coronas into clutter section if a clutter replacement name is specified (since RF1 doesn't have corona objects like RF2) - HandleCoronaClutterReplacements(mesh); + // Fold coronas into clutter section if a clutter replacement name is specified (since RF1 doesn't have corona objects like RF2) + HandleCoronaClutterReplacements(mesh); - // write only the filtered brushes - WriteBrushesSection(writer, staticBrushes); + // write only the filtered brushes + WriteBrushesSection(writer, staticBrushes); // Mirror settings for the static group bool mirrorActive = Config.GeoMirror != Config.MirrorAxis.None; @@ -128,17 +128,17 @@ public static void ExportRfg(Mesh mesh, string outputPath) writer.Write(0); // 0 geo regions - // Write lights section - Logger.Dev(logSrc, $"Writing lights: count={mesh.Lights.Count}"); - writer.Write(mesh.Lights.Count); - foreach (var light in mesh.Lights) - { - Logger.Dev(logSrc, $" → light UID={light.UID}, range={light.Range}, intensity={light.OnIntensity}"); + // Write lights section + Logger.Dev(logSrc, $"Writing lights: count={mesh.Lights.Count}"); + writer.Write(mesh.Lights.Count); + foreach (var light in mesh.Lights) + { + Logger.Dev(logSrc, $" → light UID={light.UID}, range={light.Range}, intensity={light.OnIntensity}"); - // UID - writer.Write(light.UID); - // class name - Utils.WriteVString(writer, light.ClassName); + // UID + writer.Write(light.UID); + // class name + Utils.WriteVString(writer, light.ClassName); // position var lightPos = mirrorActive ? MirrorPosAboutPivot(light.Position, posAxis, 0f) : light.Position; @@ -147,126 +147,126 @@ public static void ExportRfg(Mesh mesh, string outputPath) // 3×3 rotation matrix var R = mirrorActive ? MirrorRotationAboutOrigin(light.Rotation, Config.GeoMirror) : light.Rotation; writer.Write(R.M11); writer.Write(R.M12); writer.Write(R.M13); - writer.Write(R.M21); writer.Write(R.M22); writer.Write(R.M23); - writer.Write(R.M31); writer.Write(R.M32); writer.Write(R.M33); - - // script name - Utils.WriteVString(writer, light.ScriptName); - - // hidden flag - writer.Write((byte)(light.HiddenInEditor ? 1 : 0)); - - // raw bitflags - uint rawFlags = 0; - if (light.Dynamic) rawFlags |= 0x00000001; - if (light.Fade) rawFlags |= 0x00000002; - if (light.ShadowCasting) rawFlags |= 0x00000004; - if (light.IsEnabled) rawFlags |= 0x00000008; - rawFlags |= ((uint)light.Type & 0x3) << 4; - rawFlags |= ((uint)light.InitialState & 0xF) << 8; - if (light.RuntimeShadow) rawFlags |= 0x00002000; - writer.Write(rawFlags); - - // color (RGBA bytes) - writer.Write((byte)(light.Color.X * 255)); - writer.Write((byte)(light.Color.Y * 255)); - writer.Write((byte)(light.Color.Z * 255)); - writer.Write((byte)(light.Color.W * 255)); - - // floats - writer.Write(light.Range); - writer.Write(light.FOV); - writer.Write(light.FOVDropoff); - writer.Write(light.IntensityAtMaxRange); - - // dropoff type - writer.Write(light.DropoffType); - - // tube width - writer.Write(light.TubeLightWidth); - - // on/off timing - writer.Write(light.OnIntensity); - writer.Write(light.OnTime); - writer.Write(light.OnTimeVariation); - writer.Write(light.OffIntensity); - writer.Write(light.OffTime); - writer.Write(light.OffTimeVariation); - } - - writer.Write(0); // 0 cutscene cameras - writer.Write(0); // 0 cutscene path nodes - writer.Write(0); // 0 ambient sounds - - // Write events section - Logger.Dev(logSrc, $"Writing events: count={mesh.Events.Count}"); - writer.Write(mesh.Events.Count); - foreach (var ev in mesh.Events) - { - Logger.Dev(logSrc, $" → event UID={ev.UID}, type={ev.ClassName}"); - - // UID - writer.Write(ev.UID); - - // class name - Utils.WriteVString(writer, ev.ClassName); - - // position - writer.Write(ev.Position.X); - writer.Write(ev.Position.Y); - writer.Write(ev.Position.Z); - - // script name - Utils.WriteVString(writer, ev.ScriptName); - - // hidden flag - writer.Write((byte)(ev.HiddenInEditor ? 1 : 0)); - - // delay - writer.Write(ev.Delay); - - // two bools - writer.Write((byte)(ev.Bool1 ? 1 : 0)); - writer.Write((byte)(ev.Bool2 ? 1 : 0)); - - // two ints - writer.Write(ev.Int1); - writer.Write(ev.Int2); - - // two floats - writer.Write(ev.Float1); - writer.Write(ev.Float2); - - // two strings - Utils.WriteVString(writer, ev.Str1); - Utils.WriteVString(writer, ev.Str2); - - // links (uid_list) - writer.Write(ev.Links.Count); - foreach (var link in ev.Links) - writer.Write(link); - - // optional rotation - if (ev.HasRotation) - { - writer.Write(ev.Rotation.M11); writer.Write(ev.Rotation.M12); writer.Write(ev.Rotation.M13); - writer.Write(ev.Rotation.M21); writer.Write(ev.Rotation.M22); writer.Write(ev.Rotation.M23); - writer.Write(ev.Rotation.M31); writer.Write(ev.Rotation.M32); writer.Write(ev.Rotation.M33); - } - - // color - writer.Write(ev.RawColor); - } - - // Write MP respawn points section - Logger.Dev(logSrc, $"Writing MP respawn points: count={mesh.MPRespawnPoints.Count}"); - writer.Write(mesh.MPRespawnPoints.Count); - foreach (var pt in mesh.MPRespawnPoints) - { - Logger.Dev(logSrc, $" → MPSpawn UID={pt.UID}, Red={pt.RedTeam}, Blue={pt.BlueTeam}, Bot={pt.IsBot}"); - - // UID - writer.Write(pt.UID); + writer.Write(R.M21); writer.Write(R.M22); writer.Write(R.M23); + writer.Write(R.M31); writer.Write(R.M32); writer.Write(R.M33); + + // script name + Utils.WriteVString(writer, light.ScriptName); + + // hidden flag + writer.Write((byte)(light.HiddenInEditor ? 1 : 0)); + + // raw bitflags + uint rawFlags = 0; + if (light.Dynamic) rawFlags |= 0x00000001; + if (light.Fade) rawFlags |= 0x00000002; + if (light.ShadowCasting) rawFlags |= 0x00000004; + if (light.IsEnabled) rawFlags |= 0x00000008; + rawFlags |= ((uint)light.Type & 0x3) << 4; + rawFlags |= ((uint)light.InitialState & 0xF) << 8; + if (light.RuntimeShadow) rawFlags |= 0x00002000; + writer.Write(rawFlags); + + // color (RGBA bytes) + writer.Write((byte)(light.Color.X * 255)); + writer.Write((byte)(light.Color.Y * 255)); + writer.Write((byte)(light.Color.Z * 255)); + writer.Write((byte)(light.Color.W * 255)); + + // floats + writer.Write(light.Range); + writer.Write(light.FOV); + writer.Write(light.FOVDropoff); + writer.Write(light.IntensityAtMaxRange); + + // dropoff type + writer.Write(light.DropoffType); + + // tube width + writer.Write(light.TubeLightWidth); + + // on/off timing + writer.Write(light.OnIntensity); + writer.Write(light.OnTime); + writer.Write(light.OnTimeVariation); + writer.Write(light.OffIntensity); + writer.Write(light.OffTime); + writer.Write(light.OffTimeVariation); + } + + writer.Write(0); // 0 cutscene cameras + writer.Write(0); // 0 cutscene path nodes + writer.Write(0); // 0 ambient sounds + + // Write events section + Logger.Dev(logSrc, $"Writing events: count={mesh.Events.Count}"); + writer.Write(mesh.Events.Count); + foreach (var ev in mesh.Events) + { + Logger.Dev(logSrc, $" → event UID={ev.UID}, type={ev.ClassName}"); + + // UID + writer.Write(ev.UID); + + // class name + Utils.WriteVString(writer, ev.ClassName); + + // position + writer.Write(ev.Position.X); + writer.Write(ev.Position.Y); + writer.Write(ev.Position.Z); + + // script name + Utils.WriteVString(writer, ev.ScriptName); + + // hidden flag + writer.Write((byte)(ev.HiddenInEditor ? 1 : 0)); + + // delay + writer.Write(ev.Delay); + + // two bools + writer.Write((byte)(ev.Bool1 ? 1 : 0)); + writer.Write((byte)(ev.Bool2 ? 1 : 0)); + + // two ints + writer.Write(ev.Int1); + writer.Write(ev.Int2); + + // two floats + writer.Write(ev.Float1); + writer.Write(ev.Float2); + + // two strings + Utils.WriteVString(writer, ev.Str1); + Utils.WriteVString(writer, ev.Str2); + + // links (uid_list) + writer.Write(ev.Links.Count); + foreach (var link in ev.Links) + writer.Write(link); + + // optional rotation + if (ev.HasRotation) + { + writer.Write(ev.Rotation.M11); writer.Write(ev.Rotation.M12); writer.Write(ev.Rotation.M13); + writer.Write(ev.Rotation.M21); writer.Write(ev.Rotation.M22); writer.Write(ev.Rotation.M23); + writer.Write(ev.Rotation.M31); writer.Write(ev.Rotation.M32); writer.Write(ev.Rotation.M33); + } + + // color + writer.Write(ev.RawColor); + } + + // Write MP respawn points section + Logger.Dev(logSrc, $"Writing MP respawn points: count={mesh.MPRespawnPoints.Count}"); + writer.Write(mesh.MPRespawnPoints.Count); + foreach (var pt in mesh.MPRespawnPoints) + { + Logger.Dev(logSrc, $" → MPSpawn UID={pt.UID}, Red={pt.RedTeam}, Blue={pt.BlueTeam}, Bot={pt.IsBot}"); + + // UID + writer.Write(pt.UID); // Position var ptPos = mirrorActive ? MirrorPosAboutPivot(pt.Position, posAxis, 0f) : pt.Position; @@ -283,111 +283,111 @@ public static void ExportRfg(Mesh mesh, string outputPath) // Script name Utils.WriteVString(writer, pt.ScriptName); - // Hidden flag - writer.Write((byte)(pt.HiddenInEditor ? 1 : 0)); - - // Team ID - writer.Write(pt.TeamID); - - // Red/Blue/Bot flags - writer.Write((byte)(pt.RedTeam ? 1 : 0)); - writer.Write((byte)(pt.BlueTeam ? 1 : 0)); - writer.Write((byte)(pt.IsBot ? 1 : 0)); - } - - // --- nav points section --- - Logger.Dev(logSrc, $"Writing nav points: count={mesh.NavPoints.Count}"); - writer.Write(mesh.NavPoints.Count); - for (int i = 0; i < mesh.NavPoints.Count; i++) - { - var np = mesh.NavPoints[i]; - - // log - Logger.Dev(logSrc, - $" → NavPoint[{i}] " + - $"UID={np.UID}, HiddenInEditor={(np.HiddenInEditor ? 1 : 0)}, " + - $"Height={np.Height:F3}, Pos=({np.Position.X:F3},{np.Position.Y:F3},{np.Position.Z:F3}), " + - $"Radius={np.Radius:F3}, Type={(int)np.Type}, Directional={(np.Directional ? 1 : 0)}, " + - $"Cover={(np.Cover ? 1 : 0)}, Hide={(np.Hide ? 1 : 0)}, Crunch={(np.Crunch ? 1 : 0)}, " + - $"PauseTime={np.PauseTime:F3}, Links=[{string.Join(",", np.LinkIndices)}]" - ); - - // UID - writer.Write(np.UID); - - // hidden_in_editor (u1) - writer.Write((byte)(np.HiddenInEditor ? 1 : 0)); - - // height (f4) - writer.Write(np.Height); - - // position (vec3) - writer.Write(np.Position.X); - writer.Write(np.Position.Y); - writer.Write(np.Position.Z); - - // radius (f4) - writer.Write(np.Radius); - - // type (s4) - writer.Write((int)np.Type); - - // directional (u1) - writer.Write((byte)(np.Directional ? 1 : 0)); - - // if directional, 3×3 rotation matrix (forward, right, up) - if (np.Directional && np.Rotation.HasValue) - { - var R = np.Rotation.Value; - // forward = third row - writer.Write(R.M31); writer.Write(R.M32); writer.Write(R.M33); - // right = first row - writer.Write(R.M11); writer.Write(R.M12); writer.Write(R.M13); - // up = second row - writer.Write(R.M21); writer.Write(R.M22); writer.Write(R.M23); - } - - // … after writing directional and optional rot … - - // cover, hide, crunch (three u1s) - writer.Write((byte)(np.Cover ? 1 : 0)); - writer.Write((byte)(np.Hide ? 1 : 0)); - writer.Write((byte)(np.Crunch ? 1 : 0)); - - // pause_time (f4) - writer.Write(np.PauseTime); - - // now the uid_list - writer.Write(np.LinkIndices.Count); - foreach (var link in np.LinkIndices) - writer.Write(link); - - } - - writer.Write(0); // 0 entities - - // Write items section - Logger.Dev(logSrc, $"Writing items: count={mesh.Items.Count}"); - writer.Write(mesh.Items.Count); - foreach (var it in mesh.Items) - { - Logger.Dev(logSrc, $" → item UID={it.UID}, class={it.ClassName}"); - - // UID - writer.Write(it.UID); - - if (!string.IsNullOrEmpty(Config.ReplacementItemName)) - { - Logger.Info(logSrc, $"Swapping item class for UID {it.UID} from {it.ClassName} to {Config.ReplacementItemName}"); - } - - // pick either the original or the override - var exportName = string.IsNullOrEmpty(Config.ReplacementItemName) - ? it.ClassName - : Config.ReplacementItemName; - - // class name (either original or override) - Utils.WriteVString(writer, exportName); + // Hidden flag + writer.Write((byte)(pt.HiddenInEditor ? 1 : 0)); + + // Team ID + writer.Write(pt.TeamID); + + // Red/Blue/Bot flags + writer.Write((byte)(pt.RedTeam ? 1 : 0)); + writer.Write((byte)(pt.BlueTeam ? 1 : 0)); + writer.Write((byte)(pt.IsBot ? 1 : 0)); + } + + // --- nav points section --- + Logger.Dev(logSrc, $"Writing nav points: count={mesh.NavPoints.Count}"); + writer.Write(mesh.NavPoints.Count); + for (int i = 0; i < mesh.NavPoints.Count; i++) + { + var np = mesh.NavPoints[i]; + + // log + Logger.Dev(logSrc, + $" → NavPoint[{i}] " + + $"UID={np.UID}, HiddenInEditor={(np.HiddenInEditor ? 1 : 0)}, " + + $"Height={np.Height:F3}, Pos=({np.Position.X:F3},{np.Position.Y:F3},{np.Position.Z:F3}), " + + $"Radius={np.Radius:F3}, Type={(int)np.Type}, Directional={(np.Directional ? 1 : 0)}, " + + $"Cover={(np.Cover ? 1 : 0)}, Hide={(np.Hide ? 1 : 0)}, Crunch={(np.Crunch ? 1 : 0)}, " + + $"PauseTime={np.PauseTime:F3}, Links=[{string.Join(",", np.LinkIndices)}]" + ); + + // UID + writer.Write(np.UID); + + // hidden_in_editor (u1) + writer.Write((byte)(np.HiddenInEditor ? 1 : 0)); + + // height (f4) + writer.Write(np.Height); + + // position (vec3) + writer.Write(np.Position.X); + writer.Write(np.Position.Y); + writer.Write(np.Position.Z); + + // radius (f4) + writer.Write(np.Radius); + + // type (s4) + writer.Write((int)np.Type); + + // directional (u1) + writer.Write((byte)(np.Directional ? 1 : 0)); + + // if directional, 3×3 rotation matrix (forward, right, up) + if (np.Directional && np.Rotation.HasValue) + { + var R = np.Rotation.Value; + // forward = third row + writer.Write(R.M31); writer.Write(R.M32); writer.Write(R.M33); + // right = first row + writer.Write(R.M11); writer.Write(R.M12); writer.Write(R.M13); + // up = second row + writer.Write(R.M21); writer.Write(R.M22); writer.Write(R.M23); + } + + // … after writing directional and optional rot … + + // cover, hide, crunch (three u1s) + writer.Write((byte)(np.Cover ? 1 : 0)); + writer.Write((byte)(np.Hide ? 1 : 0)); + writer.Write((byte)(np.Crunch ? 1 : 0)); + + // pause_time (f4) + writer.Write(np.PauseTime); + + // now the uid_list + writer.Write(np.LinkIndices.Count); + foreach (var link in np.LinkIndices) + writer.Write(link); + + } + + writer.Write(0); // 0 entities + + // Write items section + Logger.Dev(logSrc, $"Writing items: count={mesh.Items.Count}"); + writer.Write(mesh.Items.Count); + foreach (var it in mesh.Items) + { + Logger.Dev(logSrc, $" → item UID={it.UID}, class={it.ClassName}"); + + // UID + writer.Write(it.UID); + + if (!string.IsNullOrEmpty(Config.ReplacementItemName)) + { + Logger.Info(logSrc, $"Swapping item class for UID {it.UID} from {it.ClassName} to {Config.ReplacementItemName}"); + } + + // pick either the original or the override + var exportName = string.IsNullOrEmpty(Config.ReplacementItemName) + ? it.ClassName + : Config.ReplacementItemName; + + // class name (either original or override) + Utils.WriteVString(writer, exportName); // position var itPos = mirrorActive ? MirrorPosAboutPivot(it.Position, posAxis, 0f) : it.Position; @@ -396,32 +396,32 @@ public static void ExportRfg(Mesh mesh, string outputPath) // 3×3 rotation matrix var R = mirrorActive ? MirrorRotationAboutOrigin(it.Rotation, Config.GeoMirror) : it.Rotation; - writer.Write(R.M11); writer.Write(R.M12); writer.Write(R.M13); - writer.Write(R.M21); writer.Write(R.M22); writer.Write(R.M23); - writer.Write(R.M31); writer.Write(R.M32); writer.Write(R.M33); + writer.Write(R.M11); writer.Write(R.M12); writer.Write(R.M13); + writer.Write(R.M21); writer.Write(R.M22); writer.Write(R.M23); + writer.Write(R.M31); writer.Write(R.M32); writer.Write(R.M33); - // script name - Utils.WriteVString(writer, it.ScriptName); + // script name + Utils.WriteVString(writer, it.ScriptName); - // hidden flag - writer.Write((byte)(it.HiddenInEditor ? 1 : 0)); + // hidden flag + writer.Write((byte)(it.HiddenInEditor ? 1 : 0)); - // count, respawn time, team ID - writer.Write(it.Count); - writer.Write(it.RespawnTime); - writer.Write(it.TeamID); - } + // count, respawn time, team ID + writer.Write(it.Count); + writer.Write(it.RespawnTime); + writer.Write(it.TeamID); + } - // --- Clutters Section --- - Logger.Dev(logSrc, $"Writing clutters: count={mesh.Clutters.Count}"); - writer.Write(mesh.Clutters.Count); - foreach (var c in mesh.Clutters) - { - // UID - writer.Write(c.UID); + // --- Clutters Section --- + Logger.Dev(logSrc, $"Writing clutters: count={mesh.Clutters.Count}"); + writer.Write(mesh.Clutters.Count); + foreach (var c in mesh.Clutters) + { + // UID + writer.Write(c.UID); - // class_name - Utils.WriteVString(writer, c.ClassName); + // class_name + Utils.WriteVString(writer, c.ClassName); var cPos = mirrorActive ? MirrorPosAboutPivot(c.Position, posAxis, 0f) : c.Position; writer.Write(cPos.X); @@ -440,58 +440,58 @@ public static void ExportRfg(Mesh mesh, string outputPath) // script_name Utils.WriteVString(writer, c.ScriptName); - // hidden_in_editor flag - writer.Write((byte)(c.HiddenInEditor ? 1 : 0)); + // hidden_in_editor flag + writer.Write((byte)(c.HiddenInEditor ? 1 : 0)); - // the “unknown” int - //writer.Write(c.Unknown); - writer.Write(0); + // the “unknown” int + //writer.Write(c.Unknown); + writer.Write(0); - // skin - Utils.WriteVString(writer, c.Skin); + // skin + Utils.WriteVString(writer, c.Skin); - // links (uid_list) - writer.Write(c.Links.Count); - foreach (var link in c.Links) - writer.Write(link); - } + // links (uid_list) + writer.Write(c.Links.Count); + foreach (var link in c.Links) + writer.Write(link); + } - // Write triggers section - Logger.Dev(logSrc, $"Writing triggers: count={mesh.Triggers.Count}"); - writer.Write(mesh.Triggers.Count); - foreach (var trg in mesh.Triggers) - { - Logger.Dev(logSrc, $" → trigger UID={trg.UID}, shape={trg.Shape}"); + // Write triggers section + Logger.Dev(logSrc, $"Writing triggers: count={mesh.Triggers.Count}"); + writer.Write(mesh.Triggers.Count); + foreach (var trg in mesh.Triggers) + { + Logger.Dev(logSrc, $" → trigger UID={trg.UID}, shape={trg.Shape}"); - // UID - writer.Write(trg.UID); + // UID + writer.Write(trg.UID); - // script name - Utils.WriteVString(writer, trg.ScriptName); + // script name + Utils.WriteVString(writer, trg.ScriptName); - // hidden flag - writer.Write((byte)(trg.HiddenInEditor ? 1 : 0)); + // hidden flag + writer.Write((byte)(trg.HiddenInEditor ? 1 : 0)); - // shape - writer.Write((int)trg.Shape); + // shape + writer.Write((int)trg.Shape); - // resets_after & resets_times - writer.Write(trg.ResetsAfter); - writer.Write(trg.ResetsTimes); + // resets_after & resets_times + writer.Write(trg.ResetsAfter); + writer.Write(trg.ResetsTimes); - // use key required + key name - writer.Write((byte)(trg.UseKeyRequired ? 1 : 0)); - Utils.WriteVString(writer, trg.KeyName); + // use key required + key name + writer.Write((byte)(trg.UseKeyRequired ? 1 : 0)); + Utils.WriteVString(writer, trg.KeyName); - // weapon activates + activated_by - writer.Write((byte)(trg.WeaponActivates ? 1 : 0)); - writer.Write((byte)trg.ActivatedBy); + // weapon activates + activated_by + writer.Write((byte)(trg.WeaponActivates ? 1 : 0)); + writer.Write((byte)trg.ActivatedBy); - // npc, auto, in_vehicle - writer.Write((byte)(trg.IsNpc ? 1 : 0)); - writer.Write((byte)(trg.IsAuto ? 1 : 0)); - writer.Write((byte)(trg.InVehicle ? 1 : 0)); + // npc, auto, in_vehicle + writer.Write((byte)(trg.IsNpc ? 1 : 0)); + writer.Write((byte)(trg.IsAuto ? 1 : 0)); + writer.Write((byte)(trg.InVehicle ? 1 : 0)); // position (mirror about world origin if active) var trgPos = mirrorActive ? MirrorPosAboutPivot(trg.Position, posAxis, 0f) : trg.Position; @@ -501,11 +501,11 @@ public static void ExportRfg(Mesh mesh, string outputPath) // sphere vs box if (trg.Shape == TriggerShape.Sphere) - { - writer.Write(trg.SphereRadius); - } - else - { + { + writer.Write(trg.SphereRadius); + } + else + { // rotation matrix (mirror in world if active) var R = mirrorActive ? MirrorRotationAboutOrigin(trg.Rotation, Config.GeoMirror) @@ -522,43 +522,43 @@ public static void ExportRfg(Mesh mesh, string outputPath) writer.Write((byte)(trg.OneWay ? 1 : 0)); } - // room/link UIDs + disabled flag - writer.Write(trg.AirlockRoomUID); - writer.Write(trg.AttachedToUID); - writer.Write(trg.UseClutterUID); - writer.Write((byte)(trg.Disabled ? 1 : 0)); - - // timing - writer.Write(trg.ButtonActiveTime); - writer.Write(trg.InsideTime); - - // team - writer.Write((int)trg.Team); - - // links - writer.Write(trg.Links.Count); - foreach (var uid in trg.Links) - writer.Write(uid); - } + // room/link UIDs + disabled flag + writer.Write(trg.AirlockRoomUID); + writer.Write(trg.AttachedToUID); + writer.Write(trg.UseClutterUID); + writer.Write((byte)(trg.Disabled ? 1 : 0)); + + // timing + writer.Write(trg.ButtonActiveTime); + writer.Write(trg.InsideTime); + + // team + writer.Write((int)trg.Team); + + // links + writer.Write(trg.Links.Count); + foreach (var uid in trg.Links) + writer.Write(uid); + } - writer.Write(0); // 0 particle emitters - writer.Write(0); // 0 gas regions + writer.Write(0); // 0 particle emitters + writer.Write(0); // 0 gas regions - //writer.Write(0); // 0 decals + //writer.Write(0); // 0 decals - // Write decals section - Logger.Dev(logSrc, $"Writing decals: count={mesh.Decals.Count}"); - writer.Write(mesh.Decals.Count); - foreach (var dc in mesh.Decals) - { - Logger.Dev(logSrc, $" → decal UID={dc.UID}, texture={dc.Texture}, scale={dc.Scale}"); + // Write decals section + Logger.Dev(logSrc, $"Writing decals: count={mesh.Decals.Count}"); + writer.Write(mesh.Decals.Count); + foreach (var dc in mesh.Decals) + { + Logger.Dev(logSrc, $" → decal UID={dc.UID}, texture={dc.Texture}, scale={dc.Scale}"); - // UID - writer.Write(dc.UID); + // UID + writer.Write(dc.UID); - // class name (“Decal”) - Utils.WriteVString(writer, dc.ClassName); + // class name (“Decal”) + Utils.WriteVString(writer, dc.ClassName); // position var dcPos = mirrorActive ? MirrorPosAboutPivot(dc.Position, posAxis, 0f) : dc.Position; @@ -580,59 +580,59 @@ public static void ExportRfg(Mesh mesh, string outputPath) /* - // position - writer.Write(dc.Position.X); - writer.Write(dc.Position.Y); - writer.Write(dc.Position.Z); - - // 3×3 rotation (forward, right, up) - var R = dc.Rotation; - // forward = third row - writer.Write(R.M31); writer.Write(R.M32); writer.Write(R.M33); - // right = first row - writer.Write(R.M11); writer.Write(R.M12); writer.Write(R.M13); - // up = second row - writer.Write(R.M21); writer.Write(R.M22); writer.Write(R.M23); - */ + // position + writer.Write(dc.Position.X); + writer.Write(dc.Position.Y); + writer.Write(dc.Position.Z); + + // 3×3 rotation (forward, right, up) + var R = dc.Rotation; + // forward = third row + writer.Write(R.M31); writer.Write(R.M32); writer.Write(R.M33); + // right = first row + writer.Write(R.M11); writer.Write(R.M12); writer.Write(R.M13); + // up = second row + writer.Write(R.M21); writer.Write(R.M22); writer.Write(R.M23); + */ // script name Utils.WriteVString(writer, dc.ScriptName); - // hidden flag - writer.Write((byte)(dc.HiddenInEditor ? 1 : 0)); + // hidden flag + writer.Write((byte)(dc.HiddenInEditor ? 1 : 0)); - // extents (vec3) - writer.Write(dc.Extents.X); - writer.Write(dc.Extents.Y); - writer.Write(dc.Extents.Z); + // extents (vec3) + writer.Write(dc.Extents.X); + writer.Write(dc.Extents.Y); + writer.Write(dc.Extents.Z); - // texture - Utils.WriteVString(writer, dc.Texture); + // texture + Utils.WriteVString(writer, dc.Texture); - // alpha (s4) - writer.Write(dc.Alpha); + // alpha (s4) + writer.Write(dc.Alpha); - // self_illuminated (u1) - writer.Write((byte)(dc.SelfIlluminated ? 1 : 0)); + // self_illuminated (u1) + writer.Write((byte)(dc.SelfIlluminated ? 1 : 0)); - // tiling (s4 enum) - writer.Write((int)dc.Tiling); + // tiling (s4 enum) + writer.Write((int)dc.Tiling); - // scale (f4) - writer.Write(dc.Scale); - } + // scale (f4) + writer.Write(dc.Scale); + } - // Write climbing regions section - Logger.Dev(logSrc, $"Writing climbing regions: count={mesh.ClimbingRegions.Count}"); - writer.Write(mesh.ClimbingRegions.Count); - foreach (var cr in mesh.ClimbingRegions) - { - Logger.Dev(logSrc, $" → climb UID={cr.UID}, type={cr.Type}, extents={cr.Extents}"); + // Write climbing regions section + Logger.Dev(logSrc, $"Writing climbing regions: count={mesh.ClimbingRegions.Count}"); + writer.Write(mesh.ClimbingRegions.Count); + foreach (var cr in mesh.ClimbingRegions) + { + Logger.Dev(logSrc, $" → climb UID={cr.UID}, type={cr.Type}, extents={cr.Extents}"); - // UID - writer.Write(cr.UID); + // UID + writer.Write(cr.UID); - // class name - Utils.WriteVString(writer, cr.ClassName); + // class name + Utils.WriteVString(writer, cr.ClassName); // position var crPos = mirrorActive ? MirrorPosAboutPivot(cr.Position, posAxis, 0f) : cr.Position; @@ -656,35 +656,35 @@ public static void ExportRfg(Mesh mesh, string outputPath) // script name Utils.WriteVString(writer, cr.ScriptName); - // hidden flag - writer.Write((byte)(cr.HiddenInEditor ? 1 : 0)); + // hidden flag + writer.Write((byte)(cr.HiddenInEditor ? 1 : 0)); - // region type - writer.Write(cr.Type); + // region type + writer.Write(cr.Type); - // extents - writer.Write(cr.Extents.X); - writer.Write(cr.Extents.Y); - writer.Write(cr.Extents.Z); - } + // extents + writer.Write(cr.Extents.X); + writer.Write(cr.Extents.Y); + writer.Write(cr.Extents.Z); + } - writer.Write(0); // 0 room effects - writer.Write(0); // 0 eax effects - writer.Write(0); // 0 bolt emitters - writer.Write(0); // 0 targets + writer.Write(0); // 0 room effects + writer.Write(0); // 0 eax effects + writer.Write(0); // 0 bolt emitters + writer.Write(0); // 0 targets - // Write push regions section - Logger.Dev(logSrc, $"Writing push regions: count={mesh.PushRegions.Count}"); - writer.Write(mesh.PushRegions.Count); - foreach (var pr in mesh.PushRegions) - { - Logger.Dev(logSrc, $" → push UID={pr.UID}, strength={pr.Strength}"); + // Write push regions section + Logger.Dev(logSrc, $"Writing push regions: count={mesh.PushRegions.Count}"); + writer.Write(mesh.PushRegions.Count); + foreach (var pr in mesh.PushRegions) + { + Logger.Dev(logSrc, $" → push UID={pr.UID}, strength={pr.Strength}"); - // UID - writer.Write(pr.UID); + // UID + writer.Write(pr.UID); - // class name - Utils.WriteVString(writer, pr.ClassName); + // class name + Utils.WriteVString(writer, pr.ClassName); // position var prPos = mirrorActive ? MirrorPosAboutPivot(pr.Position, posAxis, 0f) : pr.Position; @@ -694,150 +694,150 @@ public static void ExportRfg(Mesh mesh, string outputPath) // 3×3 rotation matrix var R = mirrorActive ? MirrorRotationAboutOrigin(pr.Rotation, Config.GeoMirror) : pr.Rotation; writer.Write(R.M11); writer.Write(R.M12); writer.Write(R.M13); - writer.Write(R.M21); writer.Write(R.M22); writer.Write(R.M23); - writer.Write(R.M31); writer.Write(R.M32); writer.Write(R.M33); - - // script name - Utils.WriteVString(writer, pr.ScriptName); - - // hidden flag - writer.Write((byte)(pr.HiddenInEditor ? 1 : 0)); - - // shape enum - writer.Write((int)pr.Shape); - - // extents or radius - if (pr.Shape == PushRegionShape.Sphere) - { - writer.Write(pr.Radius); - } - else - { - writer.Write(pr.Extents.X); - writer.Write(pr.Extents.Y); - writer.Write(pr.Extents.Z); - } - - // strength - writer.Write(pr.Strength); - - // flags (16‑bit) - ushort rawFlags = 0; - if (pr.JumpPad) rawFlags |= 0x40; - if (pr.DoesntAffectPlayer) rawFlags |= 0x20; - if (pr.Radial) rawFlags |= 0x10; - if (pr.GrowsTowardsBoundary) rawFlags |= 0x08; - if (pr.GrowsTowardsCenter) rawFlags |= 0x04; - if (pr.Grounded) rawFlags |= 0x02; - if (pr.MassIndependent) rawFlags |= 0x01; - writer.Write(rawFlags); - - // turbulence - writer.Write(pr.Turbulence); - } - - - //for (int i = 0; i < 22; i++) - // writer.Write(0); - - // --- MOVING GROUPS --- - foreach (var grp in mesh.Groups) - { - // a) group_name - Utils.WriteVString(writer, grp.Name); - - // b) is_moving - writer.Write((byte)1); - - // c) moving_data - var md = grp.MovingData; - // keyframes - writer.Write(md.Keyframes.Count); - foreach (var kf in md.Keyframes) - { - writer.Write(kf.UID); - writer.Write(kf.Pos.X); writer.Write(kf.Pos.Y); writer.Write(kf.Pos.Z); - // 3×3 rotation - writer.Write(kf.Rot.M11); writer.Write(kf.Rot.M12); writer.Write(kf.Rot.M13); - writer.Write(kf.Rot.M21); writer.Write(kf.Rot.M22); writer.Write(kf.Rot.M23); - writer.Write(kf.Rot.M31); writer.Write(kf.Rot.M32); writer.Write(kf.Rot.M33); - Utils.WriteVString(writer, kf.ScriptName); - writer.Write((byte)(kf.HiddenInEditor ? 1 : 0)); - writer.Write(kf.PauseTime); - writer.Write(kf.DepartTravelTime); - writer.Write(kf.ReturnTravelTime); - writer.Write(kf.AccelTime); - writer.Write(kf.DecelTime); - writer.Write(kf.EventUID); - writer.Write(kf.ItemUID1); - writer.Write(kf.ItemUID2); - writer.Write(kf.DegreesAboutAxis); - } - // member transforms - writer.Write(md.MemberTransforms.Count); - foreach (var mt in md.MemberTransforms) - { - writer.Write(mt.UID); - writer.Write(mt.Pos.X); writer.Write(mt.Pos.Y); writer.Write(mt.Pos.Z); - writer.Write(mt.Rot.M11); writer.Write(mt.Rot.M12); writer.Write(mt.Rot.M13); - writer.Write(mt.Rot.M21); writer.Write(mt.Rot.M22); writer.Write(mt.Rot.M23); - writer.Write(mt.Rot.M31); writer.Write(mt.Rot.M32); writer.Write(mt.Rot.M33); - } - // flags & other scalar fields - writer.Write((byte)(md.IsDoor ? 1 : 0)); - writer.Write((byte)(md.RotateInPlace ? 1 : 0)); - writer.Write((byte)(md.StartsBackwards ? 1 : 0)); - writer.Write((byte)(md.UseTravelTimeAsSpeed ? 1 : 0)); - writer.Write((byte)(md.ForceOrient ? 1 : 0)); - writer.Write((byte)(md.NoPlayerCollide ? 1 : 0)); - writer.Write(md.MovementType); - writer.Write(md.StartingKeyframe); - Utils.WriteVString(writer, md.StartSound); - writer.Write(md.StartVol); - Utils.WriteVString(writer, md.LoopingSound); - writer.Write(md.LoopingVol); - Utils.WriteVString(writer, md.StopSound); - writer.Write(md.StopVol); - Utils.WriteVString(writer, md.CloseSound); - writer.Write(md.CloseVol); - - // d) brushes (only those in this group) - var groupBrushes = mesh.Brushes - .FindAll(b => grp.Brushes.Contains(b.UID)); - WriteBrushesSection(writer, groupBrushes); - - // e–end) all other sections empty - WriteEmptySection(writer); // geo_regions - WriteEmptySection(writer); // lights - WriteEmptySection(writer); // cutscene_cameras - WriteEmptySection(writer); // cutscene_path_nodes - WriteEmptySection(writer); // ambient_sounds - WriteEmptySection(writer); // events - WriteEmptySection(writer); // mp_respawn_points - writer.Write(0); // num_nav_points - WriteEmptySection(writer); // entities - WriteEmptySection(writer); // items - WriteEmptySection(writer); // clutters - WriteEmptySection(writer); // triggers - WriteEmptySection(writer); // particle_emitters - WriteEmptySection(writer); // gas_regions - WriteEmptySection(writer); // decals - WriteEmptySection(writer); // climbing_regions - WriteEmptySection(writer); // room_effects - WriteEmptySection(writer); // eax_effects - WriteEmptySection(writer); // bolt_emitters - WriteEmptySection(writer); // targets - WriteEmptySection(writer); // push_regions - } - - Logger.Info(logSrc, "RFG export complete."); - - } - - private static void WriteEmptySection(BinaryWriter writer) - { - writer.Write(0); - } + writer.Write(R.M21); writer.Write(R.M22); writer.Write(R.M23); + writer.Write(R.M31); writer.Write(R.M32); writer.Write(R.M33); + + // script name + Utils.WriteVString(writer, pr.ScriptName); + + // hidden flag + writer.Write((byte)(pr.HiddenInEditor ? 1 : 0)); + + // shape enum + writer.Write((int)pr.Shape); + + // extents or radius + if (pr.Shape == PushRegionShape.Sphere) + { + writer.Write(pr.Radius); + } + else + { + writer.Write(pr.Extents.X); + writer.Write(pr.Extents.Y); + writer.Write(pr.Extents.Z); + } + + // strength + writer.Write(pr.Strength); + + // flags (16‑bit) + ushort rawFlags = 0; + if (pr.JumpPad) rawFlags |= 0x40; + if (pr.DoesntAffectPlayer) rawFlags |= 0x20; + if (pr.Radial) rawFlags |= 0x10; + if (pr.GrowsTowardsBoundary) rawFlags |= 0x08; + if (pr.GrowsTowardsCenter) rawFlags |= 0x04; + if (pr.Grounded) rawFlags |= 0x02; + if (pr.MassIndependent) rawFlags |= 0x01; + writer.Write(rawFlags); + + // turbulence + writer.Write(pr.Turbulence); + } + + + //for (int i = 0; i < 22; i++) + // writer.Write(0); + + // --- MOVING GROUPS --- + foreach (var grp in mesh.Groups) + { + // a) group_name + Utils.WriteVString(writer, grp.Name); + + // b) is_moving + writer.Write((byte)1); + + // c) moving_data + var md = grp.MovingData; + // keyframes + writer.Write(md.Keyframes.Count); + foreach (var kf in md.Keyframes) + { + writer.Write(kf.UID); + writer.Write(kf.Pos.X); writer.Write(kf.Pos.Y); writer.Write(kf.Pos.Z); + // 3×3 rotation + writer.Write(kf.Rot.M11); writer.Write(kf.Rot.M12); writer.Write(kf.Rot.M13); + writer.Write(kf.Rot.M21); writer.Write(kf.Rot.M22); writer.Write(kf.Rot.M23); + writer.Write(kf.Rot.M31); writer.Write(kf.Rot.M32); writer.Write(kf.Rot.M33); + Utils.WriteVString(writer, kf.ScriptName); + writer.Write((byte)(kf.HiddenInEditor ? 1 : 0)); + writer.Write(kf.PauseTime); + writer.Write(kf.DepartTravelTime); + writer.Write(kf.ReturnTravelTime); + writer.Write(kf.AccelTime); + writer.Write(kf.DecelTime); + writer.Write(kf.EventUID); + writer.Write(kf.ItemUID1); + writer.Write(kf.ItemUID2); + writer.Write(kf.DegreesAboutAxis); + } + // member transforms + writer.Write(md.MemberTransforms.Count); + foreach (var mt in md.MemberTransforms) + { + writer.Write(mt.UID); + writer.Write(mt.Pos.X); writer.Write(mt.Pos.Y); writer.Write(mt.Pos.Z); + writer.Write(mt.Rot.M11); writer.Write(mt.Rot.M12); writer.Write(mt.Rot.M13); + writer.Write(mt.Rot.M21); writer.Write(mt.Rot.M22); writer.Write(mt.Rot.M23); + writer.Write(mt.Rot.M31); writer.Write(mt.Rot.M32); writer.Write(mt.Rot.M33); + } + // flags & other scalar fields + writer.Write((byte)(md.IsDoor ? 1 : 0)); + writer.Write((byte)(md.RotateInPlace ? 1 : 0)); + writer.Write((byte)(md.StartsBackwards ? 1 : 0)); + writer.Write((byte)(md.UseTravelTimeAsSpeed ? 1 : 0)); + writer.Write((byte)(md.ForceOrient ? 1 : 0)); + writer.Write((byte)(md.NoPlayerCollide ? 1 : 0)); + writer.Write(md.MovementType); + writer.Write(md.StartingKeyframe); + Utils.WriteVString(writer, md.StartSound); + writer.Write(md.StartVol); + Utils.WriteVString(writer, md.LoopingSound); + writer.Write(md.LoopingVol); + Utils.WriteVString(writer, md.StopSound); + writer.Write(md.StopVol); + Utils.WriteVString(writer, md.CloseSound); + writer.Write(md.CloseVol); + + // d) brushes (only those in this group) + var groupBrushes = mesh.Brushes + .FindAll(b => grp.Brushes.Contains(b.UID)); + WriteBrushesSection(writer, groupBrushes); + + // e–end) all other sections empty + WriteEmptySection(writer); // geo_regions + WriteEmptySection(writer); // lights + WriteEmptySection(writer); // cutscene_cameras + WriteEmptySection(writer); // cutscene_path_nodes + WriteEmptySection(writer); // ambient_sounds + WriteEmptySection(writer); // events + WriteEmptySection(writer); // mp_respawn_points + writer.Write(0); // num_nav_points + WriteEmptySection(writer); // entities + WriteEmptySection(writer); // items + WriteEmptySection(writer); // clutters + WriteEmptySection(writer); // triggers + WriteEmptySection(writer); // particle_emitters + WriteEmptySection(writer); // gas_regions + WriteEmptySection(writer); // decals + WriteEmptySection(writer); // climbing_regions + WriteEmptySection(writer); // room_effects + WriteEmptySection(writer); // eax_effects + WriteEmptySection(writer); // bolt_emitters + WriteEmptySection(writer); // targets + WriteEmptySection(writer); // push_regions + } + + Logger.Info(logSrc, "RFG export complete."); + + } + + private static void WriteEmptySection(BinaryWriter writer) + { + writer.Write(0); + } /// /// Writes a brushes section to the RFG, using exactly the supplied brushes list. @@ -1110,102 +1110,102 @@ static void Mat3Mul(in Vector3 A_r1, in Vector3 A_r2, in Vector3 A_r3, } public static void HandleCoronaClutterReplacements(Mesh mesh) - { - // 1) collect Corona UIDs - var coronaClutterUIDs = new List(); - if (!string.IsNullOrEmpty(Config.CoronaClutterName)) - { - // 2) for each corona, add a Clutter - foreach (var c in mesh.Coronas) - { - coronaClutterUIDs.Add(c.UID); - - mesh.Clutters.Add(new Clutter - { - UID = c.UID, - ClassName = Config.CoronaClutterName, - ScriptName = c.ScriptName, - Position = c.Position, - Rotation = Matrix4x4.Identity, - HiddenInEditor = false, - Skin = "", - Links = new List() - }); - } - - // 4) create the Unhide event (links → all new clutters) - int unhideEventUID = RflUtils.FindNextValidUID(mesh); - mesh.Events.Add(new RflEvent - { - UID = unhideEventUID, - ClassName = "Unhide", - ScriptName = "Unhide", - Position = Vector3.Zero + new Vector3(0, 3, 0), - HiddenInEditor = false, - Delay = 0f, - Bool1 = false, - Bool2 = false, - Int1 = 0, - Int2 = 0, - Float1 = 0f, - Float2 = 0f, - Str1 = "", - Str2 = "", - Links = new List(coronaClutterUIDs), - RawColor = 0xFF00FFFF - }); - - // 5) create the Route_Node event (links → Unhide) - int routeNodeEventUID = RflUtils.FindNextValidUID(mesh); - mesh.Events.Add(new RflEvent - { - UID = routeNodeEventUID, - ClassName = "Route_Node", - ScriptName = "Route_Node", - Position = Vector3.Zero + new Vector3(0, 2, 0), - HiddenInEditor = false, - Delay = 0f, - Bool1 = false, - Bool2 = false, - Int1 = 2, // invert - Int2 = 0, - Float1 = 0f, - Float2 = 0f, - Str1 = "", - Str2 = "", - Links = new List { unhideEventUID }, - RawColor = 0xFF00FFFF - }); - - // 7) create the auto‐trigger (links → Route_Node) - int TriggerUID = RflUtils.FindNextValidUID(mesh); - mesh.Triggers.Add(new Trigger - { - UID = TriggerUID, - ScriptName = "Redux Trigger Auto", - HiddenInEditor = false, - Shape = TriggerShape.Sphere, - ResetsAfter = 0f, - ResetsTimes = -1, - UseKeyRequired = false, - KeyName = "", - WeaponActivates = false, - ActivatedBy = TriggerActivatedBy.AllObjects, - IsNpc = false, - IsAuto = true, - InVehicle = false, - Position = Vector3.Zero + new Vector3(0, 1, 0), - SphereRadius = 1f, - AirlockRoomUID = -1, - AttachedToUID = -1, - UseClutterUID = -1, - Disabled = false, - ButtonActiveTime = 0f, - InsideTime = 0f, - Team = TriggerTeam.None, - Links = new List { routeNodeEventUID } - }); - } - } - } + { + // 1) collect Corona UIDs + var coronaClutterUIDs = new List(); + if (!string.IsNullOrEmpty(Config.CoronaClutterName)) + { + // 2) for each corona, add a Clutter + foreach (var c in mesh.Coronas) + { + coronaClutterUIDs.Add(c.UID); + + mesh.Clutters.Add(new Clutter + { + UID = c.UID, + ClassName = Config.CoronaClutterName, + ScriptName = c.ScriptName, + Position = c.Position, + Rotation = Matrix4x4.Identity, + HiddenInEditor = false, + Skin = "", + Links = new List() + }); + } + + // 4) create the Unhide event (links → all new clutters) + int unhideEventUID = RflUtils.FindNextValidUID(mesh); + mesh.Events.Add(new RflEvent + { + UID = unhideEventUID, + ClassName = "Unhide", + ScriptName = "Unhide", + Position = Vector3.Zero + new Vector3(0, 3, 0), + HiddenInEditor = false, + Delay = 0f, + Bool1 = false, + Bool2 = false, + Int1 = 0, + Int2 = 0, + Float1 = 0f, + Float2 = 0f, + Str1 = "", + Str2 = "", + Links = new List(coronaClutterUIDs), + RawColor = 0xFF00FFFF + }); + + // 5) create the Route_Node event (links → Unhide) + int routeNodeEventUID = RflUtils.FindNextValidUID(mesh); + mesh.Events.Add(new RflEvent + { + UID = routeNodeEventUID, + ClassName = "Route_Node", + ScriptName = "Route_Node", + Position = Vector3.Zero + new Vector3(0, 2, 0), + HiddenInEditor = false, + Delay = 0f, + Bool1 = false, + Bool2 = false, + Int1 = 2, // invert + Int2 = 0, + Float1 = 0f, + Float2 = 0f, + Str1 = "", + Str2 = "", + Links = new List { unhideEventUID }, + RawColor = 0xFF00FFFF + }); + + // 7) create the auto‐trigger (links → Route_Node) + int TriggerUID = RflUtils.FindNextValidUID(mesh); + mesh.Triggers.Add(new Trigger + { + UID = TriggerUID, + ScriptName = "Redux Trigger Auto", + HiddenInEditor = false, + Shape = TriggerShape.Sphere, + ResetsAfter = 0f, + ResetsTimes = -1, + UseKeyRequired = false, + KeyName = "", + WeaponActivates = false, + ActivatedBy = TriggerActivatedBy.AllObjects, + IsNpc = false, + IsAuto = true, + InVehicle = false, + Position = Vector3.Zero + new Vector3(0, 1, 0), + SphereRadius = 1f, + AirlockRoomUID = -1, + AttachedToUID = -1, + UseClutterUID = -1, + Disabled = false, + ButtonActiveTime = 0f, + InsideTime = 0f, + Team = TriggerTeam.None, + Links = new List { routeNodeEventUID } + }); + } + } + } } diff --git a/exporters/TgaExporter.cs b/exporters/TgaExporter.cs index 5704a73..cf35963 100644 --- a/exporters/TgaExporter.cs +++ b/exporters/TgaExporter.cs @@ -7,57 +7,57 @@ namespace redux.exporters { - public static class TgaExporter - { - private const string logSrc = "TgaExporter"; - public static void Write24BitTga(string outputPath, Lightmap lm) - { - Logger.Dev(logSrc, $"Starting TGA export: \"{outputPath}\" (Dimensions: {lm.Width}×{lm.Height})"); + public static class TgaExporter + { + private const string logSrc = "TgaExporter"; + public static void Write24BitTga(string outputPath, Lightmap lm) + { + Logger.Dev(logSrc, $"Starting TGA export: \"{outputPath}\" (Dimensions: {lm.Width}×{lm.Height})"); - using var fs = File.OpenWrite(outputPath); - using var bw = new BinaryWriter(fs); + using var fs = File.OpenWrite(outputPath); + using var bw = new BinaryWriter(fs); - // 1) Header: 18 bytes - Logger.Dev(logSrc, "Writing TGA header..."); - bw.Write((byte)0); // 0: no ID field - bw.Write((byte)0); // 1: no color map - bw.Write((byte)2); // 2: uncompressed true‐color image - bw.Write((short)0); // 3–4: color map origin - bw.Write((short)0); // 5–6: color map length - bw.Write((byte)0); // 7: color map depth - bw.Write((short)0); // 8–9: x‐origin (low, high) - bw.Write((short)0); // 10–11: y‐origin - bw.Write((short)lm.Width); // 12–13: width - bw.Write((short)lm.Height); // 14–15: height - bw.Write((byte)24); // 16: pixel depth (24 bits) - bw.Write((byte)0); // 17: image descriptor (no alpha, origin bottom‐left) - Logger.Dev(logSrc, "Header written."); + // 1) Header: 18 bytes + Logger.Dev(logSrc, "Writing TGA header..."); + bw.Write((byte)0); // 0: no ID field + bw.Write((byte)0); // 1: no color map + bw.Write((byte)2); // 2: uncompressed true‐color image + bw.Write((short)0); // 3–4: color map origin + bw.Write((short)0); // 5–6: color map length + bw.Write((byte)0); // 7: color map depth + bw.Write((short)0); // 8–9: x‐origin (low, high) + bw.Write((short)0); // 10–11: y‐origin + bw.Write((short)lm.Width); // 12–13: width + bw.Write((short)lm.Height); // 14–15: height + bw.Write((byte)24); // 16: pixel depth (24 bits) + bw.Write((byte)0); // 17: image descriptor (no alpha, origin bottom‐left) + Logger.Dev(logSrc, "Header written."); - // 2) Pixel data: bottom row first, BGR order - int w = lm.Width, h = lm.Height; - byte[] data = lm.PixelData; // assumed RGB row-major from top‐left - Logger.Dev(logSrc, $"Writing pixel data ({w * h * 3} bytes total)…"); + // 2) Pixel data: bottom row first, BGR order + int w = lm.Width, h = lm.Height; + byte[] data = lm.PixelData; // assumed RGB row-major from top‐left + Logger.Dev(logSrc, $"Writing pixel data ({w * h * 3} bytes total)…"); - for (int row = h - 1; row >= 0; row--) - { - int rowStart = row * (w * 3); - for (int col = 0; col < w; col++) - { - int pixelIndex = rowStart + col * 3; - byte r = data[pixelIndex + 0]; - byte g = data[pixelIndex + 1]; - byte b = data[pixelIndex + 2]; - // Write in BGR order - bw.Write(b); - bw.Write(g); - bw.Write(r); - } - } - Logger.Dev(logSrc, "Pixel data written."); + for (int row = h - 1; row >= 0; row--) + { + int rowStart = row * (w * 3); + for (int col = 0; col < w; col++) + { + int pixelIndex = rowStart + col * 3; + byte r = data[pixelIndex + 0]; + byte g = data[pixelIndex + 1]; + byte b = data[pixelIndex + 2]; + // Write in BGR order + bw.Write(b); + bw.Write(g); + bw.Write(r); + } + } + Logger.Dev(logSrc, "Pixel data written."); - // 3) No footer/trailer needed for a simple TGA - bw.Flush(); - Logger.Dev(logSrc, $"Finished TGA export: \"{outputPath}\""); - } - } + // 3) No footer/trailer needed for a simple TGA + bw.Flush(); + Logger.Dev(logSrc, $"Finished TGA export: \"{outputPath}\""); + } + } } diff --git a/exporters/V3MExporter.cs b/exporters/V3MExporter.cs index 125fa8e..171dc75 100644 --- a/exporters/V3MExporter.cs +++ b/exporters/V3MExporter.cs @@ -7,276 +7,276 @@ namespace redux.exporters { - public static class V3mExporter - { - private const string logSrc = "V3mExporter"; - private const int V3M_SIGNATURE = 0x52463344; // 'RF3D' - private const int V3D_VERSION = 0x40000; - private const int V3D_SECTION_SUBMESH = 0x5355424D; // 'SUBM' - private const int V3D_SECTION_END = 0x00000000; - private const int V3D_LOD_TRIANGLE_PLANES = 0x20; - - public static void ExportV3m(Mesh mesh, string outputPath) - { - Logger.Dev(logSrc, $"ExportV3m: '{outputPath}', submesh count={mesh.Brushes.Count}"); - using var writer = new BinaryWriter(File.Create(outputPath)); - - // File header - writer.Write(V3M_SIGNATURE); - writer.Write(V3D_VERSION); - writer.Write(mesh.Brushes.Count); - writer.Write(0); writer.Write(0); writer.Write(0); // reset fields - int totalMaterials = mesh.Brushes.Sum(b => b.Solid.Textures.Count); - writer.Write(totalMaterials); - writer.Write(0); writer.Write(0); writer.Write(0); - - // Submeshes - foreach (var brush in mesh.Brushes) - WriteSubmesh(brush, writer); - - // End section - writer.Write(V3D_SECTION_END); - writer.Write(0); - Logger.Dev(logSrc, "ExportV3m complete"); - } - - private static void WriteSubmesh(Brush brush, BinaryWriter writer) - { - Logger.Dev(logSrc, $"-- Submesh begin Brush {brush.UID}"); - // Section header - writer.Write(V3D_SECTION_SUBMESH); - writer.Write(0); - - // Submesh header - WriteFixedString(writer, $"Brush_{brush.UID}", 24); - WriteFixedString(writer, "", 24); - writer.Write(7); writer.Write(1); writer.Write(0f); - - // Geometry per material - var perMat = GatherGeometry(brush); - - // Bounding - var allPts = perMat.Values.SelectMany(c => c.Positions).ToList(); - var aabbMin = new Vector3(float.MaxValue); - var aabbMax = new Vector3(float.MinValue); - foreach (var p in allPts) - { - aabbMin = Vector3.Min(aabbMin, p); - aabbMax = Vector3.Max(aabbMax, p); - } - float radius = allPts.Max(p => p.Length()); - WriteVec3(writer, Vector3.Zero); - writer.Write(radius); - WriteVec3(writer, aabbMin); - WriteVec3(writer, aabbMax); - - // LOD mesh header - writer.Write((uint)V3D_LOD_TRIANGLE_PLANES); - int totalVerts = allPts.Count; - int numBatches = perMat.Count; - writer.Write(totalVerts); - writer.Write((ushort)numBatches); - - // Build raw data block - using var ms = new MemoryStream(); - using var dw = new BinaryWriter(ms); - void Align(int a) { long pad = (a - dw.BaseStream.Position % a) % a; if (pad > 0) dw.Write(new byte[pad]); } - - // Batch headers - int texIdx = 0; - foreach (var _ in perMat) - { - dw.Write(new byte[0x20]); dw.Write(texIdx++); dw.Write(new byte[0x14]); - } - Align(0x10); - - // Batch data - foreach (var chunk in perMat.Values) - { - // positions - foreach (var v in chunk.Positions) { - dw.Write(v.X); dw.Write(v.Y); dw.Write(v.Z); - } - Align(0x10); - - // normals - foreach (var n in chunk.Normals) { - dw.Write(n.X); dw.Write(n.Y); dw.Write(n.Z); - } - Align(0x10); - - // uvs (flip Y) - Logger.Dev(logSrc, $"Writing UVs: count={chunk.UVs.Count}"); - for (int i = 0; i < chunk.UVs.Count; i++) - { - var uv = chunk.UVs[i]; - float u = uv.X; - float v = 1f - uv.Y; - dw.Write(u); - dw.Write(v); - if (i < 3) - Logger.Dev(logSrc, $"UV[{i}] = ({u:0.###}, {v:0.###})"); - } - Align(0x10); - - - // triangles - foreach (var (i0, i1, i2, flags) in chunk.Triangles) - { - dw.Write((ushort)i0); dw.Write((ushort)i1); dw.Write((ushort)i2); dw.Write((ushort)flags); - } - Align(0x10); - - // planes - foreach (var (n, d) in chunk.Planes) - { - dw.Write(n.X); dw.Write(n.Y); dw.Write(n.Z); dw.Write(d); - } - Align(0x10); - - // same_pos offsets (zeros) - dw.Write(new byte[chunk.Positions.Count * sizeof(short)]); - Align(0x10); - } - - // Write data block - writer.Write((int)ms.Length); - writer.Write(ms.ToArray()); - writer.Write(-1); // unknown1 - - // batch_info entries - foreach (var chunk in perMat.Values) - { - int nv = chunk.Positions.Count; - int nf = chunk.Triangles.Count; - writer.Write((ushort)nv); - writer.Write((ushort)nf); - writer.Write((ushort)(nv * 12)); // positions_size - writer.Write((ushort)(nf * 8)); // indices_size - writer.Write((ushort)(nv * 2)); // same_pos_vertex_offsets_size - writer.Write((ushort)0); // bone_links_size - writer.Write((ushort)(nv * 8)); // tex_coords_size - writer.Write((uint)5344321); // render_flags - } - - // prop points - writer.Write(0); - - // LOD textures - writer.Write((uint)perMat.Count); - byte ti = 0; - foreach (var mat in perMat.Keys) - { - writer.Write(ti++); - WriteZeroTerminatedString(writer, mat + ".tga"); - } - - // Submesh materials - writer.Write(brush.Solid.Textures.Count); - foreach (var tex in brush.Solid.Textures) - { - WriteFixedString(writer, Path.GetFileName(tex), 32); - writer.Write(0f); // emissive - writer.Write(0f); writer.Write(0f); writer.Write(0f); // unused - WriteFixedString(writer, "", 32); - writer.Write((uint)1); // flags: bit0 - } - - // unknown1 - writer.Write(1); - WriteFixedString(writer, $"Brush_{brush.UID}", 24); - writer.Write(0f); - } - - private static Dictionary GatherGeometry(Brush brush) - { - var perMat = new Dictionary(); - foreach (var face in brush.Solid.Faces) - { - var idx = face.Vertices; - int tris = idx.Count > 3 ? idx.Count - 2 : 1; - for (int i = 0; i < tris; i++) - { - // World positions - Vector3 p0 = Transform(brush, idx[0]); - Vector3 p1 = Transform(brush, idx[i + 1]); - Vector3 p2 = Transform(brush, idx[i + 2]); - - // Texture UV from global brush.UVs - Vector2 uv0 = idx[0] < brush.UVs.Count ? brush.UVs[idx[0]] : Vector2.Zero; - Vector2 uv1 = idx[i + 1] < brush.UVs.Count ? brush.UVs[idx[i + 1]] : Vector2.Zero; - Vector2 uv2 = idx[i + 2] < brush.UVs.Count ? brush.UVs[idx[i + 2]] : Vector2.Zero; - - // Compute normal & plane - var n = Vector3.Normalize(Vector3.Cross(p1 - p0, p2 - p0)); - float d = -Vector3.Dot(n, p0); - - // Material name - string mat = Path.GetFileNameWithoutExtension( - face.TextureIndex >= 0 && face.TextureIndex < brush.Solid.Textures.Count - ? brush.Solid.Textures[face.TextureIndex] - : string.Empty - ); - - if (!perMat.TryGetValue(mat, out var chunk)) - perMat[mat] = chunk = new Chunk(); - - // Add or reuse vertex - int v0 = chunk.AddVertex(p0, n, uv0); - int v1 = chunk.AddVertex(p1, n, uv1); - int v2 = chunk.AddVertex(p2, n, uv2); - - // Record triangle and plane - chunk.Triangles.Add((v0, v1, v2, 0)); - chunk.Planes.Add((n, d)); - } - } - Logger.Dev(logSrc, $"Gathered geometry: {perMat.Count} materials, total vertices = {perMat.Values.Sum(c => c.Positions.Count)}"); - return perMat; - } - - private static Vector3 Transform(Brush b, int vi) - => Vector3.Transform(b.Vertices[vi], b.RotationMatrix) + b.Position; - - private static void WriteFixedString(BinaryWriter w, string s, int len) - { - var bs = System.Text.Encoding.ASCII.GetBytes(s); - int count = Math.Min(bs.Length, len - 1); - w.Write(bs, 0, count); - for (int i = count; i < len; i++) w.Write((byte)0); - } - - private static void WriteZeroTerminatedString(BinaryWriter w, string s) - { - var bs = System.Text.Encoding.ASCII.GetBytes(s); - w.Write(bs); - w.Write((byte)0); - } - - private static void WriteVec3(BinaryWriter w, Vector3 v) - { - w.Write(v.X); w.Write(v.Y); w.Write(v.Z); - } - - private class Chunk - { - public readonly List Positions = new(); - public readonly List Normals = new(); - public readonly List UVs = new(); - public readonly List<(int, int, int, uint)> Triangles = new(); - public readonly List<(Vector3, float)> Planes = new(); - private readonly Dictionary<(Vector3, Vector3, Vector2), int> map = new(); - public int AddVertex(Vector3 p, Vector3 n, Vector2 uv) - { - var key = (p, n, uv); - if (map.TryGetValue(key, out var idx)) return idx; - idx = Positions.Count; - Positions.Add(p); - Normals.Add(n); - UVs.Add(uv); - map[key] = idx; - return idx; - } - } - } + public static class V3mExporter + { + private const string logSrc = "V3mExporter"; + private const int V3M_SIGNATURE = 0x52463344; // 'RF3D' + private const int V3D_VERSION = 0x40000; + private const int V3D_SECTION_SUBMESH = 0x5355424D; // 'SUBM' + private const int V3D_SECTION_END = 0x00000000; + private const int V3D_LOD_TRIANGLE_PLANES = 0x20; + + public static void ExportV3m(Mesh mesh, string outputPath) + { + Logger.Dev(logSrc, $"ExportV3m: '{outputPath}', submesh count={mesh.Brushes.Count}"); + using var writer = new BinaryWriter(File.Create(outputPath)); + + // File header + writer.Write(V3M_SIGNATURE); + writer.Write(V3D_VERSION); + writer.Write(mesh.Brushes.Count); + writer.Write(0); writer.Write(0); writer.Write(0); // reset fields + int totalMaterials = mesh.Brushes.Sum(b => b.Solid.Textures.Count); + writer.Write(totalMaterials); + writer.Write(0); writer.Write(0); writer.Write(0); + + // Submeshes + foreach (var brush in mesh.Brushes) + WriteSubmesh(brush, writer); + + // End section + writer.Write(V3D_SECTION_END); + writer.Write(0); + Logger.Dev(logSrc, "ExportV3m complete"); + } + + private static void WriteSubmesh(Brush brush, BinaryWriter writer) + { + Logger.Dev(logSrc, $"-- Submesh begin Brush {brush.UID}"); + // Section header + writer.Write(V3D_SECTION_SUBMESH); + writer.Write(0); + + // Submesh header + WriteFixedString(writer, $"Brush_{brush.UID}", 24); + WriteFixedString(writer, "", 24); + writer.Write(7); writer.Write(1); writer.Write(0f); + + // Geometry per material + var perMat = GatherGeometry(brush); + + // Bounding + var allPts = perMat.Values.SelectMany(c => c.Positions).ToList(); + var aabbMin = new Vector3(float.MaxValue); + var aabbMax = new Vector3(float.MinValue); + foreach (var p in allPts) + { + aabbMin = Vector3.Min(aabbMin, p); + aabbMax = Vector3.Max(aabbMax, p); + } + float radius = allPts.Max(p => p.Length()); + WriteVec3(writer, Vector3.Zero); + writer.Write(radius); + WriteVec3(writer, aabbMin); + WriteVec3(writer, aabbMax); + + // LOD mesh header + writer.Write((uint)V3D_LOD_TRIANGLE_PLANES); + int totalVerts = allPts.Count; + int numBatches = perMat.Count; + writer.Write(totalVerts); + writer.Write((ushort)numBatches); + + // Build raw data block + using var ms = new MemoryStream(); + using var dw = new BinaryWriter(ms); + void Align(int a) { long pad = (a - dw.BaseStream.Position % a) % a; if (pad > 0) dw.Write(new byte[pad]); } + + // Batch headers + int texIdx = 0; + foreach (var _ in perMat) + { + dw.Write(new byte[0x20]); dw.Write(texIdx++); dw.Write(new byte[0x14]); + } + Align(0x10); + + // Batch data + foreach (var chunk in perMat.Values) + { + // positions + foreach (var v in chunk.Positions) { + dw.Write(v.X); dw.Write(v.Y); dw.Write(v.Z); + } + Align(0x10); + + // normals + foreach (var n in chunk.Normals) { + dw.Write(n.X); dw.Write(n.Y); dw.Write(n.Z); + } + Align(0x10); + + // uvs (flip Y) + Logger.Dev(logSrc, $"Writing UVs: count={chunk.UVs.Count}"); + for (int i = 0; i < chunk.UVs.Count; i++) + { + var uv = chunk.UVs[i]; + float u = uv.X; + float v = 1f - uv.Y; + dw.Write(u); + dw.Write(v); + if (i < 3) + Logger.Dev(logSrc, $"UV[{i}] = ({u:0.###}, {v:0.###})"); + } + Align(0x10); + + + // triangles + foreach (var (i0, i1, i2, flags) in chunk.Triangles) + { + dw.Write((ushort)i0); dw.Write((ushort)i1); dw.Write((ushort)i2); dw.Write((ushort)flags); + } + Align(0x10); + + // planes + foreach (var (n, d) in chunk.Planes) + { + dw.Write(n.X); dw.Write(n.Y); dw.Write(n.Z); dw.Write(d); + } + Align(0x10); + + // same_pos offsets (zeros) + dw.Write(new byte[chunk.Positions.Count * sizeof(short)]); + Align(0x10); + } + + // Write data block + writer.Write((int)ms.Length); + writer.Write(ms.ToArray()); + writer.Write(-1); // unknown1 + + // batch_info entries + foreach (var chunk in perMat.Values) + { + int nv = chunk.Positions.Count; + int nf = chunk.Triangles.Count; + writer.Write((ushort)nv); + writer.Write((ushort)nf); + writer.Write((ushort)(nv * 12)); // positions_size + writer.Write((ushort)(nf * 8)); // indices_size + writer.Write((ushort)(nv * 2)); // same_pos_vertex_offsets_size + writer.Write((ushort)0); // bone_links_size + writer.Write((ushort)(nv * 8)); // tex_coords_size + writer.Write((uint)5344321); // render_flags + } + + // prop points + writer.Write(0); + + // LOD textures + writer.Write((uint)perMat.Count); + byte ti = 0; + foreach (var mat in perMat.Keys) + { + writer.Write(ti++); + WriteZeroTerminatedString(writer, mat + ".tga"); + } + + // Submesh materials + writer.Write(brush.Solid.Textures.Count); + foreach (var tex in brush.Solid.Textures) + { + WriteFixedString(writer, Path.GetFileName(tex), 32); + writer.Write(0f); // emissive + writer.Write(0f); writer.Write(0f); writer.Write(0f); // unused + WriteFixedString(writer, "", 32); + writer.Write((uint)1); // flags: bit0 + } + + // unknown1 + writer.Write(1); + WriteFixedString(writer, $"Brush_{brush.UID}", 24); + writer.Write(0f); + } + + private static Dictionary GatherGeometry(Brush brush) + { + var perMat = new Dictionary(); + foreach (var face in brush.Solid.Faces) + { + var idx = face.Vertices; + int tris = idx.Count > 3 ? idx.Count - 2 : 1; + for (int i = 0; i < tris; i++) + { + // World positions + Vector3 p0 = Transform(brush, idx[0]); + Vector3 p1 = Transform(brush, idx[i + 1]); + Vector3 p2 = Transform(brush, idx[i + 2]); + + // Texture UV from global brush.UVs + Vector2 uv0 = idx[0] < brush.UVs.Count ? brush.UVs[idx[0]] : Vector2.Zero; + Vector2 uv1 = idx[i + 1] < brush.UVs.Count ? brush.UVs[idx[i + 1]] : Vector2.Zero; + Vector2 uv2 = idx[i + 2] < brush.UVs.Count ? brush.UVs[idx[i + 2]] : Vector2.Zero; + + // Compute normal & plane + var n = Vector3.Normalize(Vector3.Cross(p1 - p0, p2 - p0)); + float d = -Vector3.Dot(n, p0); + + // Material name + string mat = Path.GetFileNameWithoutExtension( + face.TextureIndex >= 0 && face.TextureIndex < brush.Solid.Textures.Count + ? brush.Solid.Textures[face.TextureIndex] + : string.Empty + ); + + if (!perMat.TryGetValue(mat, out var chunk)) + perMat[mat] = chunk = new Chunk(); + + // Add or reuse vertex + int v0 = chunk.AddVertex(p0, n, uv0); + int v1 = chunk.AddVertex(p1, n, uv1); + int v2 = chunk.AddVertex(p2, n, uv2); + + // Record triangle and plane + chunk.Triangles.Add((v0, v1, v2, 0)); + chunk.Planes.Add((n, d)); + } + } + Logger.Dev(logSrc, $"Gathered geometry: {perMat.Count} materials, total vertices = {perMat.Values.Sum(c => c.Positions.Count)}"); + return perMat; + } + + private static Vector3 Transform(Brush b, int vi) + => Vector3.Transform(b.Vertices[vi], b.RotationMatrix) + b.Position; + + private static void WriteFixedString(BinaryWriter w, string s, int len) + { + var bs = System.Text.Encoding.ASCII.GetBytes(s); + int count = Math.Min(bs.Length, len - 1); + w.Write(bs, 0, count); + for (int i = count; i < len; i++) w.Write((byte)0); + } + + private static void WriteZeroTerminatedString(BinaryWriter w, string s) + { + var bs = System.Text.Encoding.ASCII.GetBytes(s); + w.Write(bs); + w.Write((byte)0); + } + + private static void WriteVec3(BinaryWriter w, Vector3 v) + { + w.Write(v.X); w.Write(v.Y); w.Write(v.Z); + } + + private class Chunk + { + public readonly List Positions = new(); + public readonly List Normals = new(); + public readonly List UVs = new(); + public readonly List<(int, int, int, uint)> Triangles = new(); + public readonly List<(Vector3, float)> Planes = new(); + private readonly Dictionary<(Vector3, Vector3, Vector2), int> map = new(); + public int AddVertex(Vector3 p, Vector3 n, Vector2 uv) + { + var key = (p, n, uv); + if (map.TryGetValue(key, out var idx)) return idx; + idx = Positions.Count; + Positions.Add(p); + Normals.Add(n); + UVs.Add(uv); + map[key] = idx; + return idx; + } + } + } } diff --git a/parsers/ObjParser.cs b/parsers/ObjParser.cs index 05e9340..f1a9e0a 100644 --- a/parsers/ObjParser.cs +++ b/parsers/ObjParser.cs @@ -7,180 +7,180 @@ namespace redux.parsers { - class ObjParser - { - private const string logSrc = "ObjParser"; - - public static Mesh ReadObj(string objPath) - { - Logger.Info(logSrc, $"Parsing OBJ file: {objPath}"); - - if (!Config.TriangulatePolygons) - Logger.Info(logSrc, $"-ngons option used, not forcing triangulation"); - - var mesh = new Mesh(); - var positions = new List(); - var uvs = new List(); - var normals = new List(); - var textureIndexByMaterial = new Dictionary(StringComparer.OrdinalIgnoreCase); - string currentMaterial = "default"; - - Brush currentBrush = null!; - var currentVertexMap = new Dictionary(); - - foreach (var line in File.ReadLines(objPath)) - { - var trimmed = line.Trim(); - if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith("#")) - continue; - - var parts = trimmed.Split(' ', StringSplitOptions.RemoveEmptyEntries); - switch (parts[0]) - { - case "o" when parts.Length > 1: - currentBrush = CreateNewBrush(parts[1], mesh); - textureIndexByMaterial.Clear(); - currentVertexMap.Clear(); - break; - - case "v" when parts.Length >= 4: - positions.Add(ParseVec3(parts[1], parts[2], parts[3], invertX: true)); - break; - - case "vt" when parts.Length >= 3: - uvs.Add(ParseVec2(parts[1], parts[2], flipV: true)); - break; - - case "vn" when parts.Length >= 4: - normals.Add(ParseVec3(parts[1], parts[2], parts[3], invertX: true)); - break; - - case "usemtl" when parts.Length > 1: - currentMaterial = parts[1]; - currentBrush.TextureName = currentMaterial; - break; - - case "f" when currentBrush != null: - ParseFace(parts, positions, uvs, normals, currentBrush, - textureIndexByMaterial, currentVertexMap); - break; - } - } - - Logger.Info(logSrc, $"Parsed OBJ → {mesh.Brushes.Count} brushes."); - return mesh; - } - - static Brush CreateNewBrush(string objectName, Mesh mesh) - { - int uid = ObjUtils.ParseUidFromObjectName(objectName, mesh.Brushes.Count + 1); - var flags = ObjUtils.ParseFlagsFromObjectName(objectName); - var brush = new Brush - { - UID = uid, - Position = Vector3.Zero, - RotationMatrix = Matrix4x4.Identity, - TextureName = "missing_texture", - Solid = new Solid { State = 3, Life = -1, Flags = flags } - }; - mesh.Brushes.Add(brush); - Logger.Debug(logSrc, $"Created Brush {objectName} UID={uid} Flags=0x{flags:X}"); - return brush; - } - - static void ParseFace(string[] parts, - List positions, - List uvs, - List normals, - Brush brush, - Dictionary texMap, - Dictionary vmap) - { - if (!texMap.TryGetValue(brush.TextureName, out int texIndex)) - { - string tex = brush.TextureName.EndsWith(".tga", StringComparison.OrdinalIgnoreCase) - ? brush.TextureName - : brush.TextureName + ".tga"; - brush.Solid.Textures.Add(tex); - texIndex = brush.Solid.Textures.Count - 1; - texMap[brush.TextureName] = texIndex; - } - - var faceIdxs = new List(); - for (int i = 1; i < parts.Length; i++) - { - var vp = parts[i].Split('/'); - int pi = int.Parse(vp[0], CultureInfo.InvariantCulture) - 1; - int ui = vp.Length > 1 && vp[1] != "" ? int.Parse(vp[1], CultureInfo.InvariantCulture) - 1 : -1; - int ni = vp.Length > 2 && vp[2] != "" ? int.Parse(vp[2], CultureInfo.InvariantCulture) - 1 : -1; - - var pos = pi >= 0 && pi < positions.Count ? positions[pi] : Vector3.Zero; - var uv = ui >= 0 && ui < uvs.Count ? uvs[ui] : Vector2.Zero; - var norm = ni >= 0 && ni < normals.Count ? normals[ni] : Vector3.Zero; - - var key = new RflUtils.VertexKey(pos, uv, norm); - if (!vmap.TryGetValue(key, out int idx)) - { - idx = brush.Vertices.Count; - brush.Vertices.Add(pos); - brush.UVs.Add(uv); - brush.Normals.Add(norm); - vmap[key] = idx; - } - faceIdxs.Add(idx); - } - - // build faces - if (Config.TriangulatePolygons) - { - for (int i = 1; i < faceIdxs.Count - 1; i++) - AddFace(brush, faceIdxs[0], faceIdxs[i], faceIdxs[i + 1], texIndex); - } - else - AddFace(brush, faceIdxs, texIndex); - } - - static void AddFace(Brush brush, int i0, int i1, int i2, int texIndex) - { - brush.Indices.AddRange(new[] { i0, i1, i2 }); - var face = new Face { TextureIndex = texIndex }; - - face.Vertices.AddRange(new[] { i0, i1, i2 }); - face.UVs.AddRange(new[] { brush.UVs[i0], brush.UVs[i1], brush.UVs[i2] }); - - var normal = face.Normal = Vector3.Normalize(Vector3.Cross( - brush.Vertices[i1] - brush.Vertices[i0], - brush.Vertices[i2] - brush.Vertices[i0] - )); - - // flip if requested - //if (Config.FlipNormals) - //normal = -normal; - - face.Normal = normal; - - brush.Solid.Faces.Add(face); - } - - static void AddFace(Brush brush, List idxs, int texIndex) - { - for (int i = 1; i < idxs.Count - 1; i++) - AddFace(brush, idxs[0], idxs[i], idxs[i + 1], texIndex); - } - - static Vector3 ParseVec3(string sx, string sy, string sz, bool invertX = false) - { - float x = float.Parse(sx, CultureInfo.InvariantCulture); - float y = float.Parse(sy, CultureInfo.InvariantCulture); - float z = float.Parse(sz, CultureInfo.InvariantCulture); - return invertX ? new Vector3(-x, y, z) : new Vector3(x, y, z); - } - - static Vector2 ParseVec2(string su, string sv, bool flipV = false) - { - var u = float.Parse(su, CultureInfo.InvariantCulture); - var v = float.Parse(sv, CultureInfo.InvariantCulture); - return flipV ? new Vector2(u, 1 - v) : new Vector2(u, v); - } - } + class ObjParser + { + private const string logSrc = "ObjParser"; + + public static Mesh ReadObj(string objPath) + { + Logger.Info(logSrc, $"Parsing OBJ file: {objPath}"); + + if (!Config.TriangulatePolygons) + Logger.Info(logSrc, $"-ngons option used, not forcing triangulation"); + + var mesh = new Mesh(); + var positions = new List(); + var uvs = new List(); + var normals = new List(); + var textureIndexByMaterial = new Dictionary(StringComparer.OrdinalIgnoreCase); + string currentMaterial = "default"; + + Brush currentBrush = null!; + var currentVertexMap = new Dictionary(); + + foreach (var line in File.ReadLines(objPath)) + { + var trimmed = line.Trim(); + if (string.IsNullOrEmpty(trimmed) || trimmed.StartsWith("#")) + continue; + + var parts = trimmed.Split(' ', StringSplitOptions.RemoveEmptyEntries); + switch (parts[0]) + { + case "o" when parts.Length > 1: + currentBrush = CreateNewBrush(parts[1], mesh); + textureIndexByMaterial.Clear(); + currentVertexMap.Clear(); + break; + + case "v" when parts.Length >= 4: + positions.Add(ParseVec3(parts[1], parts[2], parts[3], invertX: true)); + break; + + case "vt" when parts.Length >= 3: + uvs.Add(ParseVec2(parts[1], parts[2], flipV: true)); + break; + + case "vn" when parts.Length >= 4: + normals.Add(ParseVec3(parts[1], parts[2], parts[3], invertX: true)); + break; + + case "usemtl" when parts.Length > 1: + currentMaterial = parts[1]; + currentBrush.TextureName = currentMaterial; + break; + + case "f" when currentBrush != null: + ParseFace(parts, positions, uvs, normals, currentBrush, + textureIndexByMaterial, currentVertexMap); + break; + } + } + + Logger.Info(logSrc, $"Parsed OBJ → {mesh.Brushes.Count} brushes."); + return mesh; + } + + static Brush CreateNewBrush(string objectName, Mesh mesh) + { + int uid = ObjUtils.ParseUidFromObjectName(objectName, mesh.Brushes.Count + 1); + var flags = ObjUtils.ParseFlagsFromObjectName(objectName); + var brush = new Brush + { + UID = uid, + Position = Vector3.Zero, + RotationMatrix = Matrix4x4.Identity, + TextureName = "missing_texture", + Solid = new Solid { State = 3, Life = -1, Flags = flags } + }; + mesh.Brushes.Add(brush); + Logger.Debug(logSrc, $"Created Brush {objectName} UID={uid} Flags=0x{flags:X}"); + return brush; + } + + static void ParseFace(string[] parts, + List positions, + List uvs, + List normals, + Brush brush, + Dictionary texMap, + Dictionary vmap) + { + if (!texMap.TryGetValue(brush.TextureName, out int texIndex)) + { + string tex = brush.TextureName.EndsWith(".tga", StringComparison.OrdinalIgnoreCase) + ? brush.TextureName + : brush.TextureName + ".tga"; + brush.Solid.Textures.Add(tex); + texIndex = brush.Solid.Textures.Count - 1; + texMap[brush.TextureName] = texIndex; + } + + var faceIdxs = new List(); + for (int i = 1; i < parts.Length; i++) + { + var vp = parts[i].Split('/'); + int pi = int.Parse(vp[0], CultureInfo.InvariantCulture) - 1; + int ui = vp.Length > 1 && vp[1] != "" ? int.Parse(vp[1], CultureInfo.InvariantCulture) - 1 : -1; + int ni = vp.Length > 2 && vp[2] != "" ? int.Parse(vp[2], CultureInfo.InvariantCulture) - 1 : -1; + + var pos = pi >= 0 && pi < positions.Count ? positions[pi] : Vector3.Zero; + var uv = ui >= 0 && ui < uvs.Count ? uvs[ui] : Vector2.Zero; + var norm = ni >= 0 && ni < normals.Count ? normals[ni] : Vector3.Zero; + + var key = new RflUtils.VertexKey(pos, uv, norm); + if (!vmap.TryGetValue(key, out int idx)) + { + idx = brush.Vertices.Count; + brush.Vertices.Add(pos); + brush.UVs.Add(uv); + brush.Normals.Add(norm); + vmap[key] = idx; + } + faceIdxs.Add(idx); + } + + // build faces + if (Config.TriangulatePolygons) + { + for (int i = 1; i < faceIdxs.Count - 1; i++) + AddFace(brush, faceIdxs[0], faceIdxs[i], faceIdxs[i + 1], texIndex); + } + else + AddFace(brush, faceIdxs, texIndex); + } + + static void AddFace(Brush brush, int i0, int i1, int i2, int texIndex) + { + brush.Indices.AddRange(new[] { i0, i1, i2 }); + var face = new Face { TextureIndex = texIndex }; + + face.Vertices.AddRange(new[] { i0, i1, i2 }); + face.UVs.AddRange(new[] { brush.UVs[i0], brush.UVs[i1], brush.UVs[i2] }); + + var normal = face.Normal = Vector3.Normalize(Vector3.Cross( + brush.Vertices[i1] - brush.Vertices[i0], + brush.Vertices[i2] - brush.Vertices[i0] + )); + + // flip if requested + //if (Config.FlipNormals) + //normal = -normal; + + face.Normal = normal; + + brush.Solid.Faces.Add(face); + } + + static void AddFace(Brush brush, List idxs, int texIndex) + { + for (int i = 1; i < idxs.Count - 1; i++) + AddFace(brush, idxs[0], idxs[i], idxs[i + 1], texIndex); + } + + static Vector3 ParseVec3(string sx, string sy, string sz, bool invertX = false) + { + float x = float.Parse(sx, CultureInfo.InvariantCulture); + float y = float.Parse(sy, CultureInfo.InvariantCulture); + float z = float.Parse(sz, CultureInfo.InvariantCulture); + return invertX ? new Vector3(-x, y, z) : new Vector3(x, y, z); + } + + static Vector2 ParseVec2(string su, string sv, bool flipV = false) + { + var u = float.Parse(su, CultureInfo.InvariantCulture); + var v = float.Parse(sv, CultureInfo.InvariantCulture); + return flipV ? new Vector2(u, 1 - v) : new Vector2(u, v); + } + } } diff --git a/parsers/PegParser.cs b/parsers/PegParser.cs index 329b310..4abdca6 100644 --- a/parsers/PegParser.cs +++ b/parsers/PegParser.cs @@ -5,360 +5,360 @@ namespace redux.parsers { - class PegParser - { - private const string logSrc = "PegParser"; - - public static void ExtractPeg(string pegPath) - { - Logger.Info(logSrc, $"Parsing PEG file: {pegPath}"); - if (!File.Exists(pegPath)) - { - Logger.Error(logSrc, $"File not found: {pegPath}"); - return; - } - - using var fs = File.OpenRead(pegPath); - long fileSize = fs.Length; - using var br = new BinaryReader(fs); - - // 1) Read and validate signature - uint signature = br.ReadUInt32(); - if (signature != 0x564B4547) // “GEKV” little-endian - { - Logger.Error(logSrc, $"Invalid PEG signature: 0x{signature:X}"); - return; - } - - // 2) Read version - uint version = br.ReadUInt32(); - if (version != 6) - { - Logger.Error(logSrc, $"Unsupported PEG version: {version}"); - return; - } - - // 3) Read header fields - uint hdrSz = br.ReadUInt32(); - uint datSz = br.ReadUInt32(); - uint texCount = br.ReadUInt32(); - uint unknown14 = br.ReadUInt32(); - uint frameCount = br.ReadUInt32(); - uint unknown1C = br.ReadUInt32(); - - // Pre-allocate texture entries - var textures = new List((int)texCount); - for (int i = 0; i < texCount; i++) - textures.Add(new Texx()); - - // 4) Read each texture’s header - for (int i = 0; i < texCount; i++) - { - var tx = textures[i]; - tx.Width = br.ReadUInt16(); - tx.Height = br.ReadUInt16(); - tx.Fmt = br.ReadByte(); - tx.Fmt2 = br.ReadByte(); - tx.Flgs = br.ReadByte(); - tx.FrmCnt = br.ReadByte(); - tx.AnimDelay = br.ReadByte(); - tx.MipCount = br.ReadByte(); - tx.Unk1 = br.ReadByte(); - tx.Unk2 = br.ReadByte(); - - // Read fixed-length 48-byte name (null-terminated) - byte[] nameBytes = br.ReadBytes(48); - int nameLen = Array.IndexOf(nameBytes, 0); - if (nameLen < 0) nameLen = 48; - tx.Name = Encoding.ASCII.GetString(nameBytes, 0, nameLen); - - tx.Offset = br.ReadUInt32(); - - // Compute size of the *previous* texture once we know this offset - if (i > 0) - { - textures[i - 1].Size = tx.Offset - textures[i - 1].Offset; - } - - // Last texture’s size extends to EOF - if (i == texCount - 1) - { - tx.Size = (uint)fileSize - tx.Offset; - } - } - - // 5) Create output directory (same folder as .peg, named without extension) - string outputDir = Path.Combine( - Path.GetDirectoryName(pegPath) ?? "", - Path.GetFileNameWithoutExtension(pegPath) - ); - if (!Directory.Exists(outputDir)) - Directory.CreateDirectory(outputDir); - - // 6) Decode & save each texture - foreach (var tx in textures) - { - fs.Seek(tx.Offset, SeekOrigin.Begin); - byte[] data = br.ReadBytes((int)tx.Size); - - string originalName = tx.Name; - string processedName = originalName; - if (Config.TranslateRF2Textures) - { - processedName = RF2TextureTranslator.TranslateRF2Texture(originalName); - Logger.Dev(logSrc, $"TranslateRF2Textures: \"{originalName}\" → \"{processedName}\""); - } - else if (Config.InsertRF2TexturePrefix) - { - processedName = RF2TextureTranslator.InsertRxPrefix(originalName); - Logger.Dev(logSrc, $"InsertRF2TexturePrefix: \"{originalName}\" → \"{processedName}\""); - } - else - { - // no change - Logger.Debug(logSrc, $"No translation/prefix: \"{originalName}\""); - } - - string baseName = Path.GetFileNameWithoutExtension(processedName); - - Bitmap bmp = null; - try - { - switch (tx.Fmt) - { - case 3: - bmp = DecodeA1B5G5R5(tx, data); - break; - case 4: - bmp = DecodeIndexed(tx, data); - break; - case 7: - bmp = DecodeA8B8G8R8(tx, data); - break; - default: - Logger.Warn(logSrc, $"Unknown format {tx.Fmt} for texture '{processedName}'. Skipping."); - continue; - } - - // Remove any existing extension, e.g. “.tga” - string baseNameTx = Path.GetFileNameWithoutExtension(processedName); - - if (Config.ExportImageFormat == Config.ImageFormat.png) - { - string fileName = baseNameTx + ".png"; - string outPath = Path.Combine(outputDir, fileName); - bmp.Save(outPath, System.Drawing.Imaging.ImageFormat.Png); - Logger.Info(logSrc, $"Saved texture '{processedName}' as PNG → {outPath}"); - } - else // tga - { - string fileName = baseNameTx + ".tga"; - string outPath = Path.Combine(outputDir, fileName); - SaveAsTga(bmp, outPath); - Logger.Info(logSrc, $"Saved texture '{processedName}' as TGA → {outPath}"); - } - } - catch (Exception ex) - { - Logger.Error(logSrc, $"Error decoding texture '{processedName}': {ex.Message}"); - } - finally - { - bmp?.Dispose(); - } - } - } - - private static Bitmap DecodeA1B5G5R5(Texx tx, byte[] input) - { - Logger.Dev(logSrc, $"Converting {tx.Name} using DecodeA1B5G5R5"); - int width = tx.Width; - int height = tx.Height; - var bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb); - - int ipos = 0; - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - ushort wrd = BitConverter.ToUInt16(input, ipos); - byte a = (byte)((wrd & 0x8000) != 0 ? 255 : 0); - byte b = (byte)(((wrd >> 10) & 0x1F) * 255 / 31); - byte g = (byte)(((wrd >> 5) & 0x1F) * 255 / 31); - byte r = (byte)((wrd & 0x1F) * 255 / 31); - - bmp.SetPixel(x, y, Color.FromArgb(a, r, g, b)); - ipos += 2; - } - } - - return bmp; - } - - private static Bitmap DecodeA8B8G8R8(Texx tx, byte[] input) - { - Logger.Dev(logSrc, $"Converting {tx.Name} using DecodeA8B8G8R8"); - int width = tx.Width; - int height = tx.Height; - var bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb); - - int ipos = 0; - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - byte r = input[ipos++]; - byte g = input[ipos++]; - byte b = input[ipos++]; - byte rawA = input[ipos++]; - // Stretch 0…128 → 0…255: - byte a = ScaleAlpha7to8(rawA); - - bmp.SetPixel(x, y, Color.FromArgb(a, r, g, b)); - } - } - - return bmp; - } - private static Bitmap DecodeIndexed(Texx tx, byte[] input) - { - Logger.Dev(logSrc, $"Converting {tx.Name} using DecodeIndexed"); - int width = tx.Width; - int height = tx.Height; - - // Build 256-entry palette: - var palette = new Color[256]; - int ipos = 0; - for (int i = 0; i < 256; i++) - { - if (tx.Fmt2 == 1) - { - // A1B5G5R5: one‐bit alpha → clamp 0 or 255 - ushort wrd = (ushort)(input[ipos++] | (input[ipos++] << 8)); - byte a = (byte)((wrd & 0x8000) != 0 ? 255 : 0); - byte b = (byte)(((wrd >> 10) & 0x1F) * 255 / 31); - byte g = (byte)(((wrd >> 5) & 0x1F) * 255 / 31); - byte r = (byte)((wrd & 0x1F) * 255 / 31); - palette[i] = Color.FromArgb(a, r, g, b); - } - else if (tx.Fmt2 == 2) - { - // A8B8G8R8 in the palette: rawA ∈ [0..128], - // so scale 0..128→0..255 - byte r = input[ipos++]; - byte g = input[ipos++]; - byte b = input[ipos++]; - byte rawA = input[ipos++]; - byte a = ScaleAlpha7to8(rawA); - - palette[i] = Color.FromArgb(a, r, g, b); - } - else - { - palette[i] = Color.FromArgb(0, 0, 0, 0); - } - } - - // Now decode each pixel’s index: - var bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb); - for (int y = 0; y < height; y++) - { - for (int x = 0; x < width; x++) - { - byte idx = input[ipos++]; - int sw = (idx & 0xE7) | ((idx >> 1) & 0x08) | ((idx << 1) & 0x10); - Color c = palette[sw]; - bmp.SetPixel(x, y, c); - } - } - - return bmp; - } - - private static void SaveAsTga(Bitmap bmp, string outPath) - { - int width = bmp.Width; - int height = bmp.Height; - - // 1) Determine if we actually need alpha. - bool needsAlpha = false; - for (int y = 0; y < height && !needsAlpha; y++) - { - for (int x = 0; x < width; x++) - { - Color c = bmp.GetPixel(x, y); - if (c.A < 255) - { - needsAlpha = true; - break; - } - } - } - - // 2) Write TGA header. - using var fs = File.Create(outPath); - using var bw = new BinaryWriter(fs); - - byte imageType = 2; // uncompressed true-color - byte pixelDepth = (byte)(needsAlpha ? 32 : 24); - byte descriptor = (byte)(needsAlpha ? 0x08 : 0x00); - // 0x08 = 8 bits of alpha; origin bottom-left - - // --- 18-byte header --- - bw.Write((byte)0); // ID length - bw.Write((byte)0); // Color map type - bw.Write(imageType); // Image type = 2 - - // Color map spec (unused for true-color) - bw.Write((ushort)0); // first entry index - bw.Write((ushort)0); // color map length - bw.Write((byte)0); // color map entry size - - // Image spec - bw.Write((ushort)0); // x‐origin - bw.Write((ushort)0); // y‐origin - bw.Write((ushort)width); // width - bw.Write((ushort)height); // height - bw.Write(pixelDepth); // pixel depth (24 or 32 bits) - bw.Write(descriptor); // image descriptor - - // 3) Write pixel data bottom→top, left→right - for (int y = height - 1; y >= 0; y--) - { - for (int x = 0; x < width; x++) - { - Color c = bmp.GetPixel(x, y); - // TGA expects B, G, R, [A] - bw.Write(c.B); - bw.Write(c.G); - bw.Write(c.R); - if (needsAlpha) - bw.Write(c.A); - } - } - // no footer needed for uncompressed - } - private static byte ScaleAlpha7to8(byte rawA) - { - // rawA [0..128], map 0→0 and 128→255 - return (byte)(rawA * 255 / 128); - } - - private class Texx - { - public string Name = ""; - public int Width; - public int Height; - public byte Fmt; - public byte Fmt2; - public byte Flgs; - public byte FrmCnt; - public byte AnimDelay; - public byte MipCount; - public byte Unk1; - public byte Unk2; - public uint Offset; - public uint Size; - } - } + class PegParser + { + private const string logSrc = "PegParser"; + + public static void ExtractPeg(string pegPath) + { + Logger.Info(logSrc, $"Parsing PEG file: {pegPath}"); + if (!File.Exists(pegPath)) + { + Logger.Error(logSrc, $"File not found: {pegPath}"); + return; + } + + using var fs = File.OpenRead(pegPath); + long fileSize = fs.Length; + using var br = new BinaryReader(fs); + + // 1) Read and validate signature + uint signature = br.ReadUInt32(); + if (signature != 0x564B4547) // “GEKV” little-endian + { + Logger.Error(logSrc, $"Invalid PEG signature: 0x{signature:X}"); + return; + } + + // 2) Read version + uint version = br.ReadUInt32(); + if (version != 6) + { + Logger.Error(logSrc, $"Unsupported PEG version: {version}"); + return; + } + + // 3) Read header fields + uint hdrSz = br.ReadUInt32(); + uint datSz = br.ReadUInt32(); + uint texCount = br.ReadUInt32(); + uint unknown14 = br.ReadUInt32(); + uint frameCount = br.ReadUInt32(); + uint unknown1C = br.ReadUInt32(); + + // Pre-allocate texture entries + var textures = new List((int)texCount); + for (int i = 0; i < texCount; i++) + textures.Add(new Texx()); + + // 4) Read each texture’s header + for (int i = 0; i < texCount; i++) + { + var tx = textures[i]; + tx.Width = br.ReadUInt16(); + tx.Height = br.ReadUInt16(); + tx.Fmt = br.ReadByte(); + tx.Fmt2 = br.ReadByte(); + tx.Flgs = br.ReadByte(); + tx.FrmCnt = br.ReadByte(); + tx.AnimDelay = br.ReadByte(); + tx.MipCount = br.ReadByte(); + tx.Unk1 = br.ReadByte(); + tx.Unk2 = br.ReadByte(); + + // Read fixed-length 48-byte name (null-terminated) + byte[] nameBytes = br.ReadBytes(48); + int nameLen = Array.IndexOf(nameBytes, 0); + if (nameLen < 0) nameLen = 48; + tx.Name = Encoding.ASCII.GetString(nameBytes, 0, nameLen); + + tx.Offset = br.ReadUInt32(); + + // Compute size of the *previous* texture once we know this offset + if (i > 0) + { + textures[i - 1].Size = tx.Offset - textures[i - 1].Offset; + } + + // Last texture’s size extends to EOF + if (i == texCount - 1) + { + tx.Size = (uint)fileSize - tx.Offset; + } + } + + // 5) Create output directory (same folder as .peg, named without extension) + string outputDir = Path.Combine( + Path.GetDirectoryName(pegPath) ?? "", + Path.GetFileNameWithoutExtension(pegPath) + ); + if (!Directory.Exists(outputDir)) + Directory.CreateDirectory(outputDir); + + // 6) Decode & save each texture + foreach (var tx in textures) + { + fs.Seek(tx.Offset, SeekOrigin.Begin); + byte[] data = br.ReadBytes((int)tx.Size); + + string originalName = tx.Name; + string processedName = originalName; + if (Config.TranslateRF2Textures) + { + processedName = RF2TextureTranslator.TranslateRF2Texture(originalName); + Logger.Dev(logSrc, $"TranslateRF2Textures: \"{originalName}\" → \"{processedName}\""); + } + else if (Config.InsertRF2TexturePrefix) + { + processedName = RF2TextureTranslator.InsertRxPrefix(originalName); + Logger.Dev(logSrc, $"InsertRF2TexturePrefix: \"{originalName}\" → \"{processedName}\""); + } + else + { + // no change + Logger.Debug(logSrc, $"No translation/prefix: \"{originalName}\""); + } + + string baseName = Path.GetFileNameWithoutExtension(processedName); + + Bitmap bmp = null; + try + { + switch (tx.Fmt) + { + case 3: + bmp = DecodeA1B5G5R5(tx, data); + break; + case 4: + bmp = DecodeIndexed(tx, data); + break; + case 7: + bmp = DecodeA8B8G8R8(tx, data); + break; + default: + Logger.Warn(logSrc, $"Unknown format {tx.Fmt} for texture '{processedName}'. Skipping."); + continue; + } + + // Remove any existing extension, e.g. “.tga” + string baseNameTx = Path.GetFileNameWithoutExtension(processedName); + + if (Config.ExportImageFormat == Config.ImageFormat.png) + { + string fileName = baseNameTx + ".png"; + string outPath = Path.Combine(outputDir, fileName); + bmp.Save(outPath, System.Drawing.Imaging.ImageFormat.Png); + Logger.Info(logSrc, $"Saved texture '{processedName}' as PNG → {outPath}"); + } + else // tga + { + string fileName = baseNameTx + ".tga"; + string outPath = Path.Combine(outputDir, fileName); + SaveAsTga(bmp, outPath); + Logger.Info(logSrc, $"Saved texture '{processedName}' as TGA → {outPath}"); + } + } + catch (Exception ex) + { + Logger.Error(logSrc, $"Error decoding texture '{processedName}': {ex.Message}"); + } + finally + { + bmp?.Dispose(); + } + } + } + + private static Bitmap DecodeA1B5G5R5(Texx tx, byte[] input) + { + Logger.Dev(logSrc, $"Converting {tx.Name} using DecodeA1B5G5R5"); + int width = tx.Width; + int height = tx.Height; + var bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb); + + int ipos = 0; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + ushort wrd = BitConverter.ToUInt16(input, ipos); + byte a = (byte)((wrd & 0x8000) != 0 ? 255 : 0); + byte b = (byte)(((wrd >> 10) & 0x1F) * 255 / 31); + byte g = (byte)(((wrd >> 5) & 0x1F) * 255 / 31); + byte r = (byte)((wrd & 0x1F) * 255 / 31); + + bmp.SetPixel(x, y, Color.FromArgb(a, r, g, b)); + ipos += 2; + } + } + + return bmp; + } + + private static Bitmap DecodeA8B8G8R8(Texx tx, byte[] input) + { + Logger.Dev(logSrc, $"Converting {tx.Name} using DecodeA8B8G8R8"); + int width = tx.Width; + int height = tx.Height; + var bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb); + + int ipos = 0; + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + byte r = input[ipos++]; + byte g = input[ipos++]; + byte b = input[ipos++]; + byte rawA = input[ipos++]; + // Stretch 0…128 → 0…255: + byte a = ScaleAlpha7to8(rawA); + + bmp.SetPixel(x, y, Color.FromArgb(a, r, g, b)); + } + } + + return bmp; + } + private static Bitmap DecodeIndexed(Texx tx, byte[] input) + { + Logger.Dev(logSrc, $"Converting {tx.Name} using DecodeIndexed"); + int width = tx.Width; + int height = tx.Height; + + // Build 256-entry palette: + var palette = new Color[256]; + int ipos = 0; + for (int i = 0; i < 256; i++) + { + if (tx.Fmt2 == 1) + { + // A1B5G5R5: one‐bit alpha → clamp 0 or 255 + ushort wrd = (ushort)(input[ipos++] | (input[ipos++] << 8)); + byte a = (byte)((wrd & 0x8000) != 0 ? 255 : 0); + byte b = (byte)(((wrd >> 10) & 0x1F) * 255 / 31); + byte g = (byte)(((wrd >> 5) & 0x1F) * 255 / 31); + byte r = (byte)((wrd & 0x1F) * 255 / 31); + palette[i] = Color.FromArgb(a, r, g, b); + } + else if (tx.Fmt2 == 2) + { + // A8B8G8R8 in the palette: rawA ∈ [0..128], + // so scale 0..128→0..255 + byte r = input[ipos++]; + byte g = input[ipos++]; + byte b = input[ipos++]; + byte rawA = input[ipos++]; + byte a = ScaleAlpha7to8(rawA); + + palette[i] = Color.FromArgb(a, r, g, b); + } + else + { + palette[i] = Color.FromArgb(0, 0, 0, 0); + } + } + + // Now decode each pixel’s index: + var bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb); + for (int y = 0; y < height; y++) + { + for (int x = 0; x < width; x++) + { + byte idx = input[ipos++]; + int sw = (idx & 0xE7) | ((idx >> 1) & 0x08) | ((idx << 1) & 0x10); + Color c = palette[sw]; + bmp.SetPixel(x, y, c); + } + } + + return bmp; + } + + private static void SaveAsTga(Bitmap bmp, string outPath) + { + int width = bmp.Width; + int height = bmp.Height; + + // 1) Determine if we actually need alpha. + bool needsAlpha = false; + for (int y = 0; y < height && !needsAlpha; y++) + { + for (int x = 0; x < width; x++) + { + Color c = bmp.GetPixel(x, y); + if (c.A < 255) + { + needsAlpha = true; + break; + } + } + } + + // 2) Write TGA header. + using var fs = File.Create(outPath); + using var bw = new BinaryWriter(fs); + + byte imageType = 2; // uncompressed true-color + byte pixelDepth = (byte)(needsAlpha ? 32 : 24); + byte descriptor = (byte)(needsAlpha ? 0x08 : 0x00); + // 0x08 = 8 bits of alpha; origin bottom-left + + // --- 18-byte header --- + bw.Write((byte)0); // ID length + bw.Write((byte)0); // Color map type + bw.Write(imageType); // Image type = 2 + + // Color map spec (unused for true-color) + bw.Write((ushort)0); // first entry index + bw.Write((ushort)0); // color map length + bw.Write((byte)0); // color map entry size + + // Image spec + bw.Write((ushort)0); // x‐origin + bw.Write((ushort)0); // y‐origin + bw.Write((ushort)width); // width + bw.Write((ushort)height); // height + bw.Write(pixelDepth); // pixel depth (24 or 32 bits) + bw.Write(descriptor); // image descriptor + + // 3) Write pixel data bottom→top, left→right + for (int y = height - 1; y >= 0; y--) + { + for (int x = 0; x < width; x++) + { + Color c = bmp.GetPixel(x, y); + // TGA expects B, G, R, [A] + bw.Write(c.B); + bw.Write(c.G); + bw.Write(c.R); + if (needsAlpha) + bw.Write(c.A); + } + } + // no footer needed for uncompressed + } + private static byte ScaleAlpha7to8(byte rawA) + { + // rawA [0..128], map 0→0 and 128→255 + return (byte)(rawA * 255 / 128); + } + + private class Texx + { + public string Name = ""; + public int Width; + public int Height; + public byte Fmt; + public byte Fmt2; + public byte Flgs; + public byte FrmCnt; + public byte AnimDelay; + public byte MipCount; + public byte Unk1; + public byte Unk2; + public uint Offset; + public uint Size; + } + } } diff --git a/parsers/RfaParser.cs b/parsers/RfaParser.cs index 73a30de..bb39fd0 100644 --- a/parsers/RfaParser.cs +++ b/parsers/RfaParser.cs @@ -7,234 +7,234 @@ namespace redux.parsers { - - public static class RfaParser - { - private const string logSrc = "RfaParser"; - private const int EXPECTED_MAGIC = 0x46564D56; - - public static RfaFile ReadRfa(string path) - { - Logger.Debug(logSrc, $"ReadRfa: Opening \"{path}\" for reading."); - using var fs = File.OpenRead(path); - using var reader = new BinaryReader(fs); - - var result = new RfaFile(); - - result.Header = ReadHeader(reader); - result.Bones = ReadBones(reader, result.Header); - result.MorphVertexMappings = ReadMorphVertexMappings(reader, result.Header); - result.MorphKeyframes = ReadMorphKeyframes(reader, result.Header); - - Logger.Debug(logSrc, $"ReadRfa: Finished reading \"{path}\". Total bones: {result.Header.NumBones}, " + - $"morph vertices: {result.Header.NumMorphVertices}, " + - $"morph keyframes: {result.Header.NumMorphKeyframes}."); - return result; - } - - private static RfaHeader ReadHeader(BinaryReader r) - { - Logger.Debug(logSrc, "ReadHeader: Seeking to file start (0) and reading header."); - r.BaseStream.Seek(0, SeekOrigin.Begin); - - var hdr = new RfaHeader(); - - hdr.Magic = r.ReadBytes(4); - int magicInt = BitConverter.ToInt32(hdr.Magic, 0); - Logger.Debug(logSrc, $"Header: magic bytes = 0x{magicInt:X8} (\"{(char)hdr.Magic[0]}{(char)hdr.Magic[1]}{(char)hdr.Magic[2]}{(char)hdr.Magic[3]}\")"); - if (magicInt != EXPECTED_MAGIC) - throw new InvalidDataException($"RFA: magic mismatch (got 0x{magicInt:X8}, expected 0x{EXPECTED_MAGIC:X8})."); - - // Version - hdr.Version = r.ReadInt32(); - Logger.Debug(logSrc, $"Header: version = {hdr.Version}"); - - // PosReduction, RotReduction - hdr.PosReduction = r.ReadSingle(); - hdr.RotReduction = r.ReadSingle(); - Logger.Debug(logSrc, $"Header: pos_reduction = {hdr.PosReduction}, rot_reduction = {hdr.RotReduction}"); - - // StartTime, EndTime - hdr.StartTime = r.ReadInt32(); - hdr.EndTime = r.ReadInt32(); - Logger.Debug(logSrc, $"Header: start_time = {hdr.StartTime}, end_time = {hdr.EndTime}"); - - // NumBones, NumMorphVertices, NumMorphKeyframes - hdr.NumBones = r.ReadInt32(); - hdr.NumMorphVertices = r.ReadInt32(); - hdr.NumMorphKeyframes = r.ReadInt32(); - Logger.Debug(logSrc, $"Header: num_bones = {hdr.NumBones}, num_morph_vertices = {hdr.NumMorphVertices}, num_morph_keyframes = {hdr.NumMorphKeyframes}"); - - // RampInTime, RampOutTime - hdr.RampInTime = r.ReadInt32(); - hdr.RampOutTime = r.ReadInt32(); - Logger.Debug(logSrc, $"Header: ramp_in_time = {hdr.RampInTime}, ramp_out_time = {hdr.RampOutTime}"); - - // TotalRotation - float rx = r.ReadSingle(); - float ry = r.ReadSingle(); - float rz = r.ReadSingle(); - float rw = r.ReadSingle(); - hdr.TotalRotation = new Quaternion(rx, ry, rz, rw); - Logger.Debug(logSrc, $"Header: total_rotation = ({rx}, {ry}, {rz}, {rw})"); - - // TotalTranslation - float tx = r.ReadSingle(); - float ty = r.ReadSingle(); - float tz = r.ReadSingle(); - hdr.TotalTranslation = new Vector3(tx, ty, tz); - Logger.Debug(logSrc, $"Header: total_translation = ({tx}, {ty}, {tz})"); - - // MorphVertexMappingsOffset, MorphVertexDataOffset - hdr.MorphVertexMappingsOffset = r.ReadInt32(); - hdr.MorphVertexDataOffset = r.ReadInt32(); - Logger.Debug(logSrc, $"Header: morph_vert_mappings_offset = {hdr.MorphVertexMappingsOffset}, morph_vert_data_offset = {hdr.MorphVertexDataOffset}"); - - // BoneOffsets (int32[num_bones]) - hdr.BoneOffsets = new int[hdr.NumBones]; - for (int i = 0; i < hdr.NumBones; i++) - { - hdr.BoneOffsets[i] = r.ReadInt32(); - Logger.Debug(logSrc, $"Header: bone_offsets[{i}] = {hdr.BoneOffsets[i]}"); - } - - Logger.Debug(logSrc, "ReadHeader: Completed."); - return hdr; - } - - private static List ReadBones(BinaryReader r, RfaHeader hdr) - { - var bones = new List(hdr.NumBones); - Logger.Debug(logSrc, $"ReadBones: About to read {hdr.NumBones} bones."); - - for (int i = 0; i < hdr.NumBones; i++) - { - int offset = hdr.BoneOffsets[i]; - Logger.Debug(logSrc, $"ReadBones: Seeking to bone[{i}] at absolute offset {offset}."); - r.BaseStream.Seek(offset, SeekOrigin.Begin); - - var bone = new RfaBone(); - bone.Weight = r.ReadSingle(); - bone.NumRotationKeys = r.ReadInt16(); - bone.NumTranslationKeys = r.ReadInt16(); - Logger.Debug(logSrc, $"Bone[{i}]: weight={bone.Weight}, num_rotation_keys={bone.NumRotationKeys}, num_translation_keys={bone.NumTranslationKeys}"); - - // RotationKeys - bone.RotationKeys = new List(bone.NumRotationKeys); - for (int k = 0; k < bone.NumRotationKeys; k++) - { - var rotKey = new RfaRotationKey(); - rotKey.Time = r.ReadInt32(); - - // 4×short for quaternion - short qx = r.ReadInt16(); - short qy = r.ReadInt16(); - short qz = r.ReadInt16(); - short qw = r.ReadInt16(); - const float SHORT_QUAT_SCALE = 16383f; - rotKey.Rotation = new Quaternion( - qx / SHORT_QUAT_SCALE, - qy / SHORT_QUAT_SCALE, - qz / SHORT_QUAT_SCALE, - qw / SHORT_QUAT_SCALE - ); - - rotKey.EaseIn = r.ReadByte(); - rotKey.EaseOut = r.ReadByte(); - r.ReadInt16(); // pad - Logger.Debug(logSrc, $"Bone[{i}] RotationKey[{k}]: time={rotKey.Time}, rawQuat=({qx},{qy},{qz},{qw}), normalizedQuat=({rotKey.Rotation.X:F4},{rotKey.Rotation.Y:F4},{rotKey.Rotation.Z:F4},{rotKey.Rotation.W:F4}), easeIn={rotKey.EaseIn}, easeOut={rotKey.EaseOut}"); - - bone.RotationKeys.Add(rotKey); - } - - // TranslationKeys - bone.TranslationKeys = new List(bone.NumTranslationKeys); - for (int k = 0; k < bone.NumTranslationKeys; k++) - { - var transKey = new RfaTranslationKey(); - transKey.Time = r.ReadInt32(); - - float px = r.ReadSingle(); - float py = r.ReadSingle(); - float pz = r.ReadSingle(); - transKey.Translation = new Vector3(px, py, pz); - - float i0 = r.ReadSingle(); - float i1 = r.ReadSingle(); - float i2 = r.ReadSingle(); - transKey.InTangent = new Vector3(i0, i1, i2); - - float o0 = r.ReadSingle(); - float o1 = r.ReadSingle(); - float o2 = r.ReadSingle(); - transKey.OutTangent = new Vector3(o0, o1, o2); - - Logger.Debug(logSrc, $"Bone[{i}] TranslationKey[{k}]: time={transKey.Time}, translation=({px},{py},{pz}), inTangent=({i0},{i1},{i2}), outTangent=({o0},{o1},{o2})"); - bone.TranslationKeys.Add(transKey); - } - - bones.Add(bone); - } - - Logger.Debug(logSrc, "ReadBones: Completed reading all bones."); - return bones; - } - - private static short[] ReadMorphVertexMappings(BinaryReader r, RfaHeader hdr) - { - if (hdr.NumMorphVertices <= 0 || hdr.MorphVertexMappingsOffset == 0) - { - Logger.Debug(logSrc, "ReadMorphVertexMappings: no morph vertices or offset is zero; returning empty array."); - return Array.Empty(); - } - - Logger.Debug(logSrc, $"ReadMorphVertexMappings: Seeking to offset {hdr.MorphVertexMappingsOffset} for {hdr.NumMorphVertices} entries."); - r.BaseStream.Seek(hdr.MorphVertexMappingsOffset, SeekOrigin.Begin); - - var mappings = new short[hdr.NumMorphVertices]; - for (int i = 0; i < hdr.NumMorphVertices; i++) - { - mappings[i] = r.ReadInt16(); - Logger.Debug(logSrc, $"MorphVertexMapping[{i}] = {mappings[i]}"); - } - - Logger.Debug(logSrc, "ReadMorphVertexMappings: Completed."); - return mappings; - } - - private static List ReadMorphKeyframes(BinaryReader r, RfaHeader hdr) - { - var result = new List(); - if (hdr.Version < 8 || hdr.NumMorphKeyframes <= 0 || hdr.MorphVertexDataOffset == 0) - { - Logger.Debug(logSrc, $"ReadMorphKeyframes: version={hdr.Version} (<8) or no keyframes/offset=0; skipping morph keyframe read."); - return result; - } - - Logger.Debug(logSrc, $"ReadMorphKeyframes: Seeking to offset {hdr.MorphVertexDataOffset} for {hdr.NumMorphKeyframes} keyframes of {hdr.NumMorphVertices} vertices each."); - r.BaseStream.Seek(hdr.MorphVertexDataOffset, SeekOrigin.Begin); - - for (int k = 0; k < hdr.NumMorphKeyframes; k++) - { - var mk = new MorphKeyframe(); - mk.Time = r.ReadInt32(); - Logger.Debug(logSrc, $"MorphKeyframe[{k}]: time = {mk.Time}"); - - mk.Positions = new Vector3[hdr.NumMorphVertices]; - for (int v = 0; v < hdr.NumMorphVertices; v++) - { - sbyte sx = r.ReadSByte(); - sbyte sy = r.ReadSByte(); - sbyte sz = r.ReadSByte(); - mk.Positions[v] = new Vector3(sx, sy, sz); - Logger.Debug(logSrc, $" Vertex[{v}] delta = ({sx},{sy},{sz})"); - } - - result.Add(mk); - } - - Logger.Debug(logSrc, "ReadMorphKeyframes: Completed reading all morph keyframes."); - return result; - } - } + + public static class RfaParser + { + private const string logSrc = "RfaParser"; + private const int EXPECTED_MAGIC = 0x46564D56; + + public static RfaFile ReadRfa(string path) + { + Logger.Debug(logSrc, $"ReadRfa: Opening \"{path}\" for reading."); + using var fs = File.OpenRead(path); + using var reader = new BinaryReader(fs); + + var result = new RfaFile(); + + result.Header = ReadHeader(reader); + result.Bones = ReadBones(reader, result.Header); + result.MorphVertexMappings = ReadMorphVertexMappings(reader, result.Header); + result.MorphKeyframes = ReadMorphKeyframes(reader, result.Header); + + Logger.Debug(logSrc, $"ReadRfa: Finished reading \"{path}\". Total bones: {result.Header.NumBones}, " + + $"morph vertices: {result.Header.NumMorphVertices}, " + + $"morph keyframes: {result.Header.NumMorphKeyframes}."); + return result; + } + + private static RfaHeader ReadHeader(BinaryReader r) + { + Logger.Debug(logSrc, "ReadHeader: Seeking to file start (0) and reading header."); + r.BaseStream.Seek(0, SeekOrigin.Begin); + + var hdr = new RfaHeader(); + + hdr.Magic = r.ReadBytes(4); + int magicInt = BitConverter.ToInt32(hdr.Magic, 0); + Logger.Debug(logSrc, $"Header: magic bytes = 0x{magicInt:X8} (\"{(char)hdr.Magic[0]}{(char)hdr.Magic[1]}{(char)hdr.Magic[2]}{(char)hdr.Magic[3]}\")"); + if (magicInt != EXPECTED_MAGIC) + throw new InvalidDataException($"RFA: magic mismatch (got 0x{magicInt:X8}, expected 0x{EXPECTED_MAGIC:X8})."); + + // Version + hdr.Version = r.ReadInt32(); + Logger.Debug(logSrc, $"Header: version = {hdr.Version}"); + + // PosReduction, RotReduction + hdr.PosReduction = r.ReadSingle(); + hdr.RotReduction = r.ReadSingle(); + Logger.Debug(logSrc, $"Header: pos_reduction = {hdr.PosReduction}, rot_reduction = {hdr.RotReduction}"); + + // StartTime, EndTime + hdr.StartTime = r.ReadInt32(); + hdr.EndTime = r.ReadInt32(); + Logger.Debug(logSrc, $"Header: start_time = {hdr.StartTime}, end_time = {hdr.EndTime}"); + + // NumBones, NumMorphVertices, NumMorphKeyframes + hdr.NumBones = r.ReadInt32(); + hdr.NumMorphVertices = r.ReadInt32(); + hdr.NumMorphKeyframes = r.ReadInt32(); + Logger.Debug(logSrc, $"Header: num_bones = {hdr.NumBones}, num_morph_vertices = {hdr.NumMorphVertices}, num_morph_keyframes = {hdr.NumMorphKeyframes}"); + + // RampInTime, RampOutTime + hdr.RampInTime = r.ReadInt32(); + hdr.RampOutTime = r.ReadInt32(); + Logger.Debug(logSrc, $"Header: ramp_in_time = {hdr.RampInTime}, ramp_out_time = {hdr.RampOutTime}"); + + // TotalRotation + float rx = r.ReadSingle(); + float ry = r.ReadSingle(); + float rz = r.ReadSingle(); + float rw = r.ReadSingle(); + hdr.TotalRotation = new Quaternion(rx, ry, rz, rw); + Logger.Debug(logSrc, $"Header: total_rotation = ({rx}, {ry}, {rz}, {rw})"); + + // TotalTranslation + float tx = r.ReadSingle(); + float ty = r.ReadSingle(); + float tz = r.ReadSingle(); + hdr.TotalTranslation = new Vector3(tx, ty, tz); + Logger.Debug(logSrc, $"Header: total_translation = ({tx}, {ty}, {tz})"); + + // MorphVertexMappingsOffset, MorphVertexDataOffset + hdr.MorphVertexMappingsOffset = r.ReadInt32(); + hdr.MorphVertexDataOffset = r.ReadInt32(); + Logger.Debug(logSrc, $"Header: morph_vert_mappings_offset = {hdr.MorphVertexMappingsOffset}, morph_vert_data_offset = {hdr.MorphVertexDataOffset}"); + + // BoneOffsets (int32[num_bones]) + hdr.BoneOffsets = new int[hdr.NumBones]; + for (int i = 0; i < hdr.NumBones; i++) + { + hdr.BoneOffsets[i] = r.ReadInt32(); + Logger.Debug(logSrc, $"Header: bone_offsets[{i}] = {hdr.BoneOffsets[i]}"); + } + + Logger.Debug(logSrc, "ReadHeader: Completed."); + return hdr; + } + + private static List ReadBones(BinaryReader r, RfaHeader hdr) + { + var bones = new List(hdr.NumBones); + Logger.Debug(logSrc, $"ReadBones: About to read {hdr.NumBones} bones."); + + for (int i = 0; i < hdr.NumBones; i++) + { + int offset = hdr.BoneOffsets[i]; + Logger.Debug(logSrc, $"ReadBones: Seeking to bone[{i}] at absolute offset {offset}."); + r.BaseStream.Seek(offset, SeekOrigin.Begin); + + var bone = new RfaBone(); + bone.Weight = r.ReadSingle(); + bone.NumRotationKeys = r.ReadInt16(); + bone.NumTranslationKeys = r.ReadInt16(); + Logger.Debug(logSrc, $"Bone[{i}]: weight={bone.Weight}, num_rotation_keys={bone.NumRotationKeys}, num_translation_keys={bone.NumTranslationKeys}"); + + // RotationKeys + bone.RotationKeys = new List(bone.NumRotationKeys); + for (int k = 0; k < bone.NumRotationKeys; k++) + { + var rotKey = new RfaRotationKey(); + rotKey.Time = r.ReadInt32(); + + // 4×short for quaternion + short qx = r.ReadInt16(); + short qy = r.ReadInt16(); + short qz = r.ReadInt16(); + short qw = r.ReadInt16(); + const float SHORT_QUAT_SCALE = 16383f; + rotKey.Rotation = new Quaternion( + qx / SHORT_QUAT_SCALE, + qy / SHORT_QUAT_SCALE, + qz / SHORT_QUAT_SCALE, + qw / SHORT_QUAT_SCALE + ); + + rotKey.EaseIn = r.ReadByte(); + rotKey.EaseOut = r.ReadByte(); + r.ReadInt16(); // pad + Logger.Debug(logSrc, $"Bone[{i}] RotationKey[{k}]: time={rotKey.Time}, rawQuat=({qx},{qy},{qz},{qw}), normalizedQuat=({rotKey.Rotation.X:F4},{rotKey.Rotation.Y:F4},{rotKey.Rotation.Z:F4},{rotKey.Rotation.W:F4}), easeIn={rotKey.EaseIn}, easeOut={rotKey.EaseOut}"); + + bone.RotationKeys.Add(rotKey); + } + + // TranslationKeys + bone.TranslationKeys = new List(bone.NumTranslationKeys); + for (int k = 0; k < bone.NumTranslationKeys; k++) + { + var transKey = new RfaTranslationKey(); + transKey.Time = r.ReadInt32(); + + float px = r.ReadSingle(); + float py = r.ReadSingle(); + float pz = r.ReadSingle(); + transKey.Translation = new Vector3(px, py, pz); + + float i0 = r.ReadSingle(); + float i1 = r.ReadSingle(); + float i2 = r.ReadSingle(); + transKey.InTangent = new Vector3(i0, i1, i2); + + float o0 = r.ReadSingle(); + float o1 = r.ReadSingle(); + float o2 = r.ReadSingle(); + transKey.OutTangent = new Vector3(o0, o1, o2); + + Logger.Debug(logSrc, $"Bone[{i}] TranslationKey[{k}]: time={transKey.Time}, translation=({px},{py},{pz}), inTangent=({i0},{i1},{i2}), outTangent=({o0},{o1},{o2})"); + bone.TranslationKeys.Add(transKey); + } + + bones.Add(bone); + } + + Logger.Debug(logSrc, "ReadBones: Completed reading all bones."); + return bones; + } + + private static short[] ReadMorphVertexMappings(BinaryReader r, RfaHeader hdr) + { + if (hdr.NumMorphVertices <= 0 || hdr.MorphVertexMappingsOffset == 0) + { + Logger.Debug(logSrc, "ReadMorphVertexMappings: no morph vertices or offset is zero; returning empty array."); + return Array.Empty(); + } + + Logger.Debug(logSrc, $"ReadMorphVertexMappings: Seeking to offset {hdr.MorphVertexMappingsOffset} for {hdr.NumMorphVertices} entries."); + r.BaseStream.Seek(hdr.MorphVertexMappingsOffset, SeekOrigin.Begin); + + var mappings = new short[hdr.NumMorphVertices]; + for (int i = 0; i < hdr.NumMorphVertices; i++) + { + mappings[i] = r.ReadInt16(); + Logger.Debug(logSrc, $"MorphVertexMapping[{i}] = {mappings[i]}"); + } + + Logger.Debug(logSrc, "ReadMorphVertexMappings: Completed."); + return mappings; + } + + private static List ReadMorphKeyframes(BinaryReader r, RfaHeader hdr) + { + var result = new List(); + if (hdr.Version < 8 || hdr.NumMorphKeyframes <= 0 || hdr.MorphVertexDataOffset == 0) + { + Logger.Debug(logSrc, $"ReadMorphKeyframes: version={hdr.Version} (<8) or no keyframes/offset=0; skipping morph keyframe read."); + return result; + } + + Logger.Debug(logSrc, $"ReadMorphKeyframes: Seeking to offset {hdr.MorphVertexDataOffset} for {hdr.NumMorphKeyframes} keyframes of {hdr.NumMorphVertices} vertices each."); + r.BaseStream.Seek(hdr.MorphVertexDataOffset, SeekOrigin.Begin); + + for (int k = 0; k < hdr.NumMorphKeyframes; k++) + { + var mk = new MorphKeyframe(); + mk.Time = r.ReadInt32(); + Logger.Debug(logSrc, $"MorphKeyframe[{k}]: time = {mk.Time}"); + + mk.Positions = new Vector3[hdr.NumMorphVertices]; + for (int v = 0; v < hdr.NumMorphVertices; v++) + { + sbyte sx = r.ReadSByte(); + sbyte sy = r.ReadSByte(); + sbyte sz = r.ReadSByte(); + mk.Positions[v] = new Vector3(sx, sy, sz); + Logger.Debug(logSrc, $" Vertex[{v}] delta = ({sx},{sy},{sz})"); + } + + result.Add(mk); + } + + Logger.Debug(logSrc, "ReadMorphKeyframes: Completed reading all morph keyframes."); + return result; + } + } } diff --git a/parsers/RfgParser.cs b/parsers/RfgParser.cs index 1e9ef11..309309e 100644 --- a/parsers/RfgParser.cs +++ b/parsers/RfgParser.cs @@ -7,66 +7,66 @@ namespace redux.parsers { - public static class RfgParser - { - public static Mesh ReadRfg(string filename) - { - var mesh = new Mesh(); + public static class RfgParser + { + public static Mesh ReadRfg(string filename) + { + var mesh = new Mesh(); - using (var stream = File.OpenRead(filename)) - using (var reader = new BinaryReader(stream)) - { - // Read header - uint magic = reader.ReadUInt32(); - if (magic != 0xD43DD00D) - throw new Exception("Invalid RFG magic."); + using (var stream = File.OpenRead(filename)) + using (var reader = new BinaryReader(stream)) + { + // Read header + uint magic = reader.ReadUInt32(); + if (magic != 0xD43DD00D) + throw new Exception("Invalid RFG magic."); - int version = reader.ReadInt32(); - int numGroups = reader.ReadInt32(); + int version = reader.ReadInt32(); + int numGroups = reader.ReadInt32(); - for (int g = 0; g < numGroups; g++) - { - string groupName = Utils.ReadVString(reader); - byte isMoving = reader.ReadByte(); - if (isMoving != 0) - throw new NotSupportedException("Moving groups are not currently supported."); + for (int g = 0; g < numGroups; g++) + { + string groupName = Utils.ReadVString(reader); + byte isMoving = reader.ReadByte(); + if (isMoving != 0) + throw new NotSupportedException("Moving groups are not currently supported."); - // ----- START reading brush section ----- - int numBrushes = reader.ReadInt32(); - for (int b = 0; b < numBrushes; b++) - { - Brush brush = RFGeometryParser.ReadBrush(reader, version); - mesh.Brushes.Add(brush); - } - // ----- END reading brush section ----- + // ----- START reading brush section ----- + int numBrushes = reader.ReadInt32(); + for (int b = 0; b < numBrushes; b++) + { + Brush brush = RFGeometryParser.ReadBrush(reader, version); + mesh.Brushes.Add(brush); + } + // ----- END reading brush section ----- - // ----- SKIP the rest ----- + // ----- SKIP the rest ----- - RflUtils.SkipGeoRegions(reader); - RflUtils.SkipLights(reader); - RflUtils.SkipCutsceneCameras(reader); - RflUtils.SkipCutscenePathNodes(reader); - RflUtils.SkipAmbientSounds(reader); - RflUtils.SkipEvents(reader); - RflUtils.SkipMpRespawnPoints(reader); - RflUtils.SkipNavPoints(reader); - RflUtils.SkipEntities(reader); - RflUtils.SkipItems(reader); - RflUtils.SkipClutters(reader); - RflUtils.SkipTriggers(reader); - RflUtils.SkipParticleEmitters(reader); - RflUtils.SkipGasRegions(reader); - RflUtils.SkipDecals(reader); - RflUtils.SkipClimbingRegions(reader); - RflUtils.SkipRoomEffects(reader); - RflUtils.SkipEaxEffects(reader); - RflUtils.SkipBoltEmitters(reader); - RflUtils.SkipTargets(reader); - RflUtils.SkipPushRegions(reader); - } - } + RflUtils.SkipGeoRegions(reader); + RflUtils.SkipLights(reader); + RflUtils.SkipCutsceneCameras(reader); + RflUtils.SkipCutscenePathNodes(reader); + RflUtils.SkipAmbientSounds(reader); + RflUtils.SkipEvents(reader); + RflUtils.SkipMpRespawnPoints(reader); + RflUtils.SkipNavPoints(reader); + RflUtils.SkipEntities(reader); + RflUtils.SkipItems(reader); + RflUtils.SkipClutters(reader); + RflUtils.SkipTriggers(reader); + RflUtils.SkipParticleEmitters(reader); + RflUtils.SkipGasRegions(reader); + RflUtils.SkipDecals(reader); + RflUtils.SkipClimbingRegions(reader); + RflUtils.SkipRoomEffects(reader); + RflUtils.SkipEaxEffects(reader); + RflUtils.SkipBoltEmitters(reader); + RflUtils.SkipTargets(reader); + RflUtils.SkipPushRegions(reader); + } + } - return mesh; - } - } + return mesh; + } + } } diff --git a/parsers/RflParser.cs b/parsers/RflParser.cs index cbc9602..555403d 100644 --- a/parsers/RflParser.cs +++ b/parsers/RflParser.cs @@ -15,2058 +15,2058 @@ namespace redux.parsers { - // root rfl parser - public static class RflParser - { - private const string logSrc = "RflParser"; - public static Mesh ReadRfl(string filePath) - { - var mesh = new Mesh(); - - using var stream = File.OpenRead(filePath); - using var reader = new BinaryReader(stream); - - // Validate magic number - uint magic = reader.ReadUInt32(); - if (magic != 0xD4BADA55) - throw new Exception("Invalid RFL file: wrong magic."); - - // Read header - int version = reader.ReadInt32(); - uint timestamp = reader.ReadUInt32(); - int playerStartOffset = reader.ReadInt32(); - int levelInfoOffset = reader.ReadInt32(); - int numSections = reader.ReadInt32(); - int sectionsSize = reader.ReadInt32(); - - // What version of RFL is this? - bool isAlpine = version >= 0x12C; - bool isRF1 = version <= 0xC8 || isAlpine; // <= 201 or >= 300 - bool isRF2 = version == 0x127; // 295 - - string levelName = ReadVString(reader); - string modName = ""; - if (version >= 0xB2 && !isRF2) - modName = ReadVString(reader); - - Logger.Info(logSrc, $"Parsing {levelName}..."); - if (modName.Length > 0) - Logger.Info(logSrc, $"Required mod: {modName}"); - Logger.Info(logSrc, $"RFL version: {version}"); - if (isRF2) - { - Logger.Info(logSrc, $"Red Faction 2 RFL detected, using RF2 parsing"); - if (Config.TranslateRF2Textures) - { - RF2TextureTranslator.LoadRF2TextureTranslations(); - Logger.Info(logSrc, $"Loaded {RF2TextureTranslator.TranslationCount} RF2 texture filename translation definitions"); - } - } - else if (isAlpine) - { - Logger.Info(logSrc, $"Red Faction 1 (Alpine Faction) RFL detected, using RF1 parsing"); - } - else if (isRF1) - { - Logger.Info(logSrc, $"Red Faction 1 RFL detected, using RF1 parsing"); - } - - if (Config.ParseBrushSectionInstead) - { - Logger.Info(logSrc, $"-brushes option used, parsing brush data instead of static geometry"); - } - - if (!Config.TriangulatePolygons) - { - Logger.Info(logSrc, $"-ngons option used, not forcing triangulation of parsed polygons"); - } - - // Read sections - for (int i = 0; i < numSections; i++) - { - long sectionHeaderPos = reader.BaseStream.Position; - - if (reader.BaseStream.Position + 8 > reader.BaseStream.Length) - { - Logger.Warn(logSrc, $"Reached EOF unexpectedly while reading section header {i}."); - break; - } - - int sectionType = reader.ReadInt32(); - int sectionSize = reader.ReadInt32(); - long sectionStart = reader.BaseStream.Position; - long sectionEnd = sectionStart + sectionSize; - - Logger.Info(logSrc, $"Section {i}: Type 0x{sectionType:X}, Size {sectionSize} at 0x{sectionHeaderPos:X}"); - - if (sectionEnd > reader.BaseStream.Length) - { - Logger.Warn(logSrc, $"Section {i} exceeds file length. Skipping."); - break; - } - - if (sectionType == 0x100 && !Config.ParseBrushSectionInstead) // static geometry - { - Logger.Debug(logSrc, $"Found static geometry section (0x100) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); - Brush brush = RFGeometryParser.ReadStaticGeometry(reader, version); - mesh.Brushes.Add(brush); - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - - } - else if (sectionType == 0x02000000 && Config.ParseBrushSectionInstead) // brushes - { - Logger.Debug(logSrc, $"Found brush geometry section (0x02000000) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); - RflBrushParser.ParseBrushesFromRfl(reader, sectionEnd, mesh, version); - } - else if (sectionType == 0x00000300) // lights - { - Logger.Debug(logSrc, $"Found lights section (0x00000300) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); - mesh.Lights.AddRange(RflLightParser.ParseLightsFromRfl(reader, sectionEnd, version)); - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - - } - else if (sectionType == 0x0900) // level_properties - { - Logger.Debug(logSrc, $"Found level_properties section (0x0900) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); - RflLevelPropertiesParser.ParseLevelPropertiesFromRfl(reader, sectionEnd, version); - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); // shouldn't be needed, but for safety since RF2 rfls have some unknowns - } - else if (sectionType == 0x01000000) // level_info - { - Logger.Debug(logSrc, $"Found level_info section (0x01000000) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); - RflLevelInfoParser.ParseLevelInfoFromRfl(reader, sectionEnd, version); - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); // shouldn't be needed, but for safety since RF2 rfls have some unknowns - } - else if (sectionType == 0x00000700) // mp_respawn_points - { - Logger.Debug(logSrc, $"Found mp_respawn_points section (0x700) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); - mesh.MPRespawnPoints.AddRange(RflMpRespawnPointParser.ParseMpRespawnPoints(reader, sectionEnd)); - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - } - else if (sectionType == 0x00000a00) // particle_emitters - { - Logger.Debug(logSrc, $"Found particle_emitters section (0x00000a00) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); - mesh.ParticleEmitters.AddRange(RflParticleEmitterParser.ParseParticleEmittersFromRfl(reader, sectionEnd, version)); - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - } - else if (sectionType == 0x00000600) // events - { - Logger.Warn(logSrc, $"Found events section (0x600) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); - if (RflUtils.IsRF2(version)) - { - Logger.Info(logSrc, "events section support for RF2 RFLs is a work-in-progress. Parser output may have errors."); - mesh.Events.AddRange(RflEventParser.ParseEventsRF2(reader, sectionEnd, version)); - } - else - { - mesh.Events.AddRange(RflEventParser.ParseEvents(reader, sectionEnd, version)); - } - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - } - else if (sectionType == 0x00001100) // push_regions - { - Logger.Debug(logSrc, $"Found push_regions section (0x00001100) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); - mesh.PushRegions.AddRange(RflPushRegionParser.ParsePushRegionsFromRfl(reader, sectionEnd)); - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - } - else if (sectionType == 0x00060000) // triggers - { - Logger.Debug(logSrc, $"Found triggers section (0x00060000) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); - if (isRF2) - { - Logger.Info(logSrc, "trigger section support for RF2 RFLs is a work-in-progress. Parser output may have errors."); - } - mesh.Triggers.AddRange(RflTriggerParser.ParseTriggersFromRfl(reader, sectionEnd, version)); - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - } - else if (sectionType == 0x00040000) // items - { - Logger.Debug(logSrc, $"Found items section (0x00040000) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); - mesh.Items.AddRange(RflItemParser.ParseItemsFromRfl(reader, sectionEnd, version)); - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - } - else if (sectionType == 0x00000d00) // climbing regions - { - Logger.Debug(logSrc, $"Found climbing_regions section (0x00000d00) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); - mesh.ClimbingRegions.AddRange(RflClimbingRegionParser.ParseClimbingRegionsFromRfl(reader, sectionEnd)); - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - } - else if (sectionType == 0x00010000) // waypoint lists - { - Logger.Debug(logSrc, $"Found waypoint_lists section (0x00010000) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); - mesh.WaypointLists.AddRange(RflWaypointListParser.ParseWaypointListsFromRfl(reader, sectionEnd, version)); - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - } - else if (sectionType == 0x00020000) // nav points - { - Logger.Debug(logSrc, $"Found nav_points section (0x00010000) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); - if (isRF2 && 1 == 0) // temp disabled - { - Logger.Info(logSrc, "nav_points section support for RF2 RFLs is a work-in-progress. Parser output may have errors."); - mesh.NavPoints.AddRange(RflNavPointParser.ParseNavPointsFromRfl(reader, sectionEnd, version)); - } - - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - } - else if (sectionType == 0x00001000) // decals - { - Logger.Debug(logSrc, $"Found decals section (0x1000) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); - mesh.Decals.AddRange(RflDecalParser.ParseDecalsFromRfl(reader, sectionEnd, version)); - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - } - else if (sectionType == 0x00001200) // lightmaps (RF1 only) - { - Logger.Debug(logSrc, $"Found lightmaps section (0x1200) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); - mesh.Lightmaps.AddRange(RflLightmapParser.ParseLightmapsFromRfl(reader, sectionEnd)); - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - - if (Config.DumpLightmaps) - { - Logger.Debug(logSrc, "Dumping lightmaps..."); - for (int ilm = 0; ilm < mesh.Lightmaps.Count; ilm++) - { - var lm = mesh.Lightmaps[ilm]; - string outputPath = $"lightmap_{ilm}.tga"; - TgaExporter.Write24BitTga(outputPath, lm); - } - } - } - else if (sectionType == 0x00002000) // movers - { - Logger.Debug(logSrc, $"Found movers section (0x00002000) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); - mesh.Movers.AddRange(RflMoverParser.ParseMoversFromRfl(reader, sectionEnd, version)); - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - } - else if (sectionType == 0x00003000) // moving_groups - { - Logger.Debug(logSrc, $"Found moving_groups section (0x00003000) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); - mesh.Groups.AddRange(RflGroupParser.ParseGroupsFromRfl(reader, sectionEnd, version)); - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - } - else if (sectionType == 0x00050000) // clutters - { - Logger.Debug(logSrc, $"Found clutters section (0x00050000) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); - mesh.Clutters.AddRange(RflClutterParser.ParseCluttersFromRfl(reader, sectionEnd, version)); - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - } - else if (sectionType == 0x7678) // coronas / glares chunk? - { - Logger.Info(logSrc, $"Found coronas section (0x7678) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); - mesh.Coronas.AddRange(RflCoronaParser.ParseCoronasFromRfl(reader, sectionEnd)); - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - } - - else if (sectionType == 0x0) // end section - { - // once we've reached section type 0 we can stop reading further sections - // rf2 rfls have a section 0 at the end for some reason. rf1 rfls do not - break; - } - else - { - reader.BaseStream.Seek(sectionSize, SeekOrigin.Current); // Skip unknown section - } - - //rf2: - // 0x7001 = skeletal meshes (rfc) - // 0x7002 = animations (rfa) - // 0x7003 = static meshes (rfm) - // 0x7004 = rfe - // 0x7005 = maybe texture packfiles? (peg) - // 0x400 = related to cutscenes/cameras? - // 0x500 = related to sounds? - // 0x7677 = totally unknown - // 0x7777 = something to do with events? - // 0x7680 = something to do with vfx? - // 0x30000 = entities - // 0x7681 = something to do with vfx? - // 0x7779 = something to do with spline paths? - - } - - return mesh; - } - } - - static class RflCoronaParser - { - private const string logSrc = "RflCoronaParser"; - - public static List ParseCoronasFromRfl(BinaryReader reader, long sectionEnd) - { - int count = reader.ReadInt32(); - Logger.Info(logSrc, $"Corona count = {count}"); - var list = new List(count); - - for (int i = 0; i < count; i++) - { - var c = new Corona(); - long startPos = reader.BaseStream.Position; - - c.UID = reader.ReadInt32(); - Logger.Dev(logSrc, $"Corona[{i}] UID = {c.UID}"); - - c.Name = Utils.ReadVString(reader); - Logger.Dev(logSrc, $"Corona[{i}] Name = \"{c.Name}\""); - - float px = reader.ReadSingle(), py = reader.ReadSingle(), pz = reader.ReadSingle(); - c.Position = new Vector3(px, py, pz); - Logger.Dev(logSrc, $"Corona[{i}] Pos = ({px:F3}, {py:F3}, {pz:F3})"); - - byte r = reader.ReadByte(), - g = reader.ReadByte(), - b = reader.ReadByte(), - a = reader.ReadByte(); - c.Color = new Vector4(r / 255f, g / 255f, b / 255f, a / 255f); - Logger.Dev(logSrc, $"Corona[{i}] Color = ({r},{g},{b},{a})"); - - // unknown fields - reader.ReadSingle(); - reader.ReadSingle(); - reader.ReadSingle(); - reader.ReadSingle(); - reader.ReadSingle(); - reader.ReadSingle(); - reader.ReadSingle(); - reader.ReadByte(); - reader.ReadByte(); - reader.ReadByte(); - reader.ReadByte(); - - c.ScriptName = Utils.ReadVString(reader); - Logger.Dev(logSrc, $"Corona[{i}] Script Name = \"{c.ScriptName}\""); - - // unknown fields - reader.ReadByte(); - reader.ReadByte(); - reader.ReadSingle(); - reader.ReadSingle(); - - - c.CoronaBitmap = Utils.ReadVString(reader); - Logger.Dev(logSrc, $"Corona[{i}] CoronaBitmap = \"{c.CoronaBitmap}\""); - - c.ConeAngle = reader.ReadSingle(); - Logger.Dev(logSrc, $"Corona[{i}] ConeAngle = {c.ConeAngle}"); - c.Intensity = reader.ReadSingle(); - Logger.Dev(logSrc, $"Corona[{i}] Intensity = {c.Intensity}"); - c.RadiusDistance = reader.ReadSingle(); - Logger.Dev(logSrc, $"Corona[{i}] RadiusDistance = {c.RadiusDistance}"); - c.RadiusScale = reader.ReadSingle(); - Logger.Dev(logSrc, $"Corona[{i}] RadiusScale = {c.RadiusScale}"); - c.DiminishDistance = reader.ReadSingle(); - Logger.Dev(logSrc, $"Corona[{i}] DiminishDistance = {c.DiminishDistance}"); - - c.VolumetricBitmap = Utils.ReadVString(reader); - Logger.Dev(logSrc, $"Corona[{i}] VolumetricBitmap = \"{c.VolumetricBitmap}\""); - - if (!string.IsNullOrEmpty(c.VolumetricBitmap)) - { - c.VolumetricHeight = reader.ReadSingle(); - Logger.Dev(logSrc, $"Corona[{i}] VolumetricHeight = {c.VolumetricHeight}"); - c.VolumetricLength = reader.ReadSingle(); - Logger.Dev(logSrc, $"Corona[{i}] VolumetricLength = {c.VolumetricLength}"); - - // unknown value - reader.ReadSingle(); - } - - // unknown fields - reader.ReadByte(); - reader.ReadByte(); - reader.ReadByte(); - reader.ReadByte(); - reader.ReadByte(); - - list.Add(c); - - Logger.Dev(logSrc, $"-- bytes consumed for Corona[{i}]: {reader.BaseStream.Position - startPos}\n"); - } - - return list; - } - } - static class RflClutterParser - { - private const string logSrc = "RflClutterParser"; - - public static List ParseCluttersFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) - { - int numClutters = reader.ReadInt32(); - Logger.Debug(logSrc, $"Reading {numClutters} clutter(s)…"); - - var clutters = new List(numClutters); - for (int i = 0; i < numClutters; i++) - { - var c = new Clutter(); - - c.UID = reader.ReadInt32(); - Logger.Dev(logSrc, $"Clutter[{i}] UID = {c.UID}"); - - c.ClassName = Utils.ReadVString(reader); - Logger.Dev(logSrc, $"Clutter[{i}] ClassName = \"{c.ClassName}\""); - - float px = reader.ReadSingle(), - py = reader.ReadSingle(), - pz = reader.ReadSingle(); - c.Position = new Vector3(px, py, pz); - Logger.Dev(logSrc, $"Clutter[{i}] Pos = ({px:F2},{py:F2},{pz:F2})"); - - float m00 = reader.ReadSingle(), m01 = reader.ReadSingle(), m02 = reader.ReadSingle(); - float m10 = reader.ReadSingle(), m11 = reader.ReadSingle(), m12 = reader.ReadSingle(); - float m20 = reader.ReadSingle(), m21 = reader.ReadSingle(), m22 = reader.ReadSingle(); - c.Rotation = new Matrix4x4( - m00, m01, m02, 0f, - m10, m11, m12, 0f, - m20, m21, m22, 0f, - 0f, 0f, 0f, 1f - ); - Logger.Dev(logSrc, $"Clutter[{i}] Rotation matrix read"); - - c.ScriptName = Utils.ReadVString(reader); - Logger.Dev(logSrc, $"Clutter[{i}] ScriptName = \"{c.ScriptName}\""); - - c.HiddenInEditor = reader.ReadByte() != 0; - Logger.Dev(logSrc, $"Clutter[{i}] HiddenInEditor = {c.HiddenInEditor}"); - - // RF1 has an unknown int here that isn't loaded by the game, not present in RF2 - if (RflUtils.IsRF1(rfl_version)) - { - int unk = reader.ReadInt32(); - Logger.Dev(logSrc, $"Clutter[{i}] Unknown field = {unk}"); - } - - c.Skin = Utils.ReadVString(reader); - Logger.Dev(logSrc, $"Clutter[{i}] Skin = \"{c.Skin}\""); - - int numLinks = reader.ReadInt32(); - c.Links = new List(numLinks); - for (int j = 0; j < numLinks; j++) - { - int link = reader.ReadInt32(); - c.Links.Add(link); - } - Logger.Dev(logSrc, $"Clutter[{i}] Links = [{string.Join(",", c.Links)}]"); - - clutters.Add(c); - } - - Logger.Debug(logSrc, "Finished parsing clutters"); - return clutters; - } - } - - static class RflMoverParser - { - private const string logSrc = "RflMoverParser"; - - public static List ParseMoversFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) - { - int numMovers = reader.ReadInt32(); - Logger.Debug(logSrc, $"Reading {numMovers} mover(s)..."); - Logger.Dev(logSrc, $"→ Section size: {sectionEnd - reader.BaseStream.Position} bytes"); - - var movers = new List(numMovers); - for (int i = 0; i < numMovers; i++) - { - long startPos = reader.BaseStream.Position; - var moverBrush = RFGeometryParser.ReadBrush(reader, rfl_version); - if (moverBrush != null) - { - Logger.Dev(logSrc, - $"Mover[{i}] UID={moverBrush.UID}, " + - $"Vertices={moverBrush.Vertices.Count}, " + - $"Faces={moverBrush.Solid.Faces.Count}, " + - $"BytesConsumed={reader.BaseStream.Position - startPos}" - ); - movers.Add(moverBrush); - } - else - { - Logger.Dev(logSrc, $"Mover[{i}] returned null brush, skipping"); - } - } - - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - Logger.Dev(logSrc, $"Finished parsing movers; reader at 0x{reader.BaseStream.Position:X}"); - return movers; - } - } - - static class RflGroupParser - { - private const string logSrc = "RflGroupParser"; - - public static List ParseGroupsFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) - { - int numGroups = reader.ReadInt32(); - Logger.Debug(logSrc, $"Reading {numGroups} group(s)..."); - Logger.Dev(logSrc, $"→ Section size: {sectionEnd - reader.BaseStream.Position} bytes"); - - var groups = new List(numGroups); - for (int i = 0; i < numGroups; i++) - { - long groupStart = reader.BaseStream.Position; - var grp = new Group(); - - grp.Name = Utils.ReadVString(reader); - Logger.Dev(logSrc, $"Group[{i}] Name=\"{grp.Name}\""); - - byte unknown = reader.ReadByte(); - Logger.Info(logSrc, $"Group[{i}] UnknownByte=0x{unknown:X2}"); - - if (RflUtils.IsRF2(rfl_version)) - { - //reader.ReadInt32(); - var unk1 = reader.ReadByte() != 0; - var unk2 = reader.ReadByte() != 0; - var unk3 = reader.ReadByte() != 0; - var unk4 = reader.ReadByte() != 0; - Logger.Info(logSrc, $"Group[{i}] unk1={unk1}, unk2={unk2}, unk3={unk3}, unk4={unk4}"); - } - - grp.IsMoving = reader.ReadByte() != 0; - Logger.Info(logSrc, $"Group[{i}] IsMoving={grp.IsMoving}"); - - if (grp.IsMoving) - { grp.MovingData = new MovingGroupData(); - int numKeyframes = reader.ReadInt32(); - Logger.Dev(logSrc, $"Group[{i}] KeyframesCount={numKeyframes}"); - - for (int k = 0; k < numKeyframes; k++) - { - var kf = new Keyframe - { - UID = reader.ReadInt32(), - Pos = new System.Numerics.Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), - Rot = new System.Numerics.Matrix4x4( - reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 0f, - reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 0f, - reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 0f, - 0f, 0f, 0f, 1f), - ScriptName = Utils.ReadVString(reader), - HiddenInEditor = reader.ReadByte() != 0, - PauseTime = reader.ReadSingle(), - DepartTravelTime = reader.ReadSingle(), - ReturnTravelTime = reader.ReadSingle(), - AccelTime = reader.ReadSingle(), - DecelTime = reader.ReadSingle(), - EventUID = reader.ReadInt32(), - ItemUID1 = reader.ReadInt32(), - ItemUID2 = reader.ReadInt32(), - DegreesAboutAxis = reader.ReadSingle() - }; - Logger.Dev(logSrc, - $" Keyframe[{k}] UID={kf.UID}, Pos=({kf.Pos.X:F2},{kf.Pos.Y:F2},{kf.Pos.Z:F2}), " + - $"PauseTime={kf.PauseTime:F2}" - ); - grp.MovingData.Keyframes.Add(kf); - } - - int numMemberTransforms = reader.ReadInt32(); - Logger.Dev(logSrc, $"Group[{i}] MemberTransformsCount={numMemberTransforms}"); - for (int m = 0; m < numMemberTransforms; m++) - { - var mt = new MovingGroupMemberTransform - { - UID = reader.ReadInt32(), - Pos = new System.Numerics.Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), - Rot = new System.Numerics.Matrix4x4( - reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 0f, - reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 0f, - reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 0f, - 0f, 0f, 0f, 1f) - }; - Logger.Dev(logSrc, - $" MemberTransform[{m}] UID={mt.UID}, Pos=({mt.Pos.X:F2},{mt.Pos.Y:F2},{mt.Pos.Z:F2})" - ); - grp.MovingData.MemberTransforms.Add(mt); - } - - // additional flags - grp.MovingData.IsDoor = reader.ReadByte() != 0; Logger.Dev(logSrc, $" IsDoor={grp.MovingData.IsDoor}"); - grp.MovingData.RotateInPlace = reader.ReadByte() != 0; Logger.Dev(logSrc, $" RotateInPlace={grp.MovingData.RotateInPlace}"); - grp.MovingData.StartsBackwards = reader.ReadByte() != 0; Logger.Dev(logSrc, $" StartsBackwards={grp.MovingData.StartsBackwards}"); - grp.MovingData.UseTravelTimeAsSpeed = reader.ReadByte() != 0; Logger.Dev(logSrc, $" UseTravelTimeAsSpeed={grp.MovingData.UseTravelTimeAsSpeed}"); - grp.MovingData.ForceOrient = reader.ReadByte() != 0; Logger.Dev(logSrc, $" ForceOrient={grp.MovingData.ForceOrient}"); - grp.MovingData.NoPlayerCollide = reader.ReadByte() != 0; Logger.Dev(logSrc, $" NoPlayerCollide={grp.MovingData.NoPlayerCollide}"); - - // unk RF2 specific field - if (RflUtils.IsRF2(rfl_version)) - { - reader.ReadByte(); - } - - grp.MovingData.MovementType = reader.ReadInt32(); Logger.Dev(logSrc, $" MovementType={grp.MovingData.MovementType}"); - grp.MovingData.StartingKeyframe = reader.ReadInt32(); Logger.Dev(logSrc, $" StartingKeyframe={grp.MovingData.StartingKeyframe}"); - grp.MovingData.StartSound = Utils.ReadVString(reader); Logger.Dev(logSrc, $" StartSound=\"{grp.MovingData.StartSound}\""); - grp.MovingData.StartVol = reader.ReadSingle(); Logger.Dev(logSrc, $" StartVol={grp.MovingData.StartVol}"); - grp.MovingData.LoopingSound = Utils.ReadVString(reader); Logger.Dev(logSrc, $" LoopingSound=\"{grp.MovingData.LoopingSound}\""); - grp.MovingData.LoopingVol = reader.ReadSingle(); Logger.Dev(logSrc, $" LoopingVol={grp.MovingData.LoopingVol}"); - grp.MovingData.StopSound = Utils.ReadVString(reader); Logger.Dev(logSrc, $" StopSound=\"{grp.MovingData.StopSound}\""); - grp.MovingData.StopVol = reader.ReadSingle(); Logger.Dev(logSrc, $" StopVol={grp.MovingData.StopVol}"); - grp.MovingData.CloseSound = Utils.ReadVString(reader); Logger.Dev(logSrc, $" CloseSound=\"{grp.MovingData.CloseSound}\""); - grp.MovingData.CloseVol = reader.ReadSingle(); Logger.Dev(logSrc, $" CloseVol={grp.MovingData.CloseVol}"); - - - } - - // unk RF2 specific field - if (RflUtils.IsRF2(rfl_version)) - { - var unk1 = reader.ReadInt32(); - Logger.Dev(logSrc, $"Group[{i}] unk1={unk1}"); - - } - - grp.Objects = ReadUidList(reader); - Logger.Info(logSrc, $"Group[{i}] ObjectsCount={grp.Objects.Count}"); - - grp.Brushes = ReadUidList(reader); - Logger.Info(logSrc, $"Group[{i}] BrushesCount={grp.Brushes.Count}"); - - // unk RF2 specific fields - if ( RflUtils.IsRF2(rfl_version)) - { - reader.ReadInt32(); - reader.ReadInt32(); - reader.ReadInt32(); - reader.ReadInt32(); - reader.ReadInt32(); - reader.ReadInt32(); - reader.ReadInt32(); - reader.ReadInt32(); - reader.ReadInt32(); - reader.ReadInt32(); - } - - groups.Add(grp); - Logger.Dev(logSrc, - $"Parsed Group[{i}] \"{grp.Name}\" @ bytes {reader.BaseStream.Position - groupStart}" - ); - } - - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - Logger.Dev(logSrc, $"Finished parsing groups; reader at 0x{reader.BaseStream.Position:X}"); - return groups; - } - - private static List ReadUidList(BinaryReader reader) - { - int n = reader.ReadInt32(); - Logger.Info(logSrc, $"Count: {n}"); - var list = new List(n); - for (int i = 0; i < n; i++) - { - int uid = reader.ReadInt32(); - list.Add(uid); - Logger.Info(logSrc, $"Element[{i}]: {uid}"); - } - return list; - } - } - static class RflDecalParser - { - private const string logSrc = "RflDecalParser"; - - public static List ParseDecalsFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) - { - int numDecals = reader.ReadInt32(); - Logger.Info(logSrc, $"Found {numDecals} decal(s)."); - - var result = new List(numDecals); - for (int i = 0; i < numDecals; i++) - { - var d = new Decal(); - - d.UID = reader.ReadInt32(); - - d.ClassName = Utils.ReadVString(reader); - - float px = reader.ReadSingle(), py = reader.ReadSingle(), pz = reader.ReadSingle(); - d.Position = new Vector3(px, py, pz); - - float fx = reader.ReadSingle(), fy = reader.ReadSingle(), fz = reader.ReadSingle(); - float rx = reader.ReadSingle(), ry = reader.ReadSingle(), rz = reader.ReadSingle(); - float ux = reader.ReadSingle(), uy = reader.ReadSingle(), uz = reader.ReadSingle(); - // embed mat3 into a 4×4 matrix (row‐major as usual): - d.Rotation = new Matrix4x4( - rx, ry, rz, 0f, - ux, uy, uz, 0f, - fx, fy, fz, 0f, - 0f, 0f, 0f, 1f - ); - - d.ScriptName = Utils.ReadVString(reader); - - d.HiddenInEditor = reader.ReadByte() != 0; - - float ex = reader.ReadSingle(), ey = reader.ReadSingle(), ez = reader.ReadSingle(); - d.Extents = new Vector3(ex, ey, ez); - - var tex = Utils.ReadVString(reader); - - if (RflUtils.IsRF2(rfl_version) && Config.TranslateRF2Textures) - { - string translatedTex = RF2TextureTranslator.TranslateRF2Texture(tex); - Logger.Dev(logSrc, $"Texture {i}: \"{tex}\" → \"{translatedTex}\""); - d.Texture = translatedTex; - } - else if (RflUtils.IsRF2(rfl_version) && Config.InsertRF2TexturePrefix) - { - string translatedTex = RF2TextureTranslator.InsertRxPrefix(tex); - Logger.Dev(logSrc, $"Texture {i}: \"{tex}\" → \"{translatedTex}\""); - d.Texture = translatedTex; - } - else - { - Logger.Dev(logSrc, $"Texture {i}: \"{tex}\""); - d.Texture = tex; - } - - d.Alpha = reader.ReadInt32(); - - d.SelfIlluminated = reader.ReadByte() != 0; - - d.Tiling = reader.ReadInt32(); - - d.Scale = reader.ReadSingle(); - - // Unknown fields in RF2 RFLs - uncertain if data types are correct - // One of these is "glow" (like in particle emitters), unsure which - if (RflUtils.IsRF2(rfl_version)) - { - var unk1 = reader.ReadSingle(); - var unk2 = reader.ReadInt32(); - var unk3 = reader.ReadInt32(); - Logger.Dev(logSrc, $"RF2 specific fields - Decal[{i}] unk1={unk1}, unk2={unk2}, unk3={unk3}"); - } - - Logger.Dev(logSrc, - $"Decal[{i}] UID={d.UID}, ClassName=\"{d.ClassName}\", Pos=({px},{py},{pz}), " + - $"Rot=[{fx},{fy},{fz} ; {rx},{ry},{rz} ; {ux},{uy},{uz}], " + - $"Script=\"{d.ScriptName}\", Hidden={d.HiddenInEditor}, Extents=({ex},{ey},{ez}), " + - $"Texture=\"{d.Texture}\", Alpha={d.Alpha}, SelfIlluminated={d.SelfIlluminated}, " + - $"Tiling={d.Tiling}, Scale={d.Scale}" - ); - - result.Add(d); - } - - return result; - } - } - - static class RflWaypointListParser - { - private const string logSrc = "RflWaypointListParser"; - - public static List ParseWaypointListsFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) - { - int numLists = reader.ReadInt32(); - Logger.Dev(logSrc, $"Found {numLists} waypoint_list(s)."); - - var result = new List(numLists); - for (int i = 0; i < numLists; i++) - { - var list = new WaypointList(); - - list.Name = Utils.ReadVString(reader); - Logger.Dev(logSrc, $"WaypointList[{i}].Name = \"{list.Name}\""); - - int countWp = reader.ReadInt32(); - Logger.Dev(logSrc, $"WaypointList[{i}].Count = {countWp}"); - - for (int w = 0; w < countWp; w++) - { - int idx = reader.ReadInt32(); - list.WaypointIndices.Add(idx); - Logger.Dev(logSrc, $"WaypointList[{i}].WaypointIndices[{w}] = {idx}"); - } - - result.Add(list); - } - - return result; - } - } - - static class RflNavPointParser - { - private const string logSrc = "RflNavPointParser"; - - public static List ParseNavPointsFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) - { - int numPoints = reader.ReadInt32(); - Logger.Info(logSrc, $"Found {numPoints} nav_point(s)."); - - var tempPoints = new List(numPoints); - - for (int i = 0; i < numPoints; i++) - { - var np = new NavPoint(); - - np.UID = reader.ReadInt32(); - np.HiddenInEditor = reader.ReadByte() != 0; - np.Height = reader.ReadSingle(); - - float px = reader.ReadSingle(), py = reader.ReadSingle(), pz = reader.ReadSingle(); - np.Position = new Vector3(px, py, pz); - - np.Radius = reader.ReadSingle(); - - np.Type = RflUtils.IsRF1(rfl_version) ? (NavPointType)reader.ReadInt32() : (NavPointType)reader.ReadByte(); - - // only used in rf2 parsing - byte unk1 = 0; - byte unk2 = 0; - - if (RflUtils.IsRF2(rfl_version)) - { - unk1 = reader.ReadByte(); - unk2 = reader.ReadByte(); - } - - np.Directional = reader.ReadByte() != 0; - - //reader.ReadByte(); - - // only used in rf2 parsing - float unk3 = 0.0f; - - if (RflUtils.IsRF2(rfl_version)) - { - unk3 = reader.ReadSingle(); - } - - if (np.Directional) - { - float fx = reader.ReadSingle(), fy = reader.ReadSingle(), fz = reader.ReadSingle(); - float rx = reader.ReadSingle(), ry = reader.ReadSingle(), rz = reader.ReadSingle(); - float ux = reader.ReadSingle(), uy = reader.ReadSingle(), uz = reader.ReadSingle(); - - np.Rotation = new Matrix4x4( - rx, ry, rz, 0f, - ux, uy, uz, 0f, - fx, fy, fz, 0f, - 0f, 0f, 0f, 1f - ); - } - - // only used in rf2 parsing - int unk4 = 0; - - if (RflUtils.IsRF1(rfl_version)) - { - np.Cover = reader.ReadByte() != 0; - np.Hide = reader.ReadByte() != 0; - np.Crunch = reader.ReadByte() != 0; - } - else - { - unk4 = reader.ReadInt32(); - } - - np.PauseTime = reader.ReadSingle(); - - // only used in rf2 parsing - float unk5 = 0.0f; - - if (RflUtils.IsRF2(rfl_version)) - { - // not convinced this is actually pause time in rf2 - if (np.PauseTime > 0) - { - unk5 = reader.ReadSingle(); - } - } - else - { - int numLinks = reader.ReadInt32(); - for (int L = 0; L < numLinks; L++) - { - int linkIdx = reader.ReadInt32(); - np.LinkIndices.Add(linkIdx); - } - } - - Logger.Dev(logSrc, - $"NavPoint[{i}] UID={np.UID}, HiddenInEditor={np.HiddenInEditor}, Height={np.Height}, " + - $"Position=({px},{py},{pz}), Radius={np.Radius}, Type={np.Type}, Directional={np.Directional}, " + - $"Cover={np.Cover}, Hide={np.Hide}, Crunch={np.Crunch}, PauseTime={np.PauseTime}, " + - $"InitialLinks=[{string.Join(",", np.LinkIndices)}]" + - $" unk1={unk1}, unk2={unk2}, unk3={unk3}, unk4={unk4}, unk5={unk5}" - ); - - tempPoints.Add(np); - } - - // not exported in rfg anyway - /* - // Read numPoints nav_point_connections arrays - for (int i = 0; i < numPoints; i++) - { - byte numIdx = reader.ReadByte(); - var connections = new List(numIdx); - for (int c = 0; c < numIdx; c++) - { - int idx = reader.ReadInt32(); - connections.Add(idx); - } - - tempPoints[i].LinkIndices.AddRange(connections); - Logger.Dev(logSrc, - $"NavPointConnections[{i}].Count = {numIdx}; Connections=[{string.Join(",", connections)}]" - ); - } - */ - return tempPoints; - } - } - - public static class RflLightmapParser - { - private const string logSrc = "RflLightmapParser"; - - public static List ParseLightmapsFromRfl(BinaryReader reader, long sectionEnd) - { - var lightmaps = new List(); - - int numLightmaps = reader.ReadInt32(); - Logger.Dev(logSrc, $"Found {numLightmaps} lightmap(s)."); - - for (int i = 0; i < numLightmaps; i++) - { - var lm = new Lightmap(); - - // Read width and height - lm.Width = reader.ReadInt32(); - lm.Height = reader.ReadInt32(); - Logger.Dev(logSrc, $"Lightmap[{i}] dimensions: {lm.Width}×{lm.Height}"); - - // Compute total byte count (24 bpp → 3 bytes per pixel) - int pixelCount = lm.Width * lm.Height; - int byteCount = pixelCount * 3; - - // Sanity check - long bytesRemainingInSection = sectionEnd - reader.BaseStream.Position; - if (bytesRemainingInSection < byteCount) - { - Logger.Warn(logSrc, $"Section ended prematurely while reading lightmap[{i}]: " + - $"needed {byteCount} bytes but only {bytesRemainingInSection} remain. Clamping to available."); - byteCount = (int)bytesRemainingInSection; - } - - // Read raw RGB bytes - lm.PixelData = reader.ReadBytes(byteCount); - - // Store and log - Logger.Dev(logSrc, $"Read {lm.PixelData.Length} bytes of lightmap[{i}]."); - lightmaps.Add(lm); - } - - return lightmaps; - } - } - - public static class RflParticleEmitterParser - { - private const string logSrc = "RflParticleEmitterParser"; - - public static List ParseParticleEmittersFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) - { - var emitters = new List(); - - int count = reader.ReadInt32(); - Logger.Warn(logSrc, $"Particle emitters count: {count}"); - for (int i = 0; i < count; i++) - { - var e = new ParticleEmitter(); - - e.UID = reader.ReadInt32(); - Logger.Warn(logSrc, $"Emitter[{i}].UID = {e.UID}"); - - e.ClassName = ReadVString(reader); - Logger.Warn(logSrc, $"Emitter[{i}].ClassName = \"{e.ClassName}\""); - - // Position (vec3) - e.Position = new Vector3( - reader.ReadSingle(), - reader.ReadSingle(), - reader.ReadSingle() - ); - Logger.Warn(logSrc, $"Emitter[{i}].Position = {e.Position}"); - - // Rotation (mat3) - float m00 = reader.ReadSingle(), m01 = reader.ReadSingle(), m02 = reader.ReadSingle(); - float m10 = reader.ReadSingle(), m11 = reader.ReadSingle(), m12 = reader.ReadSingle(); - float m20 = reader.ReadSingle(), m21 = reader.ReadSingle(), m22 = reader.ReadSingle(); - e.Rotation = new Matrix4x4( - m00, m01, m02, 0f, - m10, m11, m12, 0f, - m20, m21, m22, 0f, - 0f, 0f, 0f, 1f - ); - Logger.Warn(logSrc, $"Emitter[{i}].Rotation = [ [{m00}, {m01}, {m02}], [{m10}, {m11}, {m12}], [{m20}, {m21}, {m22}] ]"); - - e.ScriptName = ReadVString(reader); - Logger.Warn(logSrc, $"Emitter[{i}].ScriptName = \"{e.ScriptName}\""); - - e.HiddenInEditor = reader.ReadByte() != 0; - Logger.Warn(logSrc, $"Emitter[{i}].HiddenInEditor = {e.HiddenInEditor}"); - - e.Shape = (ParticleEmitterShape)reader.ReadInt32(); - Logger.Warn(logSrc, $"Emitter[{i}].Shape = {e.Shape}"); - - e.SphereRadius = reader.ReadSingle(); - Logger.Warn(logSrc, $"Emitter[{i}].SphereRadius = {e.SphereRadius}"); - e.PlaneWidth = reader.ReadSingle(); - Logger.Warn(logSrc, $"Emitter[{i}].PlaneWidth = {e.PlaneWidth}"); - e.PlaneDepth = reader.ReadSingle(); - Logger.Warn(logSrc, $"Emitter[{i}].PlaneDepth = {e.PlaneDepth}"); - - e.Texture = ReadVString(reader); - Logger.Warn(logSrc, $"Emitter[{i}].Texture = \"{e.Texture}\""); - - e.SpawnDelay = reader.ReadSingle(); - Logger.Warn(logSrc, $"Emitter[{i}].SpawnDelay = {e.SpawnDelay}"); - e.SpawnRandomize = reader.ReadSingle(); - Logger.Warn(logSrc, $"Emitter[{i}].SpawnRandomize = {e.SpawnRandomize}"); - - e.Velocity = reader.ReadSingle(); - Logger.Warn(logSrc, $"Emitter[{i}].Velocity = {e.Velocity}"); - e.VelocityRandomize = reader.ReadSingle(); - Logger.Warn(logSrc, $"Emitter[{i}].VelocityRandomize = {e.VelocityRandomize}"); - - e.Acceleration = reader.ReadSingle(); - Logger.Warn(logSrc, $"Emitter[{i}].Acceleration = {e.Acceleration}"); - - e.Decay = reader.ReadSingle(); - Logger.Warn(logSrc, $"Emitter[{i}].Decay = {e.Decay}"); - e.DecayRandomize = reader.ReadSingle(); - Logger.Warn(logSrc, $"Emitter[{i}].DecayRandomize = {e.DecayRandomize}"); - - e.ParticleRadius = reader.ReadSingle(); - Logger.Warn(logSrc, $"Emitter[{i}].ParticleRadius = {e.ParticleRadius}"); - e.ParticleRadiusRandomize = reader.ReadSingle(); - Logger.Warn(logSrc, $"Emitter[{i}].ParticleRadiusRandomize = {e.ParticleRadiusRandomize}"); - - e.GrowthRate = reader.ReadSingle(); - Logger.Warn(logSrc, $"Emitter[{i}].GrowthRate = {e.GrowthRate}"); - e.GravityMultiplier = reader.ReadSingle(); - Logger.Warn(logSrc, $"Emitter[{i}].GravityMultiplier = {e.GravityMultiplier}"); - e.RandomDirection = reader.ReadSingle(); - Logger.Warn(logSrc, $"Emitter[{i}].RandomDirection = {e.RandomDirection}"); - - // Colors (RGBA) - e.ParticleColor = new Vector4( - reader.ReadByte() / 255f, - reader.ReadByte() / 255f, - reader.ReadByte() / 255f, - reader.ReadByte() / 255f - ); - Logger.Warn(logSrc, $"Emitter[{i}].ParticleColor = {e.ParticleColor}"); - e.FadeToColor = new Vector4( - reader.ReadByte() / 255f, - reader.ReadByte() / 255f, - reader.ReadByte() / 255f, - reader.ReadByte() / 255f - ); - Logger.Warn(logSrc, $"Emitter[{i}].FadeToColor = {e.FadeToColor}"); - - // Flags - e.EmitterFlags = reader.ReadUInt32(); - Logger.Warn(logSrc, $"Emitter[{i}].EmitterFlags = 0x{e.EmitterFlags:X8}"); - e.ParticleFlags = reader.ReadUInt16(); - Logger.Warn(logSrc, $"Emitter[{i}].ParticleFlags = 0x{e.ParticleFlags:X4}"); - - // Four 4-bit values packed into a UInt16 - ushort nibblePack = reader.ReadUInt16(); - e.Stickiness = (byte)(nibblePack >> 12 & 0xF); - e.Bounciness = (byte)(nibblePack >> 8 & 0xF); - e.PushEffect = (byte)(nibblePack >> 4 & 0xF); - e.Swirliness = (byte)(nibblePack & 0xF); - Logger.Warn(logSrc, $"Emitter[{i}].Stickiness={e.Stickiness}, Bounciness={e.Bounciness}, PushEffect={e.PushEffect}, Swirliness={e.Swirliness}"); - - e.InitiallyOn = reader.ReadByte() != 0; - Logger.Warn(logSrc, $"Emitter[{i}].InitiallyOn = {e.InitiallyOn}"); - e.TimeOn = reader.ReadSingle(); - Logger.Warn(logSrc, $"Emitter[{i}].TimeOn = {e.TimeOn}"); - e.TimeOnRandomize = reader.ReadSingle(); - Logger.Warn(logSrc, $"Emitter[{i}].TimeOnRandomize = {e.TimeOnRandomize}"); - - e.TimeOff = reader.ReadSingle(); - Logger.Warn(logSrc, $"Emitter[{i}].TimeOff = {e.TimeOff}"); - e.TimeOffRandomize = reader.ReadSingle(); - Logger.Warn(logSrc, $"Emitter[{i}].TimeOffRandomize = {e.TimeOffRandomize}"); - - e.ActiveDistance = reader.ReadSingle(); - Logger.Warn(logSrc, $"Emitter[{i}].ActiveDistance = {e.ActiveDistance}"); - - emitters.Add(e); - } - - // Skip any padding or unknown data - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - return emitters; - } - } - - public static class RflLevelInfoParser - { - private const string logSrc = "RflLevelInfoParser"; - - public static void ParseLevelInfoFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) - { - int unknown = reader.ReadInt32(); - Logger.Warn(logSrc, $"Unk value: {unknown}"); - - string levelName = ReadVString(reader); - Logger.Warn(logSrc, $"Level name: \"{levelName}\""); - - string author = ReadVString(reader); - Logger.Warn(logSrc, $"Level author: \"{author}\""); - - string date = ReadVString(reader); - Logger.Warn(logSrc, $"Save date/time: \"{date}\""); - - byte hasMovers = reader.ReadByte(); - Logger.Warn(logSrc, $"Has movers: {hasMovers}"); - - byte multiplayer = reader.ReadByte(); - Logger.Warn(logSrc, $"Multiplayer level: {multiplayer}"); - - // Editor view layouts - for (int i = 0; i < 4; i++) - { - long cfgStart = reader.BaseStream.Position; - int viewType = reader.ReadInt32(); - Logger.Debug(logSrc, $"editor_view_config[{i}].view_type = {viewType}"); - - if (viewType == (int)EditorViewType.FreeLook) - { - // pos_3d (vec3) - float x = reader.ReadSingle(); - float y = reader.ReadSingle(); - float z = reader.ReadSingle(); - Logger.Debug(logSrc, $"editor_view_config[{i}].pos_3d = ({x}, {y}, {z})"); - } - else - { - // pos_2d (4 × f4) - float[] pos2d = new float[4]; - for (int j = 0; j < 4; j++) - pos2d[j] = reader.ReadSingle(); - Logger.Debug(logSrc, $"editor_view_config[{i}].pos_2d = [{string.Join(", ", pos2d)}]"); - } - - // rot (mat3) - float[,] m = new float[3, 3]; - for (int row = 0; row < 3; row++) - for (int col = 0; col < 3; col++) - m[row, col] = reader.ReadSingle(); - Logger.Debug(logSrc, - $"editor_view_config[{i}].rot = [\n" + - $" [{m[0, 0]}, {m[0, 1]}, {m[0, 2]}],\n" + - $" [{m[1, 0]}, {m[1, 1]}, {m[1, 2]}],\n" + - $" [{m[2, 0]}, {m[2, 1]}, {m[2, 2]}]\n" + - $"]"); - - long cfgSize = reader.BaseStream.Position - cfgStart; - Logger.Debug(logSrc, $"editor_view_config[{i}] size = {cfgSize} bytes"); - } - - // prints garbage data in this section at the end of RF2 sections, but not RF1 - /*int extraIndex = 0; - while (reader.BaseStream.Position < sectionEnd) - { - var b = reader.ReadByte(); - Logger.Warn(logSrc, $"level_info extra byte[{extraIndex}] = 0x{b:X2}"); - extraIndex++; - }*/ - } - } - - public static class RflClimbingRegionParser - { - private const string logSrc = "RflClimbingRegionParser"; - - public static List ParseClimbingRegionsFromRfl(BinaryReader reader, long sectionEnd) - { - var regions = new List(); - - int count = reader.ReadInt32(); - Logger.Dev(logSrc, $"Reading {count} climbing regions…"); - - for (int i = 0; i < count; i++) - { - var cr = new ClimbingRegion(); - - cr.UID = reader.ReadInt32(); - cr.ClassName = ReadVString(reader); - cr.Position = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - - // mat3 comes in forward, right, up order → embed into Matrix4x4 - var f = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - var r = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - var u = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - cr.Rotation = new Matrix4x4( - r.X, r.Y, r.Z, 0f, - u.X, u.Y, u.Z, 0f, - f.X, f.Y, f.Z, 0f, - 0f, 0f, 0f, 1f); - - cr.ScriptName = ReadVString(reader); - cr.HiddenInEditor = reader.ReadByte() != 0; - cr.Type = reader.ReadInt32(); - cr.Extents = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - - Logger.Dev(logSrc, - $"ClimbingRegion[{i}] UID={cr.UID}, Class=\"{cr.ClassName}\", Pos={cr.Position}, " + - $"Rot=\n{cr.Rotation}, Script=\"{cr.ScriptName}\", Hidden={cr.HiddenInEditor}, " + - $"Type={cr.Type}, Extents={cr.Extents}"); - - regions.Add(cr); - } - - // ensure we position at the end no matter what - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - return regions; - } - } - - public static class RflItemParser - { - private const string logSrc = "RflItemParser"; - - public static List ParseItemsFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) - { - var items = new List(); - int count = reader.ReadInt32(); - Logger.Dev(logSrc, $"Reading {count} items…"); - - for (int i = 0; i < count; i++) - { - var it = new RflItem(); - - it.UID = reader.ReadInt32(); - Logger.Dev(logSrc, $"Item[{i}] UID = {it.UID}"); - - it.ClassName = ReadVString(reader); - Logger.Dev(logSrc, $"Item[{i}] ClassName = \"{it.ClassName}\""); - - it.Position = new Vector3( - reader.ReadSingle(), - reader.ReadSingle(), - reader.ReadSingle() - ); - Logger.Dev(logSrc, $"Item[{i}] Position = {it.Position}"); - - // 3×3 rotation matrix → Matrix4x4 - var m00 = reader.ReadSingle(); var m01 = reader.ReadSingle(); var m02 = reader.ReadSingle(); - var m10 = reader.ReadSingle(); var m11 = reader.ReadSingle(); var m12 = reader.ReadSingle(); - var m20 = reader.ReadSingle(); var m21 = reader.ReadSingle(); var m22 = reader.ReadSingle(); - it.Rotation = new Matrix4x4( - m00, m01, m02, 0, - m10, m11, m12, 0, - m20, m21, m22, 0, - 0, 0, 0, 1 - ); - Logger.Dev(logSrc, $"Item[{i}] Rotation = {it.Rotation}"); - - it.ScriptName = ReadVString(reader); - Logger.Dev(logSrc, $"Item[{i}] ScriptName = \"{it.ScriptName}\""); - - it.HiddenInEditor = reader.ReadByte() != 0; - Logger.Dev(logSrc, $"Item[{i}] HiddenInEditor = {it.HiddenInEditor}"); - - it.Count = reader.ReadInt32(); - Logger.Dev(logSrc, $"Item[{i}] Count = {it.Count}"); - - it.RespawnTime = reader.ReadInt32(); - Logger.Dev(logSrc, $"Item[{i}] RespawnTime = {it.RespawnTime}"); - - it.TeamID = reader.ReadInt32(); - Logger.Dev(logSrc, $"Item[{i}] TeamID = {it.TeamID}"); - - // unknown bytes, found only in RF2 rfls - // 255, 255, 255, 255, 0, 0 in every instance I have found - if (rfl_version == 0x127) - { - var unk1 = reader.ReadByte(); - var unk2 = reader.ReadByte(); - var unk3 = reader.ReadByte(); - var unk4 = reader.ReadByte(); - var unk5 = reader.ReadByte(); - var unk6 = reader.ReadByte(); - Logger.Dev(logSrc, $"unk RF2-specific bytes {unk1}, {unk2}, {unk3}, {unk4}, {unk5}, {unk6}"); - } - - items.Add(it); - } - - return items; - } - } - public static class RflTriggerParser - { - private const string logSrc = "RflTriggerParser"; - - public static List ParseTriggersFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) - { - var list = new List(); - int count = reader.ReadInt32(); - Logger.Dev(logSrc, $"Reading {count} triggers…"); - - for (int i = 0; i < count; i++) - { - var t = new Trigger(); - - t.UID = reader.ReadInt32(); - Logger.Dev(logSrc, $"Trigger[{i}] UID = {t.UID}"); - - t.ScriptName = ReadVString(reader); - Logger.Dev(logSrc, $"Trigger[{i}] ScriptName = \"{t.ScriptName}\""); - - t.HiddenInEditor = reader.ReadByte() != 0; - Logger.Dev(logSrc, $"Trigger[{i}] HiddenInEditor = {t.HiddenInEditor}"); - - t.Shape = (TriggerShape)reader.ReadInt32(); - Logger.Dev(logSrc, $"Trigger[{i}] Shape = {t.Shape}"); - - t.ResetsAfter = reader.ReadSingle(); - Logger.Dev(logSrc, $"Trigger[{i}] ResetsAfter = {t.ResetsAfter:F2}"); - - t.ResetsTimes = reader.ReadInt32(); - Logger.Dev(logSrc, $"Trigger[{i}] ResetsTimes = {t.ResetsTimes}"); - - t.UseKeyRequired = reader.ReadByte() != 0; - Logger.Dev(logSrc, $"Trigger[{i}] UseKeyRequired = {t.UseKeyRequired}"); - - if (RflUtils.IsRF1(rfl_version)) - { - t.KeyName = ReadVString(reader); - } - else - { - t.KeyName = ReadVStringPlain(reader); // rf2 uses a plain vstring for key name - } - Logger.Dev(logSrc, $"Trigger[{i}] KeyName = \"{t.KeyName}\""); - - if (RflUtils.IsRF1(rfl_version)) - { - t.WeaponActivates = reader.ReadByte() != 0; - Logger.Dev(logSrc, $"Trigger[{i}] WeaponActivates = {t.WeaponActivates}"); - } - - t.ActivatedBy = (TriggerActivatedBy)reader.ReadByte(); - Logger.Dev(logSrc, $"Trigger[{i}] ActivatedBy = {t.ActivatedBy}"); - - if (RflUtils.IsRF1(rfl_version)) - { - t.IsNpc = reader.ReadByte() != 0; - Logger.Dev(logSrc, $"Trigger[{i}] IsNpc = {t.IsNpc}"); - - t.IsAuto = reader.ReadByte() != 0; - Logger.Dev(logSrc, $"Trigger[{i}] IsAuto = {t.IsAuto}"); - - t.InVehicle = reader.ReadByte() != 0; - Logger.Dev(logSrc, $"Trigger[{i}] InVehicle = {t.InVehicle}"); - } - - // position - float px = reader.ReadSingle(); - float py = reader.ReadSingle(); - float pz = reader.ReadSingle(); - t.Position = new Vector3(px, py, pz); - Logger.Dev(logSrc, $"Trigger[{i}] Position = ({px:F2}, {py:F2}, {pz:F2})"); - - if (t.Shape == TriggerShape.Sphere) - { - t.SphereRadius = reader.ReadSingle(); - Logger.Dev(logSrc, $"Trigger[{i}] SphereRadius = {t.SphereRadius:F2}"); - } - else - { - // rotation - float m00 = reader.ReadSingle(), m01 = reader.ReadSingle(), m02 = reader.ReadSingle(); - float m10 = reader.ReadSingle(), m11 = reader.ReadSingle(), m12 = reader.ReadSingle(); - float m20 = reader.ReadSingle(), m21 = reader.ReadSingle(), m22 = reader.ReadSingle(); - t.Rotation = new Matrix4x4( - m00, m01, m02, 0, - m10, m11, m12, 0, - m20, m21, m22, 0, - 0, 0, 0, 1 - ); - Logger.Dev(logSrc, $"Trigger[{i}] Rotation = [ [{m00:F2},{m01:F2},{m02:F2}], [{m10:F2},{m11:F2},{m12:F2}], [{m20:F2},{m21:F2},{m22:F2}] ]"); - - t.BoxHeight = reader.ReadSingle(); - t.BoxWidth = reader.ReadSingle(); - t.BoxDepth = reader.ReadSingle(); - Logger.Dev(logSrc, $"Trigger[{i}] BoxExtents = H:{t.BoxHeight:F2}, W:{t.BoxWidth:F2}, D:{t.BoxDepth:F2}"); - - if (RflUtils.IsRF1(rfl_version)) - { - t.OneWay = reader.ReadByte() != 0; - Logger.Dev(logSrc, $"Trigger[{i}] OneWay = {t.OneWay}"); - } - } - - t.AirlockRoomUID = reader.ReadInt32(); - Logger.Dev(logSrc, $"Trigger[{i}] AirlockRoomUID = {t.AirlockRoomUID}"); - - t.AttachedToUID = reader.ReadInt32(); - Logger.Dev(logSrc, $"Trigger[{i}] AttachedToUID = {t.AttachedToUID}"); - - t.UseClutterUID = reader.ReadInt32(); - Logger.Dev(logSrc, $"Trigger[{i}] UseClutterUID = {t.UseClutterUID}"); - - t.Disabled = reader.ReadByte() != 0; - Logger.Dev(logSrc, $"Trigger[{i}] Disabled = {t.Disabled}"); - - t.ButtonActiveTime = reader.ReadSingle(); - Logger.Dev(logSrc, $"Trigger[{i}] ButtonActiveTime = {t.ButtonActiveTime:F2}"); - - if (RflUtils.IsRF1(rfl_version)) - { - t.InsideTime = reader.ReadSingle(); - Logger.Dev(logSrc, $"Trigger[{i}] InsideTime = {t.InsideTime:F2}"); - } - else - { - reader.ReadSingle(); - // This value is super high in RF2, so isn't InsideTime. May not even be a float. Reading it is needed for alignment - } - - // RF2 omits a number of fields that are in RF1 triggers and appears to consolidate them into flags and flags2 - // Many of these are unknown. Some are likely flags present in RF1 and some new in RF2 - if (RflUtils.IsRF2(rfl_version)) - { - reader.ReadByte(); - ushort flags = reader.ReadUInt16(); - Logger.Dev(logSrc, $"Trigger[{i}] with Script Name {t.ScriptName} Flags = 0x{flags:X4}"); - } - - if (rfl_version >= 0xB1 && !(RflUtils.IsRF2(rfl_version))) - { - t.Team = (TriggerTeam)reader.ReadInt32(); - Logger.Dev(logSrc, $"Trigger[{i}] Team = {t.Team}"); - } - else if (RflUtils.IsRF2(rfl_version)) - { - uint flags2 = reader.ReadUInt32(); - //t.StartActive = (flags & 0x1) != 0; - //t.OneShot = (flags & 0x2) != 0; - Logger.Dev(logSrc, $"Trigger[{i}] with Script Name {t.ScriptName} Flags2 = 0x{flags2:X8}"); - - // bit 0x0008 = IsAuto in RF2 - t.IsAuto = (flags2 & 0x0008) != 0; - Logger.Dev(logSrc, $"Trigger[{i}] with Script Name {t.ScriptName} IsAuto = {t.IsAuto}"); - - // unknown flags in RF2 triggers - var F2unk1 = (flags2 & 0x0001) != 0; - var F2unk2 = (flags2 & 0x0002) != 0; - var F2unk3 = (flags2 & 0x0010) != 0; - var F2unk4 = (flags2 & 0x0020) != 0; - var F2unk5 = (flags2 & 0x0040) != 0; - var F2unk6 = (flags2 & 0x0080) != 0; - var F2unk7 = (flags2 & 0x0100) != 0; - - Logger.Dev(logSrc, $"Trigger[{i}] with Script Name {t.ScriptName} unk1 {F2unk1}, unk2 {F2unk2}, unk3 {F2unk3}, unk4 {F2unk4}, unk5 {F2unk5}, unk6 {F2unk6}, unk7 {F2unk7}"); - } - - int numLinks = reader.ReadInt32(); - Logger.Dev(logSrc, $"Trigger[{i}] LinksCount = {numLinks}"); - t.Links = new List(numLinks); - for (int j = 0; j < numLinks; j++) - { - int link = reader.ReadInt32(); - t.Links.Add(link); - Logger.Dev(logSrc, $"Trigger[{i}] Link[{j}] = {link}"); - } - - list.Add(t); - } - - return list; - } - } - - public static class RflPushRegionParser - { - private const string logSrc = "RflPushRegionParser"; - - public static List ParsePushRegionsFromRfl(BinaryReader reader, long sectionEnd) - { - var regions = new List(); - int count = reader.ReadInt32(); - Logger.Dev(logSrc, $"Reading {count} push regions…"); - - for (int i = 0; i < count; i++) - { - var pr = new PushRegion(); - - pr.UID = reader.ReadInt32(); - pr.ClassName = ReadVString(reader); - pr.Position = new Vector3( - reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle() - ); - - // rotation matrix - float r00 = reader.ReadSingle(), r01 = reader.ReadSingle(), r02 = reader.ReadSingle(); - float r10 = reader.ReadSingle(), r11 = reader.ReadSingle(), r12 = reader.ReadSingle(); - float r20 = reader.ReadSingle(), r21 = reader.ReadSingle(), r22 = reader.ReadSingle(); - pr.Rotation = new Matrix4x4( - r00, r01, r02, 0, - r10, r11, r12, 0, - r20, r21, r22, 0, - 0, 0, 0, 1 - ); - - pr.ScriptName = ReadVString(reader); - pr.HiddenInEditor = reader.ReadByte() != 0; - - pr.Shape = (PushRegionShape)reader.ReadInt32(); - if (pr.Shape == PushRegionShape.Sphere) - { - pr.Radius = reader.ReadSingle(); - } - else - { - // extents = Vector3 - pr.Extents = new Vector3( - reader.ReadSingle(), - reader.ReadSingle(), - reader.ReadSingle() - ); - } - - pr.Strength = reader.ReadSingle(); - - // flags bitfield - uint rawFlags = reader.ReadUInt16(); // 16 bits - pr.JumpPad = (rawFlags & 0x40) != 0; - pr.DoesntAffectPlayer = (rawFlags & 0x20) != 0; - pr.Radial = (rawFlags & 0x10) != 0; - pr.GrowsTowardsBoundary = (rawFlags & 0x08) != 0; - pr.GrowsTowardsCenter = (rawFlags & 0x04) != 0; - pr.Grounded = (rawFlags & 0x02) != 0; - pr.MassIndependent = (rawFlags & 0x01) != 0; - - pr.Turbulence = reader.ReadUInt16(); - - // log out everything - Logger.Debug(logSrc, - $"PushRegion[{i}] UID={pr.UID}, Class=\"{pr.ClassName}\", Pos={pr.Position}, " + - $"Script=\"{pr.ScriptName}\", Hidden={pr.HiddenInEditor}, Shape={pr.Shape}, " + - (pr.Shape == PushRegionShape.Sphere - ? $"Radius={pr.Radius}" - : $"Extents={pr.Extents}" - ) + - $", Strength={pr.Strength}, Flags=0x{rawFlags:X4} (JumpPad={pr.JumpPad}, " + - $"NoPlayer={pr.DoesntAffectPlayer}, Radial={pr.Radial}, " + - $"BoundaryGrow={pr.GrowsTowardsBoundary}, CenterGrow={pr.GrowsTowardsCenter}, " + - $"Grounded={pr.Grounded}, MassIndep={pr.MassIndependent}), Turbulence={pr.Turbulence}" - ); - - regions.Add(pr); - } - - return regions; - } - } - - public static class RflEventParser - { - private const string logSrc = "RflEventParser"; - - public static List ParseEventsRF2(BinaryReader reader, long sectionEnd, int rfl_version) - { - int count = reader.ReadInt32(); - Logger.Warn(logSrc, $"RF2 event count = {count}"); - - var events = new List(count); - - for (int i = 0; i < count; i++) - { - long startPos = reader.BaseStream.Position; - var ev = new RflEvent(); - - // UID - ev.UID = reader.ReadInt32(); - Logger.Warn(logSrc, $"Event[{i}] UID = {ev.UID}"); - - // class name - ev.ClassName = ReadVString(reader); - Logger.Warn(logSrc, $"Event[{i}] ClassName = \"{ev.ClassName}\""); - - // position - float px = reader.ReadSingle(); - float py = reader.ReadSingle(); - float pz = reader.ReadSingle(); - ev.Position = new Vector3(px, py, pz); - Logger.Warn(logSrc, $"Event[{i}] Position = ({px:F2}, {py:F2}, {pz:F2})"); - - // script name - ev.ScriptName = ReadVString(reader); - Logger.Warn(logSrc, $"Event[{i}] ScriptName = \"{ev.ScriptName}\""); - - // hidden in editor - ev.HiddenInEditor = reader.ReadByte() != 0; - Logger.Warn(logSrc, $"Event[{i}] HiddenInEditor = {ev.HiddenInEditor}"); - - // delay - ev.Delay = reader.ReadSingle(); - Logger.Warn(logSrc, $"Event[{i}] Delay = {ev.Delay}"); - - // 8 4-byte fields: - var unk0 = reader.ReadInt32(); // -1, 0, or 1 everywhere that I've seen, unsure of significance - var unk1 = reader.ReadInt32(); // typically 0, unsure of significance - var mb1 = reader.ReadByte() != 0; // top candidate for bool1 - var mb2 = reader.ReadByte() != 0; // top candidate for bool2 - var unk2 = reader.ReadInt32(); // confident this is int1 - var unk3 = reader.ReadInt32(); // confident this is int2 - var unk4 = reader.ReadInt32(); // int maybe? was -1 on a Delay - var unk5 = reader.ReadInt32(); // int maybe? was -1 on a Delay - var unk6 = reader.ReadSingle(); // confident this is float1 - var unk7 = reader.ReadSingle(); // top candidate for float2 - var unk8 = reader.ReadInt32(); // 00 00 00 00 everywhere that I've seen - - // str1 - ev.Str1 = ReadVString(reader); // confident of this - Logger.Warn(logSrc, $"Event[{i}] Str1 = \"{ev.Str1}\""); - - // str2 - ev.Str2 = ReadVString(reader); // confident of this - Logger.Warn(logSrc, $"Event[{i}] Str2 = \"{ev.Str2}\""); - - //reader.ReadByte(); - //reader.ReadByte(); - var unk9 = ReadVString(reader); // maybe a third str? 00 00 everywhere that I've seen - - ev.Bool1 = mb1; - ev.Bool2 = mb2; - ev.Int1 = unk2; - ev.Int2 = unk3; - ev.Float1 = unk6; - ev.Float2 = unk7; - - Logger.Warn(logSrc, $"Event[{i}] Bool1 = {ev.Bool1}"); - Logger.Warn(logSrc, $"Event[{i}] Bool2 = {ev.Bool2}"); - Logger.Warn(logSrc, $"Event[{i}] Int1 = {ev.Int1}"); - Logger.Warn(logSrc, $"Event[{i}] Int2 = {ev.Int2}"); - Logger.Warn(logSrc, $"Event[{i}] Float1 = {ev.Float1}"); - Logger.Warn(logSrc, $"Event[{i}] Float2 = {ev.Float2}"); - - Logger.Warn(logSrc, $"Event[{i}] unk0 {unk0}, unk1 {unk1}, mb1 {mb1}, mb2 {mb2}, unk2 {unk2}, unk3 {unk3}, unk4 {unk4}, unk5 {unk5}, unk6 {unk6}, unk7 {unk7}, unk8 {unk8}, unk9 {unk9}"); - - // Links - int numLinks = reader.ReadInt32(); - Logger.Warn(logSrc, $"Event[{i}] Links count = {numLinks}"); - - ev.Links = new List(numLinks); - for (int j = 0; j < numLinks; j++) - { - int link = reader.ReadInt32(); - ev.Links.Add(link); - Logger.Warn(logSrc, $"Event[{i}] Link[{j}] = {link}"); - } - - if (ev.ClassName == "Play_Explosion" || ev.ClassName == "Teleport") - { - ev.HasRotation = true; // used by exporter - float m00 = reader.ReadSingle(), m01 = reader.ReadSingle(), m02 = reader.ReadSingle(); - float m10 = reader.ReadSingle(), m11 = reader.ReadSingle(), m12 = reader.ReadSingle(); - float m20 = reader.ReadSingle(), m21 = reader.ReadSingle(), m22 = reader.ReadSingle(); - ev.Rotation = new Matrix4x4( - m00, m01, m02, 0, - m10, m11, m12, 0, - m20, m21, m22, 0, - 0, 0, 0, 1 - ); - } - - // color in editor - ev.RawColor = reader.ReadUInt32(); - Logger.Warn(logSrc, $"Event[{i}] RawColor = 0x{ev.RawColor:X8}"); - - // add to events list - events.Add(ev); - - // sanity check - long consumed = reader.BaseStream.Position - startPos; - Logger.Warn(logSrc, $"Event[{i}] bytes consumed = {consumed}"); - } - - // fast-forward to end of section - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - return events; - } - - public static List ParseEvents(BinaryReader reader, long sectionEnd, int rfl_version) - { - if (rfl_version == 0x127) // 295 - { - Logger.Warn(logSrc, "RFL version 0x127 (295) is not yet supported for event parsing. Skipping events."); - - int count2 = reader.ReadInt32(); - Logger.Warn(logSrc, $"Reading {count2} events…"); - - for (int i = 0; i < count2; i++) - { - int extraIndex = 0; - while (reader.BaseStream.Position < sectionEnd) - { - var b = reader.ReadByte(); - //Logger.Warn(logSrc, $"event byte[{extraIndex}] = 0x{b:X2}"); - extraIndex++; - } - } - - return new List(); - } - - var events = new List(); - int count = reader.ReadInt32(); - Logger.Dev(logSrc, $"Reading {count} events…"); - - for (int i = 0; i < count; i++) - { - var ev = new RflEvent(); - - ev.UID = reader.ReadInt32(); - ev.ClassName = ReadVString(reader); - ev.Position = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - ev.ScriptName = ReadVString(reader); - ev.HiddenInEditor = reader.ReadByte() != 0; - ev.Delay = reader.ReadSingle(); - ev.Bool1 = reader.ReadByte() != 0; - ev.Bool2 = reader.ReadByte() != 0; - ev.Int1 = reader.ReadInt32(); - ev.Int2 = reader.ReadInt32(); - ev.Float1 = reader.ReadSingle(); - ev.Float2 = reader.ReadSingle(); - ev.Str1 = ReadVString(reader); - ev.Str2 = ReadVString(reader); - - // links (uid_list) - int numLinks = reader.ReadInt32(); - if (numLinks < 0 || reader.BaseStream.Position + numLinks * 4L > sectionEnd) - { - Logger.Warn(logSrc, $"Suspicious link count {numLinks} in event[{i}] – clamping to 0"); - numLinks = 0; - } - ev.Links = new List(numLinks); - for (int j = 0; j < numLinks; j++) - ev.Links.Add(reader.ReadInt32()); - - if (rfl_version >= 0x91 && (ev.ClassName == "Teleport" || ev.ClassName == "Play_Vclip" || ev.ClassName == "Teleport_Player") - || rfl_version >= 0x98 && ev.ClassName == "Alarm" - || rfl_version >= 0x12C && (ev.ClassName == "AF_Teleport_Player" || ev.ClassName == "Clone_Entity") - || rfl_version >= 0x12D && ev.ClassName == "Anchor_Marker_Orient") - { - ev.HasRotation = true; // used by exporter - float m00 = reader.ReadSingle(), m01 = reader.ReadSingle(), m02 = reader.ReadSingle(); - float m10 = reader.ReadSingle(), m11 = reader.ReadSingle(), m12 = reader.ReadSingle(); - float m20 = reader.ReadSingle(), m21 = reader.ReadSingle(), m22 = reader.ReadSingle(); - ev.Rotation = new Matrix4x4( - m00, m01, m02, 0, - m10, m11, m12, 0, - m20, m21, m22, 0, - 0, 0, 0, 1 - ); - } - - if (rfl_version >= 0xB0) - { - //reader.BaseStream.Seek(4, SeekOrigin.Current); // color - ev.RawColor = reader.ReadUInt32(); - Logger.Dev(logSrc, $"Event[{i}] RawColor = 0x{ev.RawColor:X8}"); - } - - Logger.Dev(logSrc, - $"Event[{i}] UID={ev.UID}, Class=\"{ev.ClassName}\", Pos={ev.Position}, " + - $"Script=\"{ev.ScriptName}\", Hidden={ev.HiddenInEditor}, Delay={ev.Delay}, " + - $"Bool1={ev.Bool1}, Bool2={ev.Bool2}, Int1={ev.Int1}, Int2={ev.Int2}, " + - $"Float1={ev.Float1}, Float2={ev.Float2}, Str1=\"{ev.Str1}\", Str2=\"{ev.Str2}\", " + - $"Links=[{string.Join(", ", ev.Links)}]" - ); - - if (ev != null) - events.Add(ev); - } - - // skip to end of this section - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - return events; - } - } - - public static class RflMpRespawnPointParser - { - private const string logSrc = "RflMpRespawnPointParser"; - - public static List ParseMpRespawnPoints(BinaryReader reader, long sectionEnd) - { - var list = new List(); - int count = reader.ReadInt32(); - Logger.Dev(logSrc, $"Reading {count} MP respawn points…"); - - for (int i = 0; i < count; i++) - { - var pt = new MpRespawnPoint(); - pt.UID = reader.ReadInt32(); - pt.Position = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - - // 3×3 rotation matrix → embed into Matrix4x4 - float m00 = reader.ReadSingle(), m01 = reader.ReadSingle(), m02 = reader.ReadSingle(); - float m10 = reader.ReadSingle(), m11 = reader.ReadSingle(), m12 = reader.ReadSingle(); - float m20 = reader.ReadSingle(), m21 = reader.ReadSingle(), m22 = reader.ReadSingle(); - pt.Rotation = new Matrix4x4( - m00, m01, m02, 0, - m10, m11, m12, 0, - m20, m21, m22, 0, - 0, 0, 0, 1 - ); - - pt.ScriptName = ReadVString(reader); - pt.HiddenInEditor = reader.ReadByte() != 0; - pt.TeamID = reader.ReadInt32(); - pt.RedTeam = reader.ReadByte() != 0; - pt.BlueTeam = reader.ReadByte() != 0; - pt.IsBot = reader.ReadByte() != 0; - - // print everything - Logger.Dev(logSrc, - $"MP Respawn[{i}] UID={pt.UID}, Pos={pt.Position}, Team={pt.TeamID} (R?{pt.RedTeam}/B?{pt.BlueTeam}), " + - $"Bot={pt.IsBot}, Hidden={pt.HiddenInEditor}, Script=\"{pt.ScriptName}\"" - ); - - list.Add(pt); - } - - // ensure we skip to end of section - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - return list; - } - } - - - public static class RflLevelPropertiesParser - { - private const string logSrc = "RflLevelPropertiesParser"; - - public static void ParseLevelPropertiesFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) - { - // geomod_texture - string geomodTexture = ReadVString(reader); - - // hardness - int hardness = reader.ReadInt32(); - - // ambient_color (RGBA) - byte ambR = reader.ReadByte(); - byte ambG = reader.ReadByte(); - byte ambB = reader.ReadByte(); - byte ambA = reader.ReadByte(); - - // directional_ambient_light - byte dirAmbient = reader.ReadByte(); - - // fog_color - byte fogR = reader.ReadByte(); - byte fogG = reader.ReadByte(); - byte fogB = reader.ReadByte(); - byte fogA = reader.ReadByte(); - - // fog near/far planes - float fogNear = reader.ReadSingle(); - float fogFar = reader.ReadSingle(); - Logger.Warn(logSrc, "Level properties:"); - Logger.Warn(logSrc, $" GeoMod texture: \"{geomodTexture}\""); - Logger.Warn(logSrc, $" GeoMod hardness: {hardness}"); - Logger.Warn(logSrc, $" Ambient light colour: R={ambR} G={ambG} B={ambB} A={ambA}"); - Logger.Warn(logSrc, $" Directional ambient light: {(dirAmbient != 0 ? "yes" : "no")}"); - Logger.Warn(logSrc, $" Distance fog colour: R={fogR} G={fogG} B={fogB} A={fogA}"); - Logger.Warn(logSrc, $" Distance fog near clip plane: {fogNear}"); - Logger.Warn(logSrc, $" Distance fog far clip plane: {fogFar}"); - - if (rfl_version == 0x127) // rf2 - { - uint unk1 = reader.ReadUInt32(); - uint unk2 = reader.ReadUInt32(); - float unk3 = reader.ReadSingle(); - float unk4 = reader.ReadSingle(); - float unk5 = reader.ReadSingle(); - byte unk1R = reader.ReadByte(), unk1G = reader.ReadByte(), unk1B = reader.ReadByte(), unk1A = reader.ReadByte(); - byte unk2R = reader.ReadByte(), unk2G = reader.ReadByte(), unk2B = reader.ReadByte(), unk2A = reader.ReadByte(); - float unk6 = reader.ReadSingle(); - SavedValues.maybeExposure = unk6 * 10.0f; - - Logger.Warn(logSrc, "Guesses at unknown RF2 level properties fields:"); - Logger.Warn(logSrc, $" Unk1 (flags?): 0x{unk1:X8}"); - Logger.Warn(logSrc, $" Unk2 (flags?): 0x{unk2:X8}"); - Logger.Warn(logSrc, $" maybeViewDistance: {unk3}"); - Logger.Warn(logSrc, $" maybeAmbientScale: {unk4}"); - Logger.Warn(logSrc, $" maybeSunAngleDeg: {unk5}"); - Logger.Warn(logSrc, $" maybeSkyColorRGBA: [{unk1R},{unk1G},{unk1B},{unk1A}]"); - Logger.Warn(logSrc, $" maybeSkyColorAltRGBA: [{unk2R},{unk2G},{unk2B},{unk2A}]"); - Logger.Warn(logSrc, $" maybeExposure: {unk6}, light multiplier {SavedValues.maybeExposure}"); - } - - // now read everything until we hit sectionEnd - int extraIndex = 0; - while (reader.BaseStream.Position < sectionEnd) - { - var b = reader.ReadSingle(); - Logger.Warn(logSrc, $"level_properties extra byte[{extraIndex}] = 0x{b:X2}"); - extraIndex++; - } - - } - } - - - static class RflLightParser - { - private const string logSrc = "RflLightParser"; - - public static List ParseLightsFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) - { - var lights = new List(); - int numLights = reader.ReadInt32(); - Logger.Dev(logSrc, $"Reading {numLights} lights…"); - - for (int i = 0; i < numLights; i++) - { - var light = new Light(); - - light.UID = reader.ReadInt32(); - light.ClassName = ReadVString(reader); - light.Position = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - - // 3×3 rotation matrix → embed into Matrix4x4 - var m00 = reader.ReadSingle(); var m01 = reader.ReadSingle(); var m02 = reader.ReadSingle(); - var m10 = reader.ReadSingle(); var m11 = reader.ReadSingle(); var m12 = reader.ReadSingle(); - var m20 = reader.ReadSingle(); var m21 = reader.ReadSingle(); var m22 = reader.ReadSingle(); - light.Rotation = new Matrix4x4( - m00, m01, m02, 0, - m10, m11, m12, 0, - m20, m21, m22, 0, - 0, 0, 0, 1 - ); - - light.ScriptName = ReadVString(reader); - light.HiddenInEditor = reader.ReadByte() != 0; - - uint rawFlags = reader.ReadUInt32(); - light.Dynamic = (rawFlags & 0x1) != 0; - light.Fade = (rawFlags & 0x2) != 0; - light.ShadowCasting = (rawFlags & 0x4) != 0; - light.IsEnabled = (rawFlags & 0x8) != 0; - light.Type = (LightType)(rawFlags >> 4 & 0x3); - light.InitialState = (LightState)(rawFlags >> 8 & 0xF); - light.RuntimeShadow = (rawFlags & 0x2000) != 0; - - // RGBA - byte r = reader.ReadByte(); - byte g = reader.ReadByte(); - byte b = reader.ReadByte(); - byte a = reader.ReadByte(); - light.Color = new Vector4(r / 255f, g / 255f, b / 255f, a / 255f); - - light.Range = reader.ReadSingle(); - light.FOV = reader.ReadSingle(); - light.FOVDropoff = reader.ReadSingle(); - light.IntensityAtMaxRange = reader.ReadSingle(); - light.DropoffType = reader.ReadInt32(); - light.TubeLightWidth = reader.ReadSingle(); - - if (rfl_version != 0x127) // 295 - { - light.OnIntensity = reader.ReadSingle(); - } - else - { - float I0 = reader.ReadSingle(); // the raw RF2 OnIntensity - light.OnIntensity = I0; - light.Range *= 2; - /*float R = light.Range; - switch (light.DropoffType) - { - case 0: - // linear: I(d) = I0 * (1 – d/R) - // so I0_file = I_linear_at_d=0 / R → our engine wants I(d=0)=I0*R - light.OnIntensity = I0 * R; - break; - - case 1: - // squared: I(d) = I0 * (1 – d/R)^2 - // invert so total “power” matches at d=0 → scale by R^2 - light.OnIntensity = I0 * R * R; - break; - - case 2: - // cosine: I(d) = I0 * cos(d/R * (π/2)) - // invert so at d=0 you get I0 * 1 → no extra scaling, but - // if your target engine expects I(d=R)=0, you leave it unmodified - light.OnIntensity = I0; - break; - - case 3: - // sqrt: I(d) = I0 * sqrt(1 – d/R) - // invert so I(d=0) = I0 * 1 → must scale by √R - light.OnIntensity = I0 * MathF.Sqrt(R); - break; - - default: - // unknown: treat as linear - light.OnIntensity = I0 * R; - break; - }*/ - - light.OnIntensity *= SavedValues.maybeExposure; - - Logger.Dev(logSrc, $"Converted intensity for RF2 light {light.UID}. DropoffType={light.DropoffType}, Range={light.Range}, RawIntensity={I0}, NewIntensity={light.OnIntensity}"); - } - light.OnTime = reader.ReadSingle(); - light.OnTimeVariation = reader.ReadSingle(); - light.OffIntensity = reader.ReadSingle(); - light.OffTime = reader.ReadSingle(); - light.OffTimeVariation = reader.ReadSingle(); - - lights.Add(light); - - // (we leave reader at end of this light record) - Logger.Dev(logSrc, - $"Loaded Light[{i}] UID={light.UID}, class={light.ClassName}, pos={light.Position}, " + - $"flags=0x{rawFlags:X8}, color={light.Color}, range={light.Range}, int={light.OnIntensity}, intmr={light.IntensityAtMaxRange}" - ); - } - - return lights; - } - } - - public static class RflBrushParser - { - private const string logSrc = "RflBrushParser"; - public static void ParseBrushesFromRfl(BinaryReader reader, long sectionEnd, Mesh mesh, int rfl_version) - { - int numBrushes = reader.ReadInt32(); - for (int i = 0; i < numBrushes; i++) - { - Logger.Debug(logSrc, $"Reading brush {i}/{numBrushes}"); - Brush brush = RFGeometryParser.ReadBrush(reader, rfl_version); - if (brush != null) // for safety, only add valid brushes - mesh.Brushes.Add(brush); - } - - Logger.Info(logSrc, $"Parsed {numBrushes} brushes from RF1 RFL file."); - - // Ensure we're positioned correctly at the end of the section - reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); - } - } + // root rfl parser + public static class RflParser + { + private const string logSrc = "RflParser"; + public static Mesh ReadRfl(string filePath) + { + var mesh = new Mesh(); + + using var stream = File.OpenRead(filePath); + using var reader = new BinaryReader(stream); + + // Validate magic number + uint magic = reader.ReadUInt32(); + if (magic != 0xD4BADA55) + throw new Exception("Invalid RFL file: wrong magic."); + + // Read header + int version = reader.ReadInt32(); + uint timestamp = reader.ReadUInt32(); + int playerStartOffset = reader.ReadInt32(); + int levelInfoOffset = reader.ReadInt32(); + int numSections = reader.ReadInt32(); + int sectionsSize = reader.ReadInt32(); + + // What version of RFL is this? + bool isAlpine = version >= 0x12C; + bool isRF1 = version <= 0xC8 || isAlpine; // <= 201 or >= 300 + bool isRF2 = version == 0x127; // 295 + + string levelName = ReadVString(reader); + string modName = ""; + if (version >= 0xB2 && !isRF2) + modName = ReadVString(reader); + + Logger.Info(logSrc, $"Parsing {levelName}..."); + if (modName.Length > 0) + Logger.Info(logSrc, $"Required mod: {modName}"); + Logger.Info(logSrc, $"RFL version: {version}"); + if (isRF2) + { + Logger.Info(logSrc, $"Red Faction 2 RFL detected, using RF2 parsing"); + if (Config.TranslateRF2Textures) + { + RF2TextureTranslator.LoadRF2TextureTranslations(); + Logger.Info(logSrc, $"Loaded {RF2TextureTranslator.TranslationCount} RF2 texture filename translation definitions"); + } + } + else if (isAlpine) + { + Logger.Info(logSrc, $"Red Faction 1 (Alpine Faction) RFL detected, using RF1 parsing"); + } + else if (isRF1) + { + Logger.Info(logSrc, $"Red Faction 1 RFL detected, using RF1 parsing"); + } + + if (Config.ParseBrushSectionInstead) + { + Logger.Info(logSrc, $"-brushes option used, parsing brush data instead of static geometry"); + } + + if (!Config.TriangulatePolygons) + { + Logger.Info(logSrc, $"-ngons option used, not forcing triangulation of parsed polygons"); + } + + // Read sections + for (int i = 0; i < numSections; i++) + { + long sectionHeaderPos = reader.BaseStream.Position; + + if (reader.BaseStream.Position + 8 > reader.BaseStream.Length) + { + Logger.Warn(logSrc, $"Reached EOF unexpectedly while reading section header {i}."); + break; + } + + int sectionType = reader.ReadInt32(); + int sectionSize = reader.ReadInt32(); + long sectionStart = reader.BaseStream.Position; + long sectionEnd = sectionStart + sectionSize; + + Logger.Info(logSrc, $"Section {i}: Type 0x{sectionType:X}, Size {sectionSize} at 0x{sectionHeaderPos:X}"); + + if (sectionEnd > reader.BaseStream.Length) + { + Logger.Warn(logSrc, $"Section {i} exceeds file length. Skipping."); + break; + } + + if (sectionType == 0x100 && !Config.ParseBrushSectionInstead) // static geometry + { + Logger.Debug(logSrc, $"Found static geometry section (0x100) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); + Brush brush = RFGeometryParser.ReadStaticGeometry(reader, version); + mesh.Brushes.Add(brush); + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + + } + else if (sectionType == 0x02000000 && Config.ParseBrushSectionInstead) // brushes + { + Logger.Debug(logSrc, $"Found brush geometry section (0x02000000) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); + RflBrushParser.ParseBrushesFromRfl(reader, sectionEnd, mesh, version); + } + else if (sectionType == 0x00000300) // lights + { + Logger.Debug(logSrc, $"Found lights section (0x00000300) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); + mesh.Lights.AddRange(RflLightParser.ParseLightsFromRfl(reader, sectionEnd, version)); + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + + } + else if (sectionType == 0x0900) // level_properties + { + Logger.Debug(logSrc, $"Found level_properties section (0x0900) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); + RflLevelPropertiesParser.ParseLevelPropertiesFromRfl(reader, sectionEnd, version); + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); // shouldn't be needed, but for safety since RF2 rfls have some unknowns + } + else if (sectionType == 0x01000000) // level_info + { + Logger.Debug(logSrc, $"Found level_info section (0x01000000) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); + RflLevelInfoParser.ParseLevelInfoFromRfl(reader, sectionEnd, version); + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); // shouldn't be needed, but for safety since RF2 rfls have some unknowns + } + else if (sectionType == 0x00000700) // mp_respawn_points + { + Logger.Debug(logSrc, $"Found mp_respawn_points section (0x700) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); + mesh.MPRespawnPoints.AddRange(RflMpRespawnPointParser.ParseMpRespawnPoints(reader, sectionEnd)); + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + } + else if (sectionType == 0x00000a00) // particle_emitters + { + Logger.Debug(logSrc, $"Found particle_emitters section (0x00000a00) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); + mesh.ParticleEmitters.AddRange(RflParticleEmitterParser.ParseParticleEmittersFromRfl(reader, sectionEnd, version)); + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + } + else if (sectionType == 0x00000600) // events + { + Logger.Warn(logSrc, $"Found events section (0x600) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); + if (RflUtils.IsRF2(version)) + { + Logger.Info(logSrc, "events section support for RF2 RFLs is a work-in-progress. Parser output may have errors."); + mesh.Events.AddRange(RflEventParser.ParseEventsRF2(reader, sectionEnd, version)); + } + else + { + mesh.Events.AddRange(RflEventParser.ParseEvents(reader, sectionEnd, version)); + } + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + } + else if (sectionType == 0x00001100) // push_regions + { + Logger.Debug(logSrc, $"Found push_regions section (0x00001100) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); + mesh.PushRegions.AddRange(RflPushRegionParser.ParsePushRegionsFromRfl(reader, sectionEnd)); + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + } + else if (sectionType == 0x00060000) // triggers + { + Logger.Debug(logSrc, $"Found triggers section (0x00060000) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); + if (isRF2) + { + Logger.Info(logSrc, "trigger section support for RF2 RFLs is a work-in-progress. Parser output may have errors."); + } + mesh.Triggers.AddRange(RflTriggerParser.ParseTriggersFromRfl(reader, sectionEnd, version)); + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + } + else if (sectionType == 0x00040000) // items + { + Logger.Debug(logSrc, $"Found items section (0x00040000) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); + mesh.Items.AddRange(RflItemParser.ParseItemsFromRfl(reader, sectionEnd, version)); + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + } + else if (sectionType == 0x00000d00) // climbing regions + { + Logger.Debug(logSrc, $"Found climbing_regions section (0x00000d00) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); + mesh.ClimbingRegions.AddRange(RflClimbingRegionParser.ParseClimbingRegionsFromRfl(reader, sectionEnd)); + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + } + else if (sectionType == 0x00010000) // waypoint lists + { + Logger.Debug(logSrc, $"Found waypoint_lists section (0x00010000) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); + mesh.WaypointLists.AddRange(RflWaypointListParser.ParseWaypointListsFromRfl(reader, sectionEnd, version)); + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + } + else if (sectionType == 0x00020000) // nav points + { + Logger.Debug(logSrc, $"Found nav_points section (0x00010000) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); + if (isRF2 && 1 == 0) // temp disabled + { + Logger.Info(logSrc, "nav_points section support for RF2 RFLs is a work-in-progress. Parser output may have errors."); + mesh.NavPoints.AddRange(RflNavPointParser.ParseNavPointsFromRfl(reader, sectionEnd, version)); + } + + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + } + else if (sectionType == 0x00001000) // decals + { + Logger.Debug(logSrc, $"Found decals section (0x1000) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); + mesh.Decals.AddRange(RflDecalParser.ParseDecalsFromRfl(reader, sectionEnd, version)); + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + } + else if (sectionType == 0x00001200) // lightmaps (RF1 only) + { + Logger.Debug(logSrc, $"Found lightmaps section (0x1200) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); + mesh.Lightmaps.AddRange(RflLightmapParser.ParseLightmapsFromRfl(reader, sectionEnd)); + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + + if (Config.DumpLightmaps) + { + Logger.Debug(logSrc, "Dumping lightmaps..."); + for (int ilm = 0; ilm < mesh.Lightmaps.Count; ilm++) + { + var lm = mesh.Lightmaps[ilm]; + string outputPath = $"lightmap_{ilm}.tga"; + TgaExporter.Write24BitTga(outputPath, lm); + } + } + } + else if (sectionType == 0x00002000) // movers + { + Logger.Debug(logSrc, $"Found movers section (0x00002000) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); + mesh.Movers.AddRange(RflMoverParser.ParseMoversFromRfl(reader, sectionEnd, version)); + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + } + else if (sectionType == 0x00003000) // moving_groups + { + Logger.Debug(logSrc, $"Found moving_groups section (0x00003000) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); + mesh.Groups.AddRange(RflGroupParser.ParseGroupsFromRfl(reader, sectionEnd, version)); + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + } + else if (sectionType == 0x00050000) // clutters + { + Logger.Debug(logSrc, $"Found clutters section (0x00050000) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); + mesh.Clutters.AddRange(RflClutterParser.ParseCluttersFromRfl(reader, sectionEnd, version)); + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + } + else if (sectionType == 0x7678) // coronas / glares chunk? + { + Logger.Info(logSrc, $"Found coronas section (0x7678) at 0x{sectionHeaderPos:X8}, size={sectionSize}"); + mesh.Coronas.AddRange(RflCoronaParser.ParseCoronasFromRfl(reader, sectionEnd)); + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + } + + else if (sectionType == 0x0) // end section + { + // once we've reached section type 0 we can stop reading further sections + // rf2 rfls have a section 0 at the end for some reason. rf1 rfls do not + break; + } + else + { + reader.BaseStream.Seek(sectionSize, SeekOrigin.Current); // Skip unknown section + } + + //rf2: + // 0x7001 = skeletal meshes (rfc) + // 0x7002 = animations (rfa) + // 0x7003 = static meshes (rfm) + // 0x7004 = rfe + // 0x7005 = maybe texture packfiles? (peg) + // 0x400 = related to cutscenes/cameras? + // 0x500 = related to sounds? + // 0x7677 = totally unknown + // 0x7777 = something to do with events? + // 0x7680 = something to do with vfx? + // 0x30000 = entities + // 0x7681 = something to do with vfx? + // 0x7779 = something to do with spline paths? + + } + + return mesh; + } + } + + static class RflCoronaParser + { + private const string logSrc = "RflCoronaParser"; + + public static List ParseCoronasFromRfl(BinaryReader reader, long sectionEnd) + { + int count = reader.ReadInt32(); + Logger.Info(logSrc, $"Corona count = {count}"); + var list = new List(count); + + for (int i = 0; i < count; i++) + { + var c = new Corona(); + long startPos = reader.BaseStream.Position; + + c.UID = reader.ReadInt32(); + Logger.Dev(logSrc, $"Corona[{i}] UID = {c.UID}"); + + c.Name = Utils.ReadVString(reader); + Logger.Dev(logSrc, $"Corona[{i}] Name = \"{c.Name}\""); + + float px = reader.ReadSingle(), py = reader.ReadSingle(), pz = reader.ReadSingle(); + c.Position = new Vector3(px, py, pz); + Logger.Dev(logSrc, $"Corona[{i}] Pos = ({px:F3}, {py:F3}, {pz:F3})"); + + byte r = reader.ReadByte(), + g = reader.ReadByte(), + b = reader.ReadByte(), + a = reader.ReadByte(); + c.Color = new Vector4(r / 255f, g / 255f, b / 255f, a / 255f); + Logger.Dev(logSrc, $"Corona[{i}] Color = ({r},{g},{b},{a})"); + + // unknown fields + reader.ReadSingle(); + reader.ReadSingle(); + reader.ReadSingle(); + reader.ReadSingle(); + reader.ReadSingle(); + reader.ReadSingle(); + reader.ReadSingle(); + reader.ReadByte(); + reader.ReadByte(); + reader.ReadByte(); + reader.ReadByte(); + + c.ScriptName = Utils.ReadVString(reader); + Logger.Dev(logSrc, $"Corona[{i}] Script Name = \"{c.ScriptName}\""); + + // unknown fields + reader.ReadByte(); + reader.ReadByte(); + reader.ReadSingle(); + reader.ReadSingle(); + + + c.CoronaBitmap = Utils.ReadVString(reader); + Logger.Dev(logSrc, $"Corona[{i}] CoronaBitmap = \"{c.CoronaBitmap}\""); + + c.ConeAngle = reader.ReadSingle(); + Logger.Dev(logSrc, $"Corona[{i}] ConeAngle = {c.ConeAngle}"); + c.Intensity = reader.ReadSingle(); + Logger.Dev(logSrc, $"Corona[{i}] Intensity = {c.Intensity}"); + c.RadiusDistance = reader.ReadSingle(); + Logger.Dev(logSrc, $"Corona[{i}] RadiusDistance = {c.RadiusDistance}"); + c.RadiusScale = reader.ReadSingle(); + Logger.Dev(logSrc, $"Corona[{i}] RadiusScale = {c.RadiusScale}"); + c.DiminishDistance = reader.ReadSingle(); + Logger.Dev(logSrc, $"Corona[{i}] DiminishDistance = {c.DiminishDistance}"); + + c.VolumetricBitmap = Utils.ReadVString(reader); + Logger.Dev(logSrc, $"Corona[{i}] VolumetricBitmap = \"{c.VolumetricBitmap}\""); + + if (!string.IsNullOrEmpty(c.VolumetricBitmap)) + { + c.VolumetricHeight = reader.ReadSingle(); + Logger.Dev(logSrc, $"Corona[{i}] VolumetricHeight = {c.VolumetricHeight}"); + c.VolumetricLength = reader.ReadSingle(); + Logger.Dev(logSrc, $"Corona[{i}] VolumetricLength = {c.VolumetricLength}"); + + // unknown value + reader.ReadSingle(); + } + + // unknown fields + reader.ReadByte(); + reader.ReadByte(); + reader.ReadByte(); + reader.ReadByte(); + reader.ReadByte(); + + list.Add(c); + + Logger.Dev(logSrc, $"-- bytes consumed for Corona[{i}]: {reader.BaseStream.Position - startPos}\n"); + } + + return list; + } + } + static class RflClutterParser + { + private const string logSrc = "RflClutterParser"; + + public static List ParseCluttersFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) + { + int numClutters = reader.ReadInt32(); + Logger.Debug(logSrc, $"Reading {numClutters} clutter(s)…"); + + var clutters = new List(numClutters); + for (int i = 0; i < numClutters; i++) + { + var c = new Clutter(); + + c.UID = reader.ReadInt32(); + Logger.Dev(logSrc, $"Clutter[{i}] UID = {c.UID}"); + + c.ClassName = Utils.ReadVString(reader); + Logger.Dev(logSrc, $"Clutter[{i}] ClassName = \"{c.ClassName}\""); + + float px = reader.ReadSingle(), + py = reader.ReadSingle(), + pz = reader.ReadSingle(); + c.Position = new Vector3(px, py, pz); + Logger.Dev(logSrc, $"Clutter[{i}] Pos = ({px:F2},{py:F2},{pz:F2})"); + + float m00 = reader.ReadSingle(), m01 = reader.ReadSingle(), m02 = reader.ReadSingle(); + float m10 = reader.ReadSingle(), m11 = reader.ReadSingle(), m12 = reader.ReadSingle(); + float m20 = reader.ReadSingle(), m21 = reader.ReadSingle(), m22 = reader.ReadSingle(); + c.Rotation = new Matrix4x4( + m00, m01, m02, 0f, + m10, m11, m12, 0f, + m20, m21, m22, 0f, + 0f, 0f, 0f, 1f + ); + Logger.Dev(logSrc, $"Clutter[{i}] Rotation matrix read"); + + c.ScriptName = Utils.ReadVString(reader); + Logger.Dev(logSrc, $"Clutter[{i}] ScriptName = \"{c.ScriptName}\""); + + c.HiddenInEditor = reader.ReadByte() != 0; + Logger.Dev(logSrc, $"Clutter[{i}] HiddenInEditor = {c.HiddenInEditor}"); + + // RF1 has an unknown int here that isn't loaded by the game, not present in RF2 + if (RflUtils.IsRF1(rfl_version)) + { + int unk = reader.ReadInt32(); + Logger.Dev(logSrc, $"Clutter[{i}] Unknown field = {unk}"); + } + + c.Skin = Utils.ReadVString(reader); + Logger.Dev(logSrc, $"Clutter[{i}] Skin = \"{c.Skin}\""); + + int numLinks = reader.ReadInt32(); + c.Links = new List(numLinks); + for (int j = 0; j < numLinks; j++) + { + int link = reader.ReadInt32(); + c.Links.Add(link); + } + Logger.Dev(logSrc, $"Clutter[{i}] Links = [{string.Join(",", c.Links)}]"); + + clutters.Add(c); + } + + Logger.Debug(logSrc, "Finished parsing clutters"); + return clutters; + } + } + + static class RflMoverParser + { + private const string logSrc = "RflMoverParser"; + + public static List ParseMoversFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) + { + int numMovers = reader.ReadInt32(); + Logger.Debug(logSrc, $"Reading {numMovers} mover(s)..."); + Logger.Dev(logSrc, $"→ Section size: {sectionEnd - reader.BaseStream.Position} bytes"); + + var movers = new List(numMovers); + for (int i = 0; i < numMovers; i++) + { + long startPos = reader.BaseStream.Position; + var moverBrush = RFGeometryParser.ReadBrush(reader, rfl_version); + if (moverBrush != null) + { + Logger.Dev(logSrc, + $"Mover[{i}] UID={moverBrush.UID}, " + + $"Vertices={moverBrush.Vertices.Count}, " + + $"Faces={moverBrush.Solid.Faces.Count}, " + + $"BytesConsumed={reader.BaseStream.Position - startPos}" + ); + movers.Add(moverBrush); + } + else + { + Logger.Dev(logSrc, $"Mover[{i}] returned null brush, skipping"); + } + } + + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + Logger.Dev(logSrc, $"Finished parsing movers; reader at 0x{reader.BaseStream.Position:X}"); + return movers; + } + } + + static class RflGroupParser + { + private const string logSrc = "RflGroupParser"; + + public static List ParseGroupsFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) + { + int numGroups = reader.ReadInt32(); + Logger.Debug(logSrc, $"Reading {numGroups} group(s)..."); + Logger.Dev(logSrc, $"→ Section size: {sectionEnd - reader.BaseStream.Position} bytes"); + + var groups = new List(numGroups); + for (int i = 0; i < numGroups; i++) + { + long groupStart = reader.BaseStream.Position; + var grp = new Group(); + + grp.Name = Utils.ReadVString(reader); + Logger.Dev(logSrc, $"Group[{i}] Name=\"{grp.Name}\""); + + byte unknown = reader.ReadByte(); + Logger.Info(logSrc, $"Group[{i}] UnknownByte=0x{unknown:X2}"); + + if (RflUtils.IsRF2(rfl_version)) + { + //reader.ReadInt32(); + var unk1 = reader.ReadByte() != 0; + var unk2 = reader.ReadByte() != 0; + var unk3 = reader.ReadByte() != 0; + var unk4 = reader.ReadByte() != 0; + Logger.Info(logSrc, $"Group[{i}] unk1={unk1}, unk2={unk2}, unk3={unk3}, unk4={unk4}"); + } + + grp.IsMoving = reader.ReadByte() != 0; + Logger.Info(logSrc, $"Group[{i}] IsMoving={grp.IsMoving}"); + + if (grp.IsMoving) + { grp.MovingData = new MovingGroupData(); + int numKeyframes = reader.ReadInt32(); + Logger.Dev(logSrc, $"Group[{i}] KeyframesCount={numKeyframes}"); + + for (int k = 0; k < numKeyframes; k++) + { + var kf = new Keyframe + { + UID = reader.ReadInt32(), + Pos = new System.Numerics.Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), + Rot = new System.Numerics.Matrix4x4( + reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 0f, + reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 0f, + reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 0f, + 0f, 0f, 0f, 1f), + ScriptName = Utils.ReadVString(reader), + HiddenInEditor = reader.ReadByte() != 0, + PauseTime = reader.ReadSingle(), + DepartTravelTime = reader.ReadSingle(), + ReturnTravelTime = reader.ReadSingle(), + AccelTime = reader.ReadSingle(), + DecelTime = reader.ReadSingle(), + EventUID = reader.ReadInt32(), + ItemUID1 = reader.ReadInt32(), + ItemUID2 = reader.ReadInt32(), + DegreesAboutAxis = reader.ReadSingle() + }; + Logger.Dev(logSrc, + $" Keyframe[{k}] UID={kf.UID}, Pos=({kf.Pos.X:F2},{kf.Pos.Y:F2},{kf.Pos.Z:F2}), " + + $"PauseTime={kf.PauseTime:F2}" + ); + grp.MovingData.Keyframes.Add(kf); + } + + int numMemberTransforms = reader.ReadInt32(); + Logger.Dev(logSrc, $"Group[{i}] MemberTransformsCount={numMemberTransforms}"); + for (int m = 0; m < numMemberTransforms; m++) + { + var mt = new MovingGroupMemberTransform + { + UID = reader.ReadInt32(), + Pos = new System.Numerics.Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()), + Rot = new System.Numerics.Matrix4x4( + reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 0f, + reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 0f, + reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 0f, + 0f, 0f, 0f, 1f) + }; + Logger.Dev(logSrc, + $" MemberTransform[{m}] UID={mt.UID}, Pos=({mt.Pos.X:F2},{mt.Pos.Y:F2},{mt.Pos.Z:F2})" + ); + grp.MovingData.MemberTransforms.Add(mt); + } + + // additional flags + grp.MovingData.IsDoor = reader.ReadByte() != 0; Logger.Dev(logSrc, $" IsDoor={grp.MovingData.IsDoor}"); + grp.MovingData.RotateInPlace = reader.ReadByte() != 0; Logger.Dev(logSrc, $" RotateInPlace={grp.MovingData.RotateInPlace}"); + grp.MovingData.StartsBackwards = reader.ReadByte() != 0; Logger.Dev(logSrc, $" StartsBackwards={grp.MovingData.StartsBackwards}"); + grp.MovingData.UseTravelTimeAsSpeed = reader.ReadByte() != 0; Logger.Dev(logSrc, $" UseTravelTimeAsSpeed={grp.MovingData.UseTravelTimeAsSpeed}"); + grp.MovingData.ForceOrient = reader.ReadByte() != 0; Logger.Dev(logSrc, $" ForceOrient={grp.MovingData.ForceOrient}"); + grp.MovingData.NoPlayerCollide = reader.ReadByte() != 0; Logger.Dev(logSrc, $" NoPlayerCollide={grp.MovingData.NoPlayerCollide}"); + + // unk RF2 specific field + if (RflUtils.IsRF2(rfl_version)) + { + reader.ReadByte(); + } + + grp.MovingData.MovementType = reader.ReadInt32(); Logger.Dev(logSrc, $" MovementType={grp.MovingData.MovementType}"); + grp.MovingData.StartingKeyframe = reader.ReadInt32(); Logger.Dev(logSrc, $" StartingKeyframe={grp.MovingData.StartingKeyframe}"); + grp.MovingData.StartSound = Utils.ReadVString(reader); Logger.Dev(logSrc, $" StartSound=\"{grp.MovingData.StartSound}\""); + grp.MovingData.StartVol = reader.ReadSingle(); Logger.Dev(logSrc, $" StartVol={grp.MovingData.StartVol}"); + grp.MovingData.LoopingSound = Utils.ReadVString(reader); Logger.Dev(logSrc, $" LoopingSound=\"{grp.MovingData.LoopingSound}\""); + grp.MovingData.LoopingVol = reader.ReadSingle(); Logger.Dev(logSrc, $" LoopingVol={grp.MovingData.LoopingVol}"); + grp.MovingData.StopSound = Utils.ReadVString(reader); Logger.Dev(logSrc, $" StopSound=\"{grp.MovingData.StopSound}\""); + grp.MovingData.StopVol = reader.ReadSingle(); Logger.Dev(logSrc, $" StopVol={grp.MovingData.StopVol}"); + grp.MovingData.CloseSound = Utils.ReadVString(reader); Logger.Dev(logSrc, $" CloseSound=\"{grp.MovingData.CloseSound}\""); + grp.MovingData.CloseVol = reader.ReadSingle(); Logger.Dev(logSrc, $" CloseVol={grp.MovingData.CloseVol}"); + + + } + + // unk RF2 specific field + if (RflUtils.IsRF2(rfl_version)) + { + var unk1 = reader.ReadInt32(); + Logger.Dev(logSrc, $"Group[{i}] unk1={unk1}"); + + } + + grp.Objects = ReadUidList(reader); + Logger.Info(logSrc, $"Group[{i}] ObjectsCount={grp.Objects.Count}"); + + grp.Brushes = ReadUidList(reader); + Logger.Info(logSrc, $"Group[{i}] BrushesCount={grp.Brushes.Count}"); + + // unk RF2 specific fields + if ( RflUtils.IsRF2(rfl_version)) + { + reader.ReadInt32(); + reader.ReadInt32(); + reader.ReadInt32(); + reader.ReadInt32(); + reader.ReadInt32(); + reader.ReadInt32(); + reader.ReadInt32(); + reader.ReadInt32(); + reader.ReadInt32(); + reader.ReadInt32(); + } + + groups.Add(grp); + Logger.Dev(logSrc, + $"Parsed Group[{i}] \"{grp.Name}\" @ bytes {reader.BaseStream.Position - groupStart}" + ); + } + + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + Logger.Dev(logSrc, $"Finished parsing groups; reader at 0x{reader.BaseStream.Position:X}"); + return groups; + } + + private static List ReadUidList(BinaryReader reader) + { + int n = reader.ReadInt32(); + Logger.Info(logSrc, $"Count: {n}"); + var list = new List(n); + for (int i = 0; i < n; i++) + { + int uid = reader.ReadInt32(); + list.Add(uid); + Logger.Info(logSrc, $"Element[{i}]: {uid}"); + } + return list; + } + } + static class RflDecalParser + { + private const string logSrc = "RflDecalParser"; + + public static List ParseDecalsFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) + { + int numDecals = reader.ReadInt32(); + Logger.Info(logSrc, $"Found {numDecals} decal(s)."); + + var result = new List(numDecals); + for (int i = 0; i < numDecals; i++) + { + var d = new Decal(); + + d.UID = reader.ReadInt32(); + + d.ClassName = Utils.ReadVString(reader); + + float px = reader.ReadSingle(), py = reader.ReadSingle(), pz = reader.ReadSingle(); + d.Position = new Vector3(px, py, pz); + + float fx = reader.ReadSingle(), fy = reader.ReadSingle(), fz = reader.ReadSingle(); + float rx = reader.ReadSingle(), ry = reader.ReadSingle(), rz = reader.ReadSingle(); + float ux = reader.ReadSingle(), uy = reader.ReadSingle(), uz = reader.ReadSingle(); + // embed mat3 into a 4×4 matrix (row‐major as usual): + d.Rotation = new Matrix4x4( + rx, ry, rz, 0f, + ux, uy, uz, 0f, + fx, fy, fz, 0f, + 0f, 0f, 0f, 1f + ); + + d.ScriptName = Utils.ReadVString(reader); + + d.HiddenInEditor = reader.ReadByte() != 0; + + float ex = reader.ReadSingle(), ey = reader.ReadSingle(), ez = reader.ReadSingle(); + d.Extents = new Vector3(ex, ey, ez); + + var tex = Utils.ReadVString(reader); + + if (RflUtils.IsRF2(rfl_version) && Config.TranslateRF2Textures) + { + string translatedTex = RF2TextureTranslator.TranslateRF2Texture(tex); + Logger.Dev(logSrc, $"Texture {i}: \"{tex}\" → \"{translatedTex}\""); + d.Texture = translatedTex; + } + else if (RflUtils.IsRF2(rfl_version) && Config.InsertRF2TexturePrefix) + { + string translatedTex = RF2TextureTranslator.InsertRxPrefix(tex); + Logger.Dev(logSrc, $"Texture {i}: \"{tex}\" → \"{translatedTex}\""); + d.Texture = translatedTex; + } + else + { + Logger.Dev(logSrc, $"Texture {i}: \"{tex}\""); + d.Texture = tex; + } + + d.Alpha = reader.ReadInt32(); + + d.SelfIlluminated = reader.ReadByte() != 0; + + d.Tiling = reader.ReadInt32(); + + d.Scale = reader.ReadSingle(); + + // Unknown fields in RF2 RFLs - uncertain if data types are correct + // One of these is "glow" (like in particle emitters), unsure which + if (RflUtils.IsRF2(rfl_version)) + { + var unk1 = reader.ReadSingle(); + var unk2 = reader.ReadInt32(); + var unk3 = reader.ReadInt32(); + Logger.Dev(logSrc, $"RF2 specific fields - Decal[{i}] unk1={unk1}, unk2={unk2}, unk3={unk3}"); + } + + Logger.Dev(logSrc, + $"Decal[{i}] UID={d.UID}, ClassName=\"{d.ClassName}\", Pos=({px},{py},{pz}), " + + $"Rot=[{fx},{fy},{fz} ; {rx},{ry},{rz} ; {ux},{uy},{uz}], " + + $"Script=\"{d.ScriptName}\", Hidden={d.HiddenInEditor}, Extents=({ex},{ey},{ez}), " + + $"Texture=\"{d.Texture}\", Alpha={d.Alpha}, SelfIlluminated={d.SelfIlluminated}, " + + $"Tiling={d.Tiling}, Scale={d.Scale}" + ); + + result.Add(d); + } + + return result; + } + } + + static class RflWaypointListParser + { + private const string logSrc = "RflWaypointListParser"; + + public static List ParseWaypointListsFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) + { + int numLists = reader.ReadInt32(); + Logger.Dev(logSrc, $"Found {numLists} waypoint_list(s)."); + + var result = new List(numLists); + for (int i = 0; i < numLists; i++) + { + var list = new WaypointList(); + + list.Name = Utils.ReadVString(reader); + Logger.Dev(logSrc, $"WaypointList[{i}].Name = \"{list.Name}\""); + + int countWp = reader.ReadInt32(); + Logger.Dev(logSrc, $"WaypointList[{i}].Count = {countWp}"); + + for (int w = 0; w < countWp; w++) + { + int idx = reader.ReadInt32(); + list.WaypointIndices.Add(idx); + Logger.Dev(logSrc, $"WaypointList[{i}].WaypointIndices[{w}] = {idx}"); + } + + result.Add(list); + } + + return result; + } + } + + static class RflNavPointParser + { + private const string logSrc = "RflNavPointParser"; + + public static List ParseNavPointsFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) + { + int numPoints = reader.ReadInt32(); + Logger.Info(logSrc, $"Found {numPoints} nav_point(s)."); + + var tempPoints = new List(numPoints); + + for (int i = 0; i < numPoints; i++) + { + var np = new NavPoint(); + + np.UID = reader.ReadInt32(); + np.HiddenInEditor = reader.ReadByte() != 0; + np.Height = reader.ReadSingle(); + + float px = reader.ReadSingle(), py = reader.ReadSingle(), pz = reader.ReadSingle(); + np.Position = new Vector3(px, py, pz); + + np.Radius = reader.ReadSingle(); + + np.Type = RflUtils.IsRF1(rfl_version) ? (NavPointType)reader.ReadInt32() : (NavPointType)reader.ReadByte(); + + // only used in rf2 parsing + byte unk1 = 0; + byte unk2 = 0; + + if (RflUtils.IsRF2(rfl_version)) + { + unk1 = reader.ReadByte(); + unk2 = reader.ReadByte(); + } + + np.Directional = reader.ReadByte() != 0; + + //reader.ReadByte(); + + // only used in rf2 parsing + float unk3 = 0.0f; + + if (RflUtils.IsRF2(rfl_version)) + { + unk3 = reader.ReadSingle(); + } + + if (np.Directional) + { + float fx = reader.ReadSingle(), fy = reader.ReadSingle(), fz = reader.ReadSingle(); + float rx = reader.ReadSingle(), ry = reader.ReadSingle(), rz = reader.ReadSingle(); + float ux = reader.ReadSingle(), uy = reader.ReadSingle(), uz = reader.ReadSingle(); + + np.Rotation = new Matrix4x4( + rx, ry, rz, 0f, + ux, uy, uz, 0f, + fx, fy, fz, 0f, + 0f, 0f, 0f, 1f + ); + } + + // only used in rf2 parsing + int unk4 = 0; + + if (RflUtils.IsRF1(rfl_version)) + { + np.Cover = reader.ReadByte() != 0; + np.Hide = reader.ReadByte() != 0; + np.Crunch = reader.ReadByte() != 0; + } + else + { + unk4 = reader.ReadInt32(); + } + + np.PauseTime = reader.ReadSingle(); + + // only used in rf2 parsing + float unk5 = 0.0f; + + if (RflUtils.IsRF2(rfl_version)) + { + // not convinced this is actually pause time in rf2 + if (np.PauseTime > 0) + { + unk5 = reader.ReadSingle(); + } + } + else + { + int numLinks = reader.ReadInt32(); + for (int L = 0; L < numLinks; L++) + { + int linkIdx = reader.ReadInt32(); + np.LinkIndices.Add(linkIdx); + } + } + + Logger.Dev(logSrc, + $"NavPoint[{i}] UID={np.UID}, HiddenInEditor={np.HiddenInEditor}, Height={np.Height}, " + + $"Position=({px},{py},{pz}), Radius={np.Radius}, Type={np.Type}, Directional={np.Directional}, " + + $"Cover={np.Cover}, Hide={np.Hide}, Crunch={np.Crunch}, PauseTime={np.PauseTime}, " + + $"InitialLinks=[{string.Join(",", np.LinkIndices)}]" + + $" unk1={unk1}, unk2={unk2}, unk3={unk3}, unk4={unk4}, unk5={unk5}" + ); + + tempPoints.Add(np); + } + + // not exported in rfg anyway + /* + // Read numPoints nav_point_connections arrays + for (int i = 0; i < numPoints; i++) + { + byte numIdx = reader.ReadByte(); + var connections = new List(numIdx); + for (int c = 0; c < numIdx; c++) + { + int idx = reader.ReadInt32(); + connections.Add(idx); + } + + tempPoints[i].LinkIndices.AddRange(connections); + Logger.Dev(logSrc, + $"NavPointConnections[{i}].Count = {numIdx}; Connections=[{string.Join(",", connections)}]" + ); + } + */ + return tempPoints; + } + } + + public static class RflLightmapParser + { + private const string logSrc = "RflLightmapParser"; + + public static List ParseLightmapsFromRfl(BinaryReader reader, long sectionEnd) + { + var lightmaps = new List(); + + int numLightmaps = reader.ReadInt32(); + Logger.Dev(logSrc, $"Found {numLightmaps} lightmap(s)."); + + for (int i = 0; i < numLightmaps; i++) + { + var lm = new Lightmap(); + + // Read width and height + lm.Width = reader.ReadInt32(); + lm.Height = reader.ReadInt32(); + Logger.Dev(logSrc, $"Lightmap[{i}] dimensions: {lm.Width}×{lm.Height}"); + + // Compute total byte count (24 bpp → 3 bytes per pixel) + int pixelCount = lm.Width * lm.Height; + int byteCount = pixelCount * 3; + + // Sanity check + long bytesRemainingInSection = sectionEnd - reader.BaseStream.Position; + if (bytesRemainingInSection < byteCount) + { + Logger.Warn(logSrc, $"Section ended prematurely while reading lightmap[{i}]: " + + $"needed {byteCount} bytes but only {bytesRemainingInSection} remain. Clamping to available."); + byteCount = (int)bytesRemainingInSection; + } + + // Read raw RGB bytes + lm.PixelData = reader.ReadBytes(byteCount); + + // Store and log + Logger.Dev(logSrc, $"Read {lm.PixelData.Length} bytes of lightmap[{i}]."); + lightmaps.Add(lm); + } + + return lightmaps; + } + } + + public static class RflParticleEmitterParser + { + private const string logSrc = "RflParticleEmitterParser"; + + public static List ParseParticleEmittersFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) + { + var emitters = new List(); + + int count = reader.ReadInt32(); + Logger.Warn(logSrc, $"Particle emitters count: {count}"); + for (int i = 0; i < count; i++) + { + var e = new ParticleEmitter(); + + e.UID = reader.ReadInt32(); + Logger.Warn(logSrc, $"Emitter[{i}].UID = {e.UID}"); + + e.ClassName = ReadVString(reader); + Logger.Warn(logSrc, $"Emitter[{i}].ClassName = \"{e.ClassName}\""); + + // Position (vec3) + e.Position = new Vector3( + reader.ReadSingle(), + reader.ReadSingle(), + reader.ReadSingle() + ); + Logger.Warn(logSrc, $"Emitter[{i}].Position = {e.Position}"); + + // Rotation (mat3) + float m00 = reader.ReadSingle(), m01 = reader.ReadSingle(), m02 = reader.ReadSingle(); + float m10 = reader.ReadSingle(), m11 = reader.ReadSingle(), m12 = reader.ReadSingle(); + float m20 = reader.ReadSingle(), m21 = reader.ReadSingle(), m22 = reader.ReadSingle(); + e.Rotation = new Matrix4x4( + m00, m01, m02, 0f, + m10, m11, m12, 0f, + m20, m21, m22, 0f, + 0f, 0f, 0f, 1f + ); + Logger.Warn(logSrc, $"Emitter[{i}].Rotation = [ [{m00}, {m01}, {m02}], [{m10}, {m11}, {m12}], [{m20}, {m21}, {m22}] ]"); + + e.ScriptName = ReadVString(reader); + Logger.Warn(logSrc, $"Emitter[{i}].ScriptName = \"{e.ScriptName}\""); + + e.HiddenInEditor = reader.ReadByte() != 0; + Logger.Warn(logSrc, $"Emitter[{i}].HiddenInEditor = {e.HiddenInEditor}"); + + e.Shape = (ParticleEmitterShape)reader.ReadInt32(); + Logger.Warn(logSrc, $"Emitter[{i}].Shape = {e.Shape}"); + + e.SphereRadius = reader.ReadSingle(); + Logger.Warn(logSrc, $"Emitter[{i}].SphereRadius = {e.SphereRadius}"); + e.PlaneWidth = reader.ReadSingle(); + Logger.Warn(logSrc, $"Emitter[{i}].PlaneWidth = {e.PlaneWidth}"); + e.PlaneDepth = reader.ReadSingle(); + Logger.Warn(logSrc, $"Emitter[{i}].PlaneDepth = {e.PlaneDepth}"); + + e.Texture = ReadVString(reader); + Logger.Warn(logSrc, $"Emitter[{i}].Texture = \"{e.Texture}\""); + + e.SpawnDelay = reader.ReadSingle(); + Logger.Warn(logSrc, $"Emitter[{i}].SpawnDelay = {e.SpawnDelay}"); + e.SpawnRandomize = reader.ReadSingle(); + Logger.Warn(logSrc, $"Emitter[{i}].SpawnRandomize = {e.SpawnRandomize}"); + + e.Velocity = reader.ReadSingle(); + Logger.Warn(logSrc, $"Emitter[{i}].Velocity = {e.Velocity}"); + e.VelocityRandomize = reader.ReadSingle(); + Logger.Warn(logSrc, $"Emitter[{i}].VelocityRandomize = {e.VelocityRandomize}"); + + e.Acceleration = reader.ReadSingle(); + Logger.Warn(logSrc, $"Emitter[{i}].Acceleration = {e.Acceleration}"); + + e.Decay = reader.ReadSingle(); + Logger.Warn(logSrc, $"Emitter[{i}].Decay = {e.Decay}"); + e.DecayRandomize = reader.ReadSingle(); + Logger.Warn(logSrc, $"Emitter[{i}].DecayRandomize = {e.DecayRandomize}"); + + e.ParticleRadius = reader.ReadSingle(); + Logger.Warn(logSrc, $"Emitter[{i}].ParticleRadius = {e.ParticleRadius}"); + e.ParticleRadiusRandomize = reader.ReadSingle(); + Logger.Warn(logSrc, $"Emitter[{i}].ParticleRadiusRandomize = {e.ParticleRadiusRandomize}"); + + e.GrowthRate = reader.ReadSingle(); + Logger.Warn(logSrc, $"Emitter[{i}].GrowthRate = {e.GrowthRate}"); + e.GravityMultiplier = reader.ReadSingle(); + Logger.Warn(logSrc, $"Emitter[{i}].GravityMultiplier = {e.GravityMultiplier}"); + e.RandomDirection = reader.ReadSingle(); + Logger.Warn(logSrc, $"Emitter[{i}].RandomDirection = {e.RandomDirection}"); + + // Colors (RGBA) + e.ParticleColor = new Vector4( + reader.ReadByte() / 255f, + reader.ReadByte() / 255f, + reader.ReadByte() / 255f, + reader.ReadByte() / 255f + ); + Logger.Warn(logSrc, $"Emitter[{i}].ParticleColor = {e.ParticleColor}"); + e.FadeToColor = new Vector4( + reader.ReadByte() / 255f, + reader.ReadByte() / 255f, + reader.ReadByte() / 255f, + reader.ReadByte() / 255f + ); + Logger.Warn(logSrc, $"Emitter[{i}].FadeToColor = {e.FadeToColor}"); + + // Flags + e.EmitterFlags = reader.ReadUInt32(); + Logger.Warn(logSrc, $"Emitter[{i}].EmitterFlags = 0x{e.EmitterFlags:X8}"); + e.ParticleFlags = reader.ReadUInt16(); + Logger.Warn(logSrc, $"Emitter[{i}].ParticleFlags = 0x{e.ParticleFlags:X4}"); + + // Four 4-bit values packed into a UInt16 + ushort nibblePack = reader.ReadUInt16(); + e.Stickiness = (byte)(nibblePack >> 12 & 0xF); + e.Bounciness = (byte)(nibblePack >> 8 & 0xF); + e.PushEffect = (byte)(nibblePack >> 4 & 0xF); + e.Swirliness = (byte)(nibblePack & 0xF); + Logger.Warn(logSrc, $"Emitter[{i}].Stickiness={e.Stickiness}, Bounciness={e.Bounciness}, PushEffect={e.PushEffect}, Swirliness={e.Swirliness}"); + + e.InitiallyOn = reader.ReadByte() != 0; + Logger.Warn(logSrc, $"Emitter[{i}].InitiallyOn = {e.InitiallyOn}"); + e.TimeOn = reader.ReadSingle(); + Logger.Warn(logSrc, $"Emitter[{i}].TimeOn = {e.TimeOn}"); + e.TimeOnRandomize = reader.ReadSingle(); + Logger.Warn(logSrc, $"Emitter[{i}].TimeOnRandomize = {e.TimeOnRandomize}"); + + e.TimeOff = reader.ReadSingle(); + Logger.Warn(logSrc, $"Emitter[{i}].TimeOff = {e.TimeOff}"); + e.TimeOffRandomize = reader.ReadSingle(); + Logger.Warn(logSrc, $"Emitter[{i}].TimeOffRandomize = {e.TimeOffRandomize}"); + + e.ActiveDistance = reader.ReadSingle(); + Logger.Warn(logSrc, $"Emitter[{i}].ActiveDistance = {e.ActiveDistance}"); + + emitters.Add(e); + } + + // Skip any padding or unknown data + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + return emitters; + } + } + + public static class RflLevelInfoParser + { + private const string logSrc = "RflLevelInfoParser"; + + public static void ParseLevelInfoFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) + { + int unknown = reader.ReadInt32(); + Logger.Warn(logSrc, $"Unk value: {unknown}"); + + string levelName = ReadVString(reader); + Logger.Warn(logSrc, $"Level name: \"{levelName}\""); + + string author = ReadVString(reader); + Logger.Warn(logSrc, $"Level author: \"{author}\""); + + string date = ReadVString(reader); + Logger.Warn(logSrc, $"Save date/time: \"{date}\""); + + byte hasMovers = reader.ReadByte(); + Logger.Warn(logSrc, $"Has movers: {hasMovers}"); + + byte multiplayer = reader.ReadByte(); + Logger.Warn(logSrc, $"Multiplayer level: {multiplayer}"); + + // Editor view layouts + for (int i = 0; i < 4; i++) + { + long cfgStart = reader.BaseStream.Position; + int viewType = reader.ReadInt32(); + Logger.Debug(logSrc, $"editor_view_config[{i}].view_type = {viewType}"); + + if (viewType == (int)EditorViewType.FreeLook) + { + // pos_3d (vec3) + float x = reader.ReadSingle(); + float y = reader.ReadSingle(); + float z = reader.ReadSingle(); + Logger.Debug(logSrc, $"editor_view_config[{i}].pos_3d = ({x}, {y}, {z})"); + } + else + { + // pos_2d (4 × f4) + float[] pos2d = new float[4]; + for (int j = 0; j < 4; j++) + pos2d[j] = reader.ReadSingle(); + Logger.Debug(logSrc, $"editor_view_config[{i}].pos_2d = [{string.Join(", ", pos2d)}]"); + } + + // rot (mat3) + float[,] m = new float[3, 3]; + for (int row = 0; row < 3; row++) + for (int col = 0; col < 3; col++) + m[row, col] = reader.ReadSingle(); + Logger.Debug(logSrc, + $"editor_view_config[{i}].rot = [\n" + + $" [{m[0, 0]}, {m[0, 1]}, {m[0, 2]}],\n" + + $" [{m[1, 0]}, {m[1, 1]}, {m[1, 2]}],\n" + + $" [{m[2, 0]}, {m[2, 1]}, {m[2, 2]}]\n" + + $"]"); + + long cfgSize = reader.BaseStream.Position - cfgStart; + Logger.Debug(logSrc, $"editor_view_config[{i}] size = {cfgSize} bytes"); + } + + // prints garbage data in this section at the end of RF2 sections, but not RF1 + /*int extraIndex = 0; + while (reader.BaseStream.Position < sectionEnd) + { + var b = reader.ReadByte(); + Logger.Warn(logSrc, $"level_info extra byte[{extraIndex}] = 0x{b:X2}"); + extraIndex++; + }*/ + } + } + + public static class RflClimbingRegionParser + { + private const string logSrc = "RflClimbingRegionParser"; + + public static List ParseClimbingRegionsFromRfl(BinaryReader reader, long sectionEnd) + { + var regions = new List(); + + int count = reader.ReadInt32(); + Logger.Dev(logSrc, $"Reading {count} climbing regions…"); + + for (int i = 0; i < count; i++) + { + var cr = new ClimbingRegion(); + + cr.UID = reader.ReadInt32(); + cr.ClassName = ReadVString(reader); + cr.Position = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + + // mat3 comes in forward, right, up order → embed into Matrix4x4 + var f = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + var r = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + var u = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + cr.Rotation = new Matrix4x4( + r.X, r.Y, r.Z, 0f, + u.X, u.Y, u.Z, 0f, + f.X, f.Y, f.Z, 0f, + 0f, 0f, 0f, 1f); + + cr.ScriptName = ReadVString(reader); + cr.HiddenInEditor = reader.ReadByte() != 0; + cr.Type = reader.ReadInt32(); + cr.Extents = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + + Logger.Dev(logSrc, + $"ClimbingRegion[{i}] UID={cr.UID}, Class=\"{cr.ClassName}\", Pos={cr.Position}, " + + $"Rot=\n{cr.Rotation}, Script=\"{cr.ScriptName}\", Hidden={cr.HiddenInEditor}, " + + $"Type={cr.Type}, Extents={cr.Extents}"); + + regions.Add(cr); + } + + // ensure we position at the end no matter what + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + return regions; + } + } + + public static class RflItemParser + { + private const string logSrc = "RflItemParser"; + + public static List ParseItemsFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) + { + var items = new List(); + int count = reader.ReadInt32(); + Logger.Dev(logSrc, $"Reading {count} items…"); + + for (int i = 0; i < count; i++) + { + var it = new RflItem(); + + it.UID = reader.ReadInt32(); + Logger.Dev(logSrc, $"Item[{i}] UID = {it.UID}"); + + it.ClassName = ReadVString(reader); + Logger.Dev(logSrc, $"Item[{i}] ClassName = \"{it.ClassName}\""); + + it.Position = new Vector3( + reader.ReadSingle(), + reader.ReadSingle(), + reader.ReadSingle() + ); + Logger.Dev(logSrc, $"Item[{i}] Position = {it.Position}"); + + // 3×3 rotation matrix → Matrix4x4 + var m00 = reader.ReadSingle(); var m01 = reader.ReadSingle(); var m02 = reader.ReadSingle(); + var m10 = reader.ReadSingle(); var m11 = reader.ReadSingle(); var m12 = reader.ReadSingle(); + var m20 = reader.ReadSingle(); var m21 = reader.ReadSingle(); var m22 = reader.ReadSingle(); + it.Rotation = new Matrix4x4( + m00, m01, m02, 0, + m10, m11, m12, 0, + m20, m21, m22, 0, + 0, 0, 0, 1 + ); + Logger.Dev(logSrc, $"Item[{i}] Rotation = {it.Rotation}"); + + it.ScriptName = ReadVString(reader); + Logger.Dev(logSrc, $"Item[{i}] ScriptName = \"{it.ScriptName}\""); + + it.HiddenInEditor = reader.ReadByte() != 0; + Logger.Dev(logSrc, $"Item[{i}] HiddenInEditor = {it.HiddenInEditor}"); + + it.Count = reader.ReadInt32(); + Logger.Dev(logSrc, $"Item[{i}] Count = {it.Count}"); + + it.RespawnTime = reader.ReadInt32(); + Logger.Dev(logSrc, $"Item[{i}] RespawnTime = {it.RespawnTime}"); + + it.TeamID = reader.ReadInt32(); + Logger.Dev(logSrc, $"Item[{i}] TeamID = {it.TeamID}"); + + // unknown bytes, found only in RF2 rfls + // 255, 255, 255, 255, 0, 0 in every instance I have found + if (rfl_version == 0x127) + { + var unk1 = reader.ReadByte(); + var unk2 = reader.ReadByte(); + var unk3 = reader.ReadByte(); + var unk4 = reader.ReadByte(); + var unk5 = reader.ReadByte(); + var unk6 = reader.ReadByte(); + Logger.Dev(logSrc, $"unk RF2-specific bytes {unk1}, {unk2}, {unk3}, {unk4}, {unk5}, {unk6}"); + } + + items.Add(it); + } + + return items; + } + } + public static class RflTriggerParser + { + private const string logSrc = "RflTriggerParser"; + + public static List ParseTriggersFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) + { + var list = new List(); + int count = reader.ReadInt32(); + Logger.Dev(logSrc, $"Reading {count} triggers…"); + + for (int i = 0; i < count; i++) + { + var t = new Trigger(); + + t.UID = reader.ReadInt32(); + Logger.Dev(logSrc, $"Trigger[{i}] UID = {t.UID}"); + + t.ScriptName = ReadVString(reader); + Logger.Dev(logSrc, $"Trigger[{i}] ScriptName = \"{t.ScriptName}\""); + + t.HiddenInEditor = reader.ReadByte() != 0; + Logger.Dev(logSrc, $"Trigger[{i}] HiddenInEditor = {t.HiddenInEditor}"); + + t.Shape = (TriggerShape)reader.ReadInt32(); + Logger.Dev(logSrc, $"Trigger[{i}] Shape = {t.Shape}"); + + t.ResetsAfter = reader.ReadSingle(); + Logger.Dev(logSrc, $"Trigger[{i}] ResetsAfter = {t.ResetsAfter:F2}"); + + t.ResetsTimes = reader.ReadInt32(); + Logger.Dev(logSrc, $"Trigger[{i}] ResetsTimes = {t.ResetsTimes}"); + + t.UseKeyRequired = reader.ReadByte() != 0; + Logger.Dev(logSrc, $"Trigger[{i}] UseKeyRequired = {t.UseKeyRequired}"); + + if (RflUtils.IsRF1(rfl_version)) + { + t.KeyName = ReadVString(reader); + } + else + { + t.KeyName = ReadVStringPlain(reader); // rf2 uses a plain vstring for key name + } + Logger.Dev(logSrc, $"Trigger[{i}] KeyName = \"{t.KeyName}\""); + + if (RflUtils.IsRF1(rfl_version)) + { + t.WeaponActivates = reader.ReadByte() != 0; + Logger.Dev(logSrc, $"Trigger[{i}] WeaponActivates = {t.WeaponActivates}"); + } + + t.ActivatedBy = (TriggerActivatedBy)reader.ReadByte(); + Logger.Dev(logSrc, $"Trigger[{i}] ActivatedBy = {t.ActivatedBy}"); + + if (RflUtils.IsRF1(rfl_version)) + { + t.IsNpc = reader.ReadByte() != 0; + Logger.Dev(logSrc, $"Trigger[{i}] IsNpc = {t.IsNpc}"); + + t.IsAuto = reader.ReadByte() != 0; + Logger.Dev(logSrc, $"Trigger[{i}] IsAuto = {t.IsAuto}"); + + t.InVehicle = reader.ReadByte() != 0; + Logger.Dev(logSrc, $"Trigger[{i}] InVehicle = {t.InVehicle}"); + } + + // position + float px = reader.ReadSingle(); + float py = reader.ReadSingle(); + float pz = reader.ReadSingle(); + t.Position = new Vector3(px, py, pz); + Logger.Dev(logSrc, $"Trigger[{i}] Position = ({px:F2}, {py:F2}, {pz:F2})"); + + if (t.Shape == TriggerShape.Sphere) + { + t.SphereRadius = reader.ReadSingle(); + Logger.Dev(logSrc, $"Trigger[{i}] SphereRadius = {t.SphereRadius:F2}"); + } + else + { + // rotation + float m00 = reader.ReadSingle(), m01 = reader.ReadSingle(), m02 = reader.ReadSingle(); + float m10 = reader.ReadSingle(), m11 = reader.ReadSingle(), m12 = reader.ReadSingle(); + float m20 = reader.ReadSingle(), m21 = reader.ReadSingle(), m22 = reader.ReadSingle(); + t.Rotation = new Matrix4x4( + m00, m01, m02, 0, + m10, m11, m12, 0, + m20, m21, m22, 0, + 0, 0, 0, 1 + ); + Logger.Dev(logSrc, $"Trigger[{i}] Rotation = [ [{m00:F2},{m01:F2},{m02:F2}], [{m10:F2},{m11:F2},{m12:F2}], [{m20:F2},{m21:F2},{m22:F2}] ]"); + + t.BoxHeight = reader.ReadSingle(); + t.BoxWidth = reader.ReadSingle(); + t.BoxDepth = reader.ReadSingle(); + Logger.Dev(logSrc, $"Trigger[{i}] BoxExtents = H:{t.BoxHeight:F2}, W:{t.BoxWidth:F2}, D:{t.BoxDepth:F2}"); + + if (RflUtils.IsRF1(rfl_version)) + { + t.OneWay = reader.ReadByte() != 0; + Logger.Dev(logSrc, $"Trigger[{i}] OneWay = {t.OneWay}"); + } + } + + t.AirlockRoomUID = reader.ReadInt32(); + Logger.Dev(logSrc, $"Trigger[{i}] AirlockRoomUID = {t.AirlockRoomUID}"); + + t.AttachedToUID = reader.ReadInt32(); + Logger.Dev(logSrc, $"Trigger[{i}] AttachedToUID = {t.AttachedToUID}"); + + t.UseClutterUID = reader.ReadInt32(); + Logger.Dev(logSrc, $"Trigger[{i}] UseClutterUID = {t.UseClutterUID}"); + + t.Disabled = reader.ReadByte() != 0; + Logger.Dev(logSrc, $"Trigger[{i}] Disabled = {t.Disabled}"); + + t.ButtonActiveTime = reader.ReadSingle(); + Logger.Dev(logSrc, $"Trigger[{i}] ButtonActiveTime = {t.ButtonActiveTime:F2}"); + + if (RflUtils.IsRF1(rfl_version)) + { + t.InsideTime = reader.ReadSingle(); + Logger.Dev(logSrc, $"Trigger[{i}] InsideTime = {t.InsideTime:F2}"); + } + else + { + reader.ReadSingle(); + // This value is super high in RF2, so isn't InsideTime. May not even be a float. Reading it is needed for alignment + } + + // RF2 omits a number of fields that are in RF1 triggers and appears to consolidate them into flags and flags2 + // Many of these are unknown. Some are likely flags present in RF1 and some new in RF2 + if (RflUtils.IsRF2(rfl_version)) + { + reader.ReadByte(); + ushort flags = reader.ReadUInt16(); + Logger.Dev(logSrc, $"Trigger[{i}] with Script Name {t.ScriptName} Flags = 0x{flags:X4}"); + } + + if (rfl_version >= 0xB1 && !(RflUtils.IsRF2(rfl_version))) + { + t.Team = (TriggerTeam)reader.ReadInt32(); + Logger.Dev(logSrc, $"Trigger[{i}] Team = {t.Team}"); + } + else if (RflUtils.IsRF2(rfl_version)) + { + uint flags2 = reader.ReadUInt32(); + //t.StartActive = (flags & 0x1) != 0; + //t.OneShot = (flags & 0x2) != 0; + Logger.Dev(logSrc, $"Trigger[{i}] with Script Name {t.ScriptName} Flags2 = 0x{flags2:X8}"); + + // bit 0x0008 = IsAuto in RF2 + t.IsAuto = (flags2 & 0x0008) != 0; + Logger.Dev(logSrc, $"Trigger[{i}] with Script Name {t.ScriptName} IsAuto = {t.IsAuto}"); + + // unknown flags in RF2 triggers + var F2unk1 = (flags2 & 0x0001) != 0; + var F2unk2 = (flags2 & 0x0002) != 0; + var F2unk3 = (flags2 & 0x0010) != 0; + var F2unk4 = (flags2 & 0x0020) != 0; + var F2unk5 = (flags2 & 0x0040) != 0; + var F2unk6 = (flags2 & 0x0080) != 0; + var F2unk7 = (flags2 & 0x0100) != 0; + + Logger.Dev(logSrc, $"Trigger[{i}] with Script Name {t.ScriptName} unk1 {F2unk1}, unk2 {F2unk2}, unk3 {F2unk3}, unk4 {F2unk4}, unk5 {F2unk5}, unk6 {F2unk6}, unk7 {F2unk7}"); + } + + int numLinks = reader.ReadInt32(); + Logger.Dev(logSrc, $"Trigger[{i}] LinksCount = {numLinks}"); + t.Links = new List(numLinks); + for (int j = 0; j < numLinks; j++) + { + int link = reader.ReadInt32(); + t.Links.Add(link); + Logger.Dev(logSrc, $"Trigger[{i}] Link[{j}] = {link}"); + } + + list.Add(t); + } + + return list; + } + } + + public static class RflPushRegionParser + { + private const string logSrc = "RflPushRegionParser"; + + public static List ParsePushRegionsFromRfl(BinaryReader reader, long sectionEnd) + { + var regions = new List(); + int count = reader.ReadInt32(); + Logger.Dev(logSrc, $"Reading {count} push regions…"); + + for (int i = 0; i < count; i++) + { + var pr = new PushRegion(); + + pr.UID = reader.ReadInt32(); + pr.ClassName = ReadVString(reader); + pr.Position = new Vector3( + reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle() + ); + + // rotation matrix + float r00 = reader.ReadSingle(), r01 = reader.ReadSingle(), r02 = reader.ReadSingle(); + float r10 = reader.ReadSingle(), r11 = reader.ReadSingle(), r12 = reader.ReadSingle(); + float r20 = reader.ReadSingle(), r21 = reader.ReadSingle(), r22 = reader.ReadSingle(); + pr.Rotation = new Matrix4x4( + r00, r01, r02, 0, + r10, r11, r12, 0, + r20, r21, r22, 0, + 0, 0, 0, 1 + ); + + pr.ScriptName = ReadVString(reader); + pr.HiddenInEditor = reader.ReadByte() != 0; + + pr.Shape = (PushRegionShape)reader.ReadInt32(); + if (pr.Shape == PushRegionShape.Sphere) + { + pr.Radius = reader.ReadSingle(); + } + else + { + // extents = Vector3 + pr.Extents = new Vector3( + reader.ReadSingle(), + reader.ReadSingle(), + reader.ReadSingle() + ); + } + + pr.Strength = reader.ReadSingle(); + + // flags bitfield + uint rawFlags = reader.ReadUInt16(); // 16 bits + pr.JumpPad = (rawFlags & 0x40) != 0; + pr.DoesntAffectPlayer = (rawFlags & 0x20) != 0; + pr.Radial = (rawFlags & 0x10) != 0; + pr.GrowsTowardsBoundary = (rawFlags & 0x08) != 0; + pr.GrowsTowardsCenter = (rawFlags & 0x04) != 0; + pr.Grounded = (rawFlags & 0x02) != 0; + pr.MassIndependent = (rawFlags & 0x01) != 0; + + pr.Turbulence = reader.ReadUInt16(); + + // log out everything + Logger.Debug(logSrc, + $"PushRegion[{i}] UID={pr.UID}, Class=\"{pr.ClassName}\", Pos={pr.Position}, " + + $"Script=\"{pr.ScriptName}\", Hidden={pr.HiddenInEditor}, Shape={pr.Shape}, " + + (pr.Shape == PushRegionShape.Sphere + ? $"Radius={pr.Radius}" + : $"Extents={pr.Extents}" + ) + + $", Strength={pr.Strength}, Flags=0x{rawFlags:X4} (JumpPad={pr.JumpPad}, " + + $"NoPlayer={pr.DoesntAffectPlayer}, Radial={pr.Radial}, " + + $"BoundaryGrow={pr.GrowsTowardsBoundary}, CenterGrow={pr.GrowsTowardsCenter}, " + + $"Grounded={pr.Grounded}, MassIndep={pr.MassIndependent}), Turbulence={pr.Turbulence}" + ); + + regions.Add(pr); + } + + return regions; + } + } + + public static class RflEventParser + { + private const string logSrc = "RflEventParser"; + + public static List ParseEventsRF2(BinaryReader reader, long sectionEnd, int rfl_version) + { + int count = reader.ReadInt32(); + Logger.Warn(logSrc, $"RF2 event count = {count}"); + + var events = new List(count); + + for (int i = 0; i < count; i++) + { + long startPos = reader.BaseStream.Position; + var ev = new RflEvent(); + + // UID + ev.UID = reader.ReadInt32(); + Logger.Warn(logSrc, $"Event[{i}] UID = {ev.UID}"); + + // class name + ev.ClassName = ReadVString(reader); + Logger.Warn(logSrc, $"Event[{i}] ClassName = \"{ev.ClassName}\""); + + // position + float px = reader.ReadSingle(); + float py = reader.ReadSingle(); + float pz = reader.ReadSingle(); + ev.Position = new Vector3(px, py, pz); + Logger.Warn(logSrc, $"Event[{i}] Position = ({px:F2}, {py:F2}, {pz:F2})"); + + // script name + ev.ScriptName = ReadVString(reader); + Logger.Warn(logSrc, $"Event[{i}] ScriptName = \"{ev.ScriptName}\""); + + // hidden in editor + ev.HiddenInEditor = reader.ReadByte() != 0; + Logger.Warn(logSrc, $"Event[{i}] HiddenInEditor = {ev.HiddenInEditor}"); + + // delay + ev.Delay = reader.ReadSingle(); + Logger.Warn(logSrc, $"Event[{i}] Delay = {ev.Delay}"); + + // 8 4-byte fields: + var unk0 = reader.ReadInt32(); // -1, 0, or 1 everywhere that I've seen, unsure of significance + var unk1 = reader.ReadInt32(); // typically 0, unsure of significance + var mb1 = reader.ReadByte() != 0; // top candidate for bool1 + var mb2 = reader.ReadByte() != 0; // top candidate for bool2 + var unk2 = reader.ReadInt32(); // confident this is int1 + var unk3 = reader.ReadInt32(); // confident this is int2 + var unk4 = reader.ReadInt32(); // int maybe? was -1 on a Delay + var unk5 = reader.ReadInt32(); // int maybe? was -1 on a Delay + var unk6 = reader.ReadSingle(); // confident this is float1 + var unk7 = reader.ReadSingle(); // top candidate for float2 + var unk8 = reader.ReadInt32(); // 00 00 00 00 everywhere that I've seen + + // str1 + ev.Str1 = ReadVString(reader); // confident of this + Logger.Warn(logSrc, $"Event[{i}] Str1 = \"{ev.Str1}\""); + + // str2 + ev.Str2 = ReadVString(reader); // confident of this + Logger.Warn(logSrc, $"Event[{i}] Str2 = \"{ev.Str2}\""); + + //reader.ReadByte(); + //reader.ReadByte(); + var unk9 = ReadVString(reader); // maybe a third str? 00 00 everywhere that I've seen + + ev.Bool1 = mb1; + ev.Bool2 = mb2; + ev.Int1 = unk2; + ev.Int2 = unk3; + ev.Float1 = unk6; + ev.Float2 = unk7; + + Logger.Warn(logSrc, $"Event[{i}] Bool1 = {ev.Bool1}"); + Logger.Warn(logSrc, $"Event[{i}] Bool2 = {ev.Bool2}"); + Logger.Warn(logSrc, $"Event[{i}] Int1 = {ev.Int1}"); + Logger.Warn(logSrc, $"Event[{i}] Int2 = {ev.Int2}"); + Logger.Warn(logSrc, $"Event[{i}] Float1 = {ev.Float1}"); + Logger.Warn(logSrc, $"Event[{i}] Float2 = {ev.Float2}"); + + Logger.Warn(logSrc, $"Event[{i}] unk0 {unk0}, unk1 {unk1}, mb1 {mb1}, mb2 {mb2}, unk2 {unk2}, unk3 {unk3}, unk4 {unk4}, unk5 {unk5}, unk6 {unk6}, unk7 {unk7}, unk8 {unk8}, unk9 {unk9}"); + + // Links + int numLinks = reader.ReadInt32(); + Logger.Warn(logSrc, $"Event[{i}] Links count = {numLinks}"); + + ev.Links = new List(numLinks); + for (int j = 0; j < numLinks; j++) + { + int link = reader.ReadInt32(); + ev.Links.Add(link); + Logger.Warn(logSrc, $"Event[{i}] Link[{j}] = {link}"); + } + + if (ev.ClassName == "Play_Explosion" || ev.ClassName == "Teleport") + { + ev.HasRotation = true; // used by exporter + float m00 = reader.ReadSingle(), m01 = reader.ReadSingle(), m02 = reader.ReadSingle(); + float m10 = reader.ReadSingle(), m11 = reader.ReadSingle(), m12 = reader.ReadSingle(); + float m20 = reader.ReadSingle(), m21 = reader.ReadSingle(), m22 = reader.ReadSingle(); + ev.Rotation = new Matrix4x4( + m00, m01, m02, 0, + m10, m11, m12, 0, + m20, m21, m22, 0, + 0, 0, 0, 1 + ); + } + + // color in editor + ev.RawColor = reader.ReadUInt32(); + Logger.Warn(logSrc, $"Event[{i}] RawColor = 0x{ev.RawColor:X8}"); + + // add to events list + events.Add(ev); + + // sanity check + long consumed = reader.BaseStream.Position - startPos; + Logger.Warn(logSrc, $"Event[{i}] bytes consumed = {consumed}"); + } + + // fast-forward to end of section + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + return events; + } + + public static List ParseEvents(BinaryReader reader, long sectionEnd, int rfl_version) + { + if (rfl_version == 0x127) // 295 + { + Logger.Warn(logSrc, "RFL version 0x127 (295) is not yet supported for event parsing. Skipping events."); + + int count2 = reader.ReadInt32(); + Logger.Warn(logSrc, $"Reading {count2} events…"); + + for (int i = 0; i < count2; i++) + { + int extraIndex = 0; + while (reader.BaseStream.Position < sectionEnd) + { + var b = reader.ReadByte(); + //Logger.Warn(logSrc, $"event byte[{extraIndex}] = 0x{b:X2}"); + extraIndex++; + } + } + + return new List(); + } + + var events = new List(); + int count = reader.ReadInt32(); + Logger.Dev(logSrc, $"Reading {count} events…"); + + for (int i = 0; i < count; i++) + { + var ev = new RflEvent(); + + ev.UID = reader.ReadInt32(); + ev.ClassName = ReadVString(reader); + ev.Position = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + ev.ScriptName = ReadVString(reader); + ev.HiddenInEditor = reader.ReadByte() != 0; + ev.Delay = reader.ReadSingle(); + ev.Bool1 = reader.ReadByte() != 0; + ev.Bool2 = reader.ReadByte() != 0; + ev.Int1 = reader.ReadInt32(); + ev.Int2 = reader.ReadInt32(); + ev.Float1 = reader.ReadSingle(); + ev.Float2 = reader.ReadSingle(); + ev.Str1 = ReadVString(reader); + ev.Str2 = ReadVString(reader); + + // links (uid_list) + int numLinks = reader.ReadInt32(); + if (numLinks < 0 || reader.BaseStream.Position + numLinks * 4L > sectionEnd) + { + Logger.Warn(logSrc, $"Suspicious link count {numLinks} in event[{i}] – clamping to 0"); + numLinks = 0; + } + ev.Links = new List(numLinks); + for (int j = 0; j < numLinks; j++) + ev.Links.Add(reader.ReadInt32()); + + if (rfl_version >= 0x91 && (ev.ClassName == "Teleport" || ev.ClassName == "Play_Vclip" || ev.ClassName == "Teleport_Player") + || rfl_version >= 0x98 && ev.ClassName == "Alarm" + || rfl_version >= 0x12C && (ev.ClassName == "AF_Teleport_Player" || ev.ClassName == "Clone_Entity") + || rfl_version >= 0x12D && ev.ClassName == "Anchor_Marker_Orient") + { + ev.HasRotation = true; // used by exporter + float m00 = reader.ReadSingle(), m01 = reader.ReadSingle(), m02 = reader.ReadSingle(); + float m10 = reader.ReadSingle(), m11 = reader.ReadSingle(), m12 = reader.ReadSingle(); + float m20 = reader.ReadSingle(), m21 = reader.ReadSingle(), m22 = reader.ReadSingle(); + ev.Rotation = new Matrix4x4( + m00, m01, m02, 0, + m10, m11, m12, 0, + m20, m21, m22, 0, + 0, 0, 0, 1 + ); + } + + if (rfl_version >= 0xB0) + { + //reader.BaseStream.Seek(4, SeekOrigin.Current); // color + ev.RawColor = reader.ReadUInt32(); + Logger.Dev(logSrc, $"Event[{i}] RawColor = 0x{ev.RawColor:X8}"); + } + + Logger.Dev(logSrc, + $"Event[{i}] UID={ev.UID}, Class=\"{ev.ClassName}\", Pos={ev.Position}, " + + $"Script=\"{ev.ScriptName}\", Hidden={ev.HiddenInEditor}, Delay={ev.Delay}, " + + $"Bool1={ev.Bool1}, Bool2={ev.Bool2}, Int1={ev.Int1}, Int2={ev.Int2}, " + + $"Float1={ev.Float1}, Float2={ev.Float2}, Str1=\"{ev.Str1}\", Str2=\"{ev.Str2}\", " + + $"Links=[{string.Join(", ", ev.Links)}]" + ); + + if (ev != null) + events.Add(ev); + } + + // skip to end of this section + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + return events; + } + } + + public static class RflMpRespawnPointParser + { + private const string logSrc = "RflMpRespawnPointParser"; + + public static List ParseMpRespawnPoints(BinaryReader reader, long sectionEnd) + { + var list = new List(); + int count = reader.ReadInt32(); + Logger.Dev(logSrc, $"Reading {count} MP respawn points…"); + + for (int i = 0; i < count; i++) + { + var pt = new MpRespawnPoint(); + pt.UID = reader.ReadInt32(); + pt.Position = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + + // 3×3 rotation matrix → embed into Matrix4x4 + float m00 = reader.ReadSingle(), m01 = reader.ReadSingle(), m02 = reader.ReadSingle(); + float m10 = reader.ReadSingle(), m11 = reader.ReadSingle(), m12 = reader.ReadSingle(); + float m20 = reader.ReadSingle(), m21 = reader.ReadSingle(), m22 = reader.ReadSingle(); + pt.Rotation = new Matrix4x4( + m00, m01, m02, 0, + m10, m11, m12, 0, + m20, m21, m22, 0, + 0, 0, 0, 1 + ); + + pt.ScriptName = ReadVString(reader); + pt.HiddenInEditor = reader.ReadByte() != 0; + pt.TeamID = reader.ReadInt32(); + pt.RedTeam = reader.ReadByte() != 0; + pt.BlueTeam = reader.ReadByte() != 0; + pt.IsBot = reader.ReadByte() != 0; + + // print everything + Logger.Dev(logSrc, + $"MP Respawn[{i}] UID={pt.UID}, Pos={pt.Position}, Team={pt.TeamID} (R?{pt.RedTeam}/B?{pt.BlueTeam}), " + + $"Bot={pt.IsBot}, Hidden={pt.HiddenInEditor}, Script=\"{pt.ScriptName}\"" + ); + + list.Add(pt); + } + + // ensure we skip to end of section + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + return list; + } + } + + + public static class RflLevelPropertiesParser + { + private const string logSrc = "RflLevelPropertiesParser"; + + public static void ParseLevelPropertiesFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) + { + // geomod_texture + string geomodTexture = ReadVString(reader); + + // hardness + int hardness = reader.ReadInt32(); + + // ambient_color (RGBA) + byte ambR = reader.ReadByte(); + byte ambG = reader.ReadByte(); + byte ambB = reader.ReadByte(); + byte ambA = reader.ReadByte(); + + // directional_ambient_light + byte dirAmbient = reader.ReadByte(); + + // fog_color + byte fogR = reader.ReadByte(); + byte fogG = reader.ReadByte(); + byte fogB = reader.ReadByte(); + byte fogA = reader.ReadByte(); + + // fog near/far planes + float fogNear = reader.ReadSingle(); + float fogFar = reader.ReadSingle(); + Logger.Warn(logSrc, "Level properties:"); + Logger.Warn(logSrc, $" GeoMod texture: \"{geomodTexture}\""); + Logger.Warn(logSrc, $" GeoMod hardness: {hardness}"); + Logger.Warn(logSrc, $" Ambient light colour: R={ambR} G={ambG} B={ambB} A={ambA}"); + Logger.Warn(logSrc, $" Directional ambient light: {(dirAmbient != 0 ? "yes" : "no")}"); + Logger.Warn(logSrc, $" Distance fog colour: R={fogR} G={fogG} B={fogB} A={fogA}"); + Logger.Warn(logSrc, $" Distance fog near clip plane: {fogNear}"); + Logger.Warn(logSrc, $" Distance fog far clip plane: {fogFar}"); + + if (rfl_version == 0x127) // rf2 + { + uint unk1 = reader.ReadUInt32(); + uint unk2 = reader.ReadUInt32(); + float unk3 = reader.ReadSingle(); + float unk4 = reader.ReadSingle(); + float unk5 = reader.ReadSingle(); + byte unk1R = reader.ReadByte(), unk1G = reader.ReadByte(), unk1B = reader.ReadByte(), unk1A = reader.ReadByte(); + byte unk2R = reader.ReadByte(), unk2G = reader.ReadByte(), unk2B = reader.ReadByte(), unk2A = reader.ReadByte(); + float unk6 = reader.ReadSingle(); + SavedValues.maybeExposure = unk6 * 10.0f; + + Logger.Warn(logSrc, "Guesses at unknown RF2 level properties fields:"); + Logger.Warn(logSrc, $" Unk1 (flags?): 0x{unk1:X8}"); + Logger.Warn(logSrc, $" Unk2 (flags?): 0x{unk2:X8}"); + Logger.Warn(logSrc, $" maybeViewDistance: {unk3}"); + Logger.Warn(logSrc, $" maybeAmbientScale: {unk4}"); + Logger.Warn(logSrc, $" maybeSunAngleDeg: {unk5}"); + Logger.Warn(logSrc, $" maybeSkyColorRGBA: [{unk1R},{unk1G},{unk1B},{unk1A}]"); + Logger.Warn(logSrc, $" maybeSkyColorAltRGBA: [{unk2R},{unk2G},{unk2B},{unk2A}]"); + Logger.Warn(logSrc, $" maybeExposure: {unk6}, light multiplier {SavedValues.maybeExposure}"); + } + + // now read everything until we hit sectionEnd + int extraIndex = 0; + while (reader.BaseStream.Position < sectionEnd) + { + var b = reader.ReadSingle(); + Logger.Warn(logSrc, $"level_properties extra byte[{extraIndex}] = 0x{b:X2}"); + extraIndex++; + } + + } + } + + + static class RflLightParser + { + private const string logSrc = "RflLightParser"; + + public static List ParseLightsFromRfl(BinaryReader reader, long sectionEnd, int rfl_version) + { + var lights = new List(); + int numLights = reader.ReadInt32(); + Logger.Dev(logSrc, $"Reading {numLights} lights…"); + + for (int i = 0; i < numLights; i++) + { + var light = new Light(); + + light.UID = reader.ReadInt32(); + light.ClassName = ReadVString(reader); + light.Position = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + + // 3×3 rotation matrix → embed into Matrix4x4 + var m00 = reader.ReadSingle(); var m01 = reader.ReadSingle(); var m02 = reader.ReadSingle(); + var m10 = reader.ReadSingle(); var m11 = reader.ReadSingle(); var m12 = reader.ReadSingle(); + var m20 = reader.ReadSingle(); var m21 = reader.ReadSingle(); var m22 = reader.ReadSingle(); + light.Rotation = new Matrix4x4( + m00, m01, m02, 0, + m10, m11, m12, 0, + m20, m21, m22, 0, + 0, 0, 0, 1 + ); + + light.ScriptName = ReadVString(reader); + light.HiddenInEditor = reader.ReadByte() != 0; + + uint rawFlags = reader.ReadUInt32(); + light.Dynamic = (rawFlags & 0x1) != 0; + light.Fade = (rawFlags & 0x2) != 0; + light.ShadowCasting = (rawFlags & 0x4) != 0; + light.IsEnabled = (rawFlags & 0x8) != 0; + light.Type = (LightType)(rawFlags >> 4 & 0x3); + light.InitialState = (LightState)(rawFlags >> 8 & 0xF); + light.RuntimeShadow = (rawFlags & 0x2000) != 0; + + // RGBA + byte r = reader.ReadByte(); + byte g = reader.ReadByte(); + byte b = reader.ReadByte(); + byte a = reader.ReadByte(); + light.Color = new Vector4(r / 255f, g / 255f, b / 255f, a / 255f); + + light.Range = reader.ReadSingle(); + light.FOV = reader.ReadSingle(); + light.FOVDropoff = reader.ReadSingle(); + light.IntensityAtMaxRange = reader.ReadSingle(); + light.DropoffType = reader.ReadInt32(); + light.TubeLightWidth = reader.ReadSingle(); + + if (rfl_version != 0x127) // 295 + { + light.OnIntensity = reader.ReadSingle(); + } + else + { + float I0 = reader.ReadSingle(); // the raw RF2 OnIntensity + light.OnIntensity = I0; + light.Range *= 2; + /*float R = light.Range; + switch (light.DropoffType) + { + case 0: + // linear: I(d) = I0 * (1 – d/R) + // so I0_file = I_linear_at_d=0 / R → our engine wants I(d=0)=I0*R + light.OnIntensity = I0 * R; + break; + + case 1: + // squared: I(d) = I0 * (1 – d/R)^2 + // invert so total “power” matches at d=0 → scale by R^2 + light.OnIntensity = I0 * R * R; + break; + + case 2: + // cosine: I(d) = I0 * cos(d/R * (π/2)) + // invert so at d=0 you get I0 * 1 → no extra scaling, but + // if your target engine expects I(d=R)=0, you leave it unmodified + light.OnIntensity = I0; + break; + + case 3: + // sqrt: I(d) = I0 * sqrt(1 – d/R) + // invert so I(d=0) = I0 * 1 → must scale by √R + light.OnIntensity = I0 * MathF.Sqrt(R); + break; + + default: + // unknown: treat as linear + light.OnIntensity = I0 * R; + break; + }*/ + + light.OnIntensity *= SavedValues.maybeExposure; + + Logger.Dev(logSrc, $"Converted intensity for RF2 light {light.UID}. DropoffType={light.DropoffType}, Range={light.Range}, RawIntensity={I0}, NewIntensity={light.OnIntensity}"); + } + light.OnTime = reader.ReadSingle(); + light.OnTimeVariation = reader.ReadSingle(); + light.OffIntensity = reader.ReadSingle(); + light.OffTime = reader.ReadSingle(); + light.OffTimeVariation = reader.ReadSingle(); + + lights.Add(light); + + // (we leave reader at end of this light record) + Logger.Dev(logSrc, + $"Loaded Light[{i}] UID={light.UID}, class={light.ClassName}, pos={light.Position}, " + + $"flags=0x{rawFlags:X8}, color={light.Color}, range={light.Range}, int={light.OnIntensity}, intmr={light.IntensityAtMaxRange}" + ); + } + + return lights; + } + } + + public static class RflBrushParser + { + private const string logSrc = "RflBrushParser"; + public static void ParseBrushesFromRfl(BinaryReader reader, long sectionEnd, Mesh mesh, int rfl_version) + { + int numBrushes = reader.ReadInt32(); + for (int i = 0; i < numBrushes; i++) + { + Logger.Debug(logSrc, $"Reading brush {i}/{numBrushes}"); + Brush brush = RFGeometryParser.ReadBrush(reader, rfl_version); + if (brush != null) // for safety, only add valid brushes + mesh.Brushes.Add(brush); + } + + Logger.Info(logSrc, $"Parsed {numBrushes} brushes from RF1 RFL file."); + + // Ensure we're positioned correctly at the end of the section + reader.BaseStream.Seek(sectionEnd, SeekOrigin.Begin); + } + } } diff --git a/parsers/RfmParser.cs b/parsers/RfmParser.cs index 12104dc..bd589fc 100644 --- a/parsers/RfmParser.cs +++ b/parsers/RfmParser.cs @@ -6,297 +6,297 @@ namespace redux.parsers { - public static class RfmParser - { - private const uint RFC_MAGIC = 0x87128712; - private const uint RFC_VERSION = 0x114; - - public static Mesh ReadRfm(string path) - { - Logger.Info(nameof(RfmParser), $"Loading RFM/RFC file: {path}"); - using var fs = File.OpenRead(path); - using var reader = new BinaryReader(fs); - - // 1) signature & version - uint sig = reader.ReadUInt32(); - Logger.Dev(nameof(RfmParser), $"Signature=0x{sig:X8}"); - if (sig != RFC_MAGIC) - throw new InvalidDataException($"Not an RFM/RFC file (sig=0x{sig:X8})"); - - uint version = reader.ReadUInt32(); - Logger.Dev(nameof(RfmParser), $"Version=0x{version:X}"); - if (version != RFC_VERSION) - throw new InvalidDataException($"Unsupported RFC version 0x{version:X}"); - - // 2) skeletal flag - byte hasBones = reader.ReadByte(); - Logger.Dev(nameof(RfmParser), $"HasBones={(hasBones != 0)}"); - - var mesh = new Mesh(); - - // 3) read Lst1 - int n1 = reader.ReadInt32(); - Logger.Dev(nameof(RfmParser), $"Lst1.Count={n1}"); - for (int i = 0; i < n1; i++) - { - string name = Utils.ReadZeroTerminatedAscii(reader); - int fld18 = reader.ReadInt32(); - var pos1 = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - float fld28 = reader.ReadSingle(); - Logger.Dev(nameof(RfmParser), $"Lst1[{i}] Name={name}, fld18={fld18}, Pos={pos1}, fld28={fld28}"); - } - - // 4) read Lst2 - int n2 = reader.ReadInt32(); - Logger.Dev(nameof(RfmParser), $"Lst2.Count={n2}"); - for (int i = 0; i < n2; i++) - { - string name = Utils.ReadZeroTerminatedAscii(reader); - int fld40 = reader.ReadInt32(); - var pos2 = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - var f50 = (reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - Logger.Dev(nameof(RfmParser), $"Lst2[{i}] Name={name}, fld40={fld40}, Pos={pos2}, F50={f50}"); - } - - // 5) bones - int boneCount = reader.ReadInt32(); - Logger.Dev(nameof(RfmParser), $"BoneCount={boneCount}"); - if (hasBones != 0 && boneCount > 0) - { - for (int i = 0; i < boneCount; i++) - { - string boneName = Utils.ReadZeroTerminatedAscii(reader); - uint parent = reader.ReadUInt32(); - for (int m = 0; m < 9; m++) reader.ReadSingle(); - var translation = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - Logger.Dev(nameof(RfmParser), $"Bone[{i}] Name={boneName}, Parent={parent}, Translation={translation}"); - mesh.Bones.Add(new Bone { Name = boneName, BaseRotation = Quaternion.Identity, BaseTranslation = translation, ParentIndex = (int)parent }); - } - V3mParser.ComputeBoneWorldPositions(mesh); - V3mParser.NormalizeBonePositions(mesh); - } - - // 6) objects & LOD layout - int objNum = reader.ReadInt32(); - int lodCount = reader.ReadInt32(); - Logger.Dev(nameof(RfmParser), $"ObjNum={objNum}, LODCount={lodCount}"); - - var lodHeap = new List(objNum * lodCount); - var objList = new List(objNum); - for (int i = 0; i < objNum; i++) objList.Add(new RfmObject()); - for (int i = 0; i < objNum * lodCount; i++) lodHeap.Add(new RfmLod()); - - // 7) per-object floats - int heapIdx = 0; - for (int i = 0; i < objNum; i++) - { - int fltCount = reader.ReadInt32(); Logger.Dev(nameof(RfmParser), $"Obj[{i}].fltCount={fltCount}"); - for (int j = 0; j < fltCount; j++, heapIdx++) - { - objList[i].Lods.Add(lodHeap[heapIdx]); - float f = reader.ReadSingle(); objList[i].Flts.Add(f); - Logger.Dev(nameof(RfmParser), $" Obj[{i}].LOD[{j}].flt={f}"); - } - } - - // 8) LOD names & IDs - foreach (var obj in objList) - { - foreach (var lod in obj.Lods) - { - lod.Name1 = Utils.ReadZeroTerminatedAscii(reader); - lod.Name2 = Utils.ReadZeroTerminatedAscii(reader); - lod.Dword1 = reader.ReadUInt32(); lod.Dword2 = reader.ReadUInt32(); - Logger.Dev(nameof(RfmParser), $"LOD Name1={lod.Name1},Name2={lod.Name2},D1={lod.Dword1},D2={lod.Dword2}"); - } - } - - // 9) parse each LOD’s geometry - foreach (var lod in lodHeap) ParseRfmLod(reader, lod, mesh); - - // 10) flatten first-LOD - int brushUid = 0; - foreach (var obj in objList) - { - var lod = obj.Lods[0]; - foreach (var geom in lod.Geoms) - { - var brush = new Brush - { - UID = brushUid++, - Position = lod.Pos, - Solid = new Solid() - }; - - // **IMPORTANT**: pull in the texture names - brush.Solid.Textures = new List(lod.Textures); - - // copy verts - brush.Vertices = geom.SimpleVertices - .Select(v => v + lod.Pos) - .ToList(); - - // copy faces - brush.Solid.Faces = geom.Triangles - .Select(tr => new Face - { - TextureIndex = tr.Item1.VtxID >= 0 ? geom.TexID : tr.Item1.VtxID, - Vertices = new List { tr.Item1.VtxID, tr.Item2.VtxID, tr.Item3.VtxID }, - UVs = new List { - new Vector2(tr.Item1.u, tr.Item1.v), - new Vector2(tr.Item2.u, tr.Item2.v), - new Vector2(tr.Item3.u, tr.Item3.v) - } - }).ToList(); - - mesh.Brushes.Add(brush); - } - } - - return mesh; - } - - private static void ParseRfmLod(BinaryReader reader, RfmLod lod, Mesh mesh) - { - Logger.Dev(nameof(RfmParser), $" → Parsing LOD “{lod.Name1}”"); - - // — textures — - int texCount = reader.ReadInt32(); - Logger.Dev(nameof(RfmParser), $" TexCount = {texCount}"); - for (int i = 0; i < texCount; i++) - { - string t = Utils.ReadZeroTerminatedAscii(reader); - Logger.Dev(nameof(RfmParser), $" Texture[{i}] = {t}"); - lod.Textures.Add(t); - } - - // — skip the “t42” records — - uint t42Count = reader.ReadUInt32(); - Logger.Dev(nameof(RfmParser), $" t42Count = {t42Count}"); - for (uint i = 0; i < t42Count; i++) - { - Logger.Dev(nameof(RfmParser), $" skipping t42[{i}]"); - // two dwords - reader.ReadUInt32(); - reader.ReadUInt32(); - // Bt1 (we don't need to store it) - reader.ReadByte(); - // skip the next 4 bytes - reader.ReadByte(); - reader.ReadByte(); - reader.ReadByte(); - reader.ReadByte(); - // then skip one more dword - reader.ReadUInt32(); - - - // two small “optional” blocks - for (int k = 0; k < 2; k++) - { - byte present = reader.ReadByte(); - if (present != 0) - { - reader.ReadUInt32(); - // Python does readNstr here => 2-byte length + that many bytes - Utils.ReadVString(reader); - reader.ReadUInt32(); - reader.ReadSingle(); - reader.ReadUInt32(); - } - } - - // skip the “throw‐away” count - reader.ReadUInt32(); - // now read the real count of floats - uint arr0Count = reader.ReadUInt32(); - for (uint k = 0; k < arr0Count; k++) - reader.ReadSingle(); - - // now the three bounding floats - reader.ReadSingle(); - reader.ReadSingle(); - reader.ReadSingle(); - - - // one V-string - Utils.ReadVString(reader); - - // another dword + float array - reader.ReadUInt32(); - uint arr1 = reader.ReadUInt32(); - for (uint k = 0; k < arr1; k++) - reader.ReadSingle(); - - // one float - reader.ReadSingle(); - - // one more float array - uint arr2 = reader.ReadUInt32(); - for (uint k = 0; k < arr2; k++) - reader.ReadSingle(); - } - - // — now the real geometry — - lod.NumGeom = reader.ReadInt32(); - Logger.Dev(nameof(RfmParser), $" NumGeom = {lod.NumGeom}"); - for (int g = 0; g < lod.NumGeom; g++) - { - Logger.Dev(nameof(RfmParser), $" Geom[{g}]:"); - var geom = new RfmGeom(); - - // simple‐mesh branch (no bones) - int vertCount = reader.ReadInt32(); - Logger.Dev(nameof(RfmParser), $" VertCount = {vertCount}"); - for (int v = 0; v < vertCount; v++) - { - var p = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - var n = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - geom.SimpleVertices.Add(p); - geom.Normals.Add(n); - } - - int triCount = reader.ReadInt32(); - Logger.Dev(nameof(RfmParser), $" TriCount = {triCount}"); - for (int t = 0; t < triCount; t++) - { - var f0 = new RfmFaceVtx(reader.ReadInt32(), reader.ReadSingle(), reader.ReadSingle()); - var f1 = new RfmFaceVtx(reader.ReadInt32(), reader.ReadSingle(), reader.ReadSingle()); - var f2 = new RfmFaceVtx(reader.ReadInt32(), reader.ReadSingle(), reader.ReadSingle()); - geom.Triangles.Add((f0, f1, f2)); - } - - geom.TexID = reader.ReadInt32(); - geom.Unk = reader.ReadInt32(); - geom.Fl1 = reader.ReadSingle(); - Logger.Dev(nameof(RfmParser), - $" TexID={geom.TexID}, Unk={geom.Unk}, Fl1={geom.Fl1}"); - - // if this mesh is actually skinned, skip its 4‐byte flags - if (mesh.Bones.Count > 0) - reader.ReadBytes(4); - - lod.Geoms.Add(geom); - } - - // — finally read the LOD’s position + bounds — - lod.Pos = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - Logger.Dev(nameof(RfmParser), $" LOD.Pos = {lod.Pos}"); - - // skip the “fl1 + bounds” floats - reader.ReadSingle(); // fl1 - reader.ReadSingle(); reader.ReadSingle(); // min bound - reader.ReadSingle(); - reader.ReadSingle(); reader.ReadSingle(); // max bound - reader.ReadSingle(); - } - - - // helper types - private class RfmObject { public List Lods = new(); public List Flts = new(); } - private class RfmLod { public string Name1, Name2; public uint Dword1, Dword2; public List Textures = new(); public int NumGeom; public List Geoms = new(); public Vector3 Pos; public List SkinnedVertices = new(); } - private class RfmGeom { public List SimpleVertices = new(); public List Normals = new(); public List<(RfmFaceVtx, RfmFaceVtx, RfmFaceVtx)> Triangles = new(); public int TexID, Unk; public float Fl1; } - private struct RfmFaceVtx { public int VtxID; public float u, v; public RfmFaceVtx(int id, float u, float v) { VtxID = id; this.u = u; this.v = v; } }; - private class RfmVertex { public Vector3 Pos, Normal; public ushort Sh1; public List Weights = new(); } - private struct RfmVtxWeight { public int BoneID; public float Weight; } - } + public static class RfmParser + { + private const uint RFC_MAGIC = 0x87128712; + private const uint RFC_VERSION = 0x114; + + public static Mesh ReadRfm(string path) + { + Logger.Info(nameof(RfmParser), $"Loading RFM/RFC file: {path}"); + using var fs = File.OpenRead(path); + using var reader = new BinaryReader(fs); + + // 1) signature & version + uint sig = reader.ReadUInt32(); + Logger.Dev(nameof(RfmParser), $"Signature=0x{sig:X8}"); + if (sig != RFC_MAGIC) + throw new InvalidDataException($"Not an RFM/RFC file (sig=0x{sig:X8})"); + + uint version = reader.ReadUInt32(); + Logger.Dev(nameof(RfmParser), $"Version=0x{version:X}"); + if (version != RFC_VERSION) + throw new InvalidDataException($"Unsupported RFC version 0x{version:X}"); + + // 2) skeletal flag + byte hasBones = reader.ReadByte(); + Logger.Dev(nameof(RfmParser), $"HasBones={(hasBones != 0)}"); + + var mesh = new Mesh(); + + // 3) read Lst1 + int n1 = reader.ReadInt32(); + Logger.Dev(nameof(RfmParser), $"Lst1.Count={n1}"); + for (int i = 0; i < n1; i++) + { + string name = Utils.ReadZeroTerminatedAscii(reader); + int fld18 = reader.ReadInt32(); + var pos1 = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + float fld28 = reader.ReadSingle(); + Logger.Dev(nameof(RfmParser), $"Lst1[{i}] Name={name}, fld18={fld18}, Pos={pos1}, fld28={fld28}"); + } + + // 4) read Lst2 + int n2 = reader.ReadInt32(); + Logger.Dev(nameof(RfmParser), $"Lst2.Count={n2}"); + for (int i = 0; i < n2; i++) + { + string name = Utils.ReadZeroTerminatedAscii(reader); + int fld40 = reader.ReadInt32(); + var pos2 = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + var f50 = (reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + Logger.Dev(nameof(RfmParser), $"Lst2[{i}] Name={name}, fld40={fld40}, Pos={pos2}, F50={f50}"); + } + + // 5) bones + int boneCount = reader.ReadInt32(); + Logger.Dev(nameof(RfmParser), $"BoneCount={boneCount}"); + if (hasBones != 0 && boneCount > 0) + { + for (int i = 0; i < boneCount; i++) + { + string boneName = Utils.ReadZeroTerminatedAscii(reader); + uint parent = reader.ReadUInt32(); + for (int m = 0; m < 9; m++) reader.ReadSingle(); + var translation = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + Logger.Dev(nameof(RfmParser), $"Bone[{i}] Name={boneName}, Parent={parent}, Translation={translation}"); + mesh.Bones.Add(new Bone { Name = boneName, BaseRotation = Quaternion.Identity, BaseTranslation = translation, ParentIndex = (int)parent }); + } + V3mParser.ComputeBoneWorldPositions(mesh); + V3mParser.NormalizeBonePositions(mesh); + } + + // 6) objects & LOD layout + int objNum = reader.ReadInt32(); + int lodCount = reader.ReadInt32(); + Logger.Dev(nameof(RfmParser), $"ObjNum={objNum}, LODCount={lodCount}"); + + var lodHeap = new List(objNum * lodCount); + var objList = new List(objNum); + for (int i = 0; i < objNum; i++) objList.Add(new RfmObject()); + for (int i = 0; i < objNum * lodCount; i++) lodHeap.Add(new RfmLod()); + + // 7) per-object floats + int heapIdx = 0; + for (int i = 0; i < objNum; i++) + { + int fltCount = reader.ReadInt32(); Logger.Dev(nameof(RfmParser), $"Obj[{i}].fltCount={fltCount}"); + for (int j = 0; j < fltCount; j++, heapIdx++) + { + objList[i].Lods.Add(lodHeap[heapIdx]); + float f = reader.ReadSingle(); objList[i].Flts.Add(f); + Logger.Dev(nameof(RfmParser), $" Obj[{i}].LOD[{j}].flt={f}"); + } + } + + // 8) LOD names & IDs + foreach (var obj in objList) + { + foreach (var lod in obj.Lods) + { + lod.Name1 = Utils.ReadZeroTerminatedAscii(reader); + lod.Name2 = Utils.ReadZeroTerminatedAscii(reader); + lod.Dword1 = reader.ReadUInt32(); lod.Dword2 = reader.ReadUInt32(); + Logger.Dev(nameof(RfmParser), $"LOD Name1={lod.Name1},Name2={lod.Name2},D1={lod.Dword1},D2={lod.Dword2}"); + } + } + + // 9) parse each LOD’s geometry + foreach (var lod in lodHeap) ParseRfmLod(reader, lod, mesh); + + // 10) flatten first-LOD + int brushUid = 0; + foreach (var obj in objList) + { + var lod = obj.Lods[0]; + foreach (var geom in lod.Geoms) + { + var brush = new Brush + { + UID = brushUid++, + Position = lod.Pos, + Solid = new Solid() + }; + + // **IMPORTANT**: pull in the texture names + brush.Solid.Textures = new List(lod.Textures); + + // copy verts + brush.Vertices = geom.SimpleVertices + .Select(v => v + lod.Pos) + .ToList(); + + // copy faces + brush.Solid.Faces = geom.Triangles + .Select(tr => new Face + { + TextureIndex = tr.Item1.VtxID >= 0 ? geom.TexID : tr.Item1.VtxID, + Vertices = new List { tr.Item1.VtxID, tr.Item2.VtxID, tr.Item3.VtxID }, + UVs = new List { + new Vector2(tr.Item1.u, tr.Item1.v), + new Vector2(tr.Item2.u, tr.Item2.v), + new Vector2(tr.Item3.u, tr.Item3.v) + } + }).ToList(); + + mesh.Brushes.Add(brush); + } + } + + return mesh; + } + + private static void ParseRfmLod(BinaryReader reader, RfmLod lod, Mesh mesh) + { + Logger.Dev(nameof(RfmParser), $" → Parsing LOD “{lod.Name1}”"); + + // — textures — + int texCount = reader.ReadInt32(); + Logger.Dev(nameof(RfmParser), $" TexCount = {texCount}"); + for (int i = 0; i < texCount; i++) + { + string t = Utils.ReadZeroTerminatedAscii(reader); + Logger.Dev(nameof(RfmParser), $" Texture[{i}] = {t}"); + lod.Textures.Add(t); + } + + // — skip the “t42” records — + uint t42Count = reader.ReadUInt32(); + Logger.Dev(nameof(RfmParser), $" t42Count = {t42Count}"); + for (uint i = 0; i < t42Count; i++) + { + Logger.Dev(nameof(RfmParser), $" skipping t42[{i}]"); + // two dwords + reader.ReadUInt32(); + reader.ReadUInt32(); + // Bt1 (we don't need to store it) + reader.ReadByte(); + // skip the next 4 bytes + reader.ReadByte(); + reader.ReadByte(); + reader.ReadByte(); + reader.ReadByte(); + // then skip one more dword + reader.ReadUInt32(); + + + // two small “optional” blocks + for (int k = 0; k < 2; k++) + { + byte present = reader.ReadByte(); + if (present != 0) + { + reader.ReadUInt32(); + // Python does readNstr here => 2-byte length + that many bytes + Utils.ReadVString(reader); + reader.ReadUInt32(); + reader.ReadSingle(); + reader.ReadUInt32(); + } + } + + // skip the “throw‐away” count + reader.ReadUInt32(); + // now read the real count of floats + uint arr0Count = reader.ReadUInt32(); + for (uint k = 0; k < arr0Count; k++) + reader.ReadSingle(); + + // now the three bounding floats + reader.ReadSingle(); + reader.ReadSingle(); + reader.ReadSingle(); + + + // one V-string + Utils.ReadVString(reader); + + // another dword + float array + reader.ReadUInt32(); + uint arr1 = reader.ReadUInt32(); + for (uint k = 0; k < arr1; k++) + reader.ReadSingle(); + + // one float + reader.ReadSingle(); + + // one more float array + uint arr2 = reader.ReadUInt32(); + for (uint k = 0; k < arr2; k++) + reader.ReadSingle(); + } + + // — now the real geometry — + lod.NumGeom = reader.ReadInt32(); + Logger.Dev(nameof(RfmParser), $" NumGeom = {lod.NumGeom}"); + for (int g = 0; g < lod.NumGeom; g++) + { + Logger.Dev(nameof(RfmParser), $" Geom[{g}]:"); + var geom = new RfmGeom(); + + // simple‐mesh branch (no bones) + int vertCount = reader.ReadInt32(); + Logger.Dev(nameof(RfmParser), $" VertCount = {vertCount}"); + for (int v = 0; v < vertCount; v++) + { + var p = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + var n = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + geom.SimpleVertices.Add(p); + geom.Normals.Add(n); + } + + int triCount = reader.ReadInt32(); + Logger.Dev(nameof(RfmParser), $" TriCount = {triCount}"); + for (int t = 0; t < triCount; t++) + { + var f0 = new RfmFaceVtx(reader.ReadInt32(), reader.ReadSingle(), reader.ReadSingle()); + var f1 = new RfmFaceVtx(reader.ReadInt32(), reader.ReadSingle(), reader.ReadSingle()); + var f2 = new RfmFaceVtx(reader.ReadInt32(), reader.ReadSingle(), reader.ReadSingle()); + geom.Triangles.Add((f0, f1, f2)); + } + + geom.TexID = reader.ReadInt32(); + geom.Unk = reader.ReadInt32(); + geom.Fl1 = reader.ReadSingle(); + Logger.Dev(nameof(RfmParser), + $" TexID={geom.TexID}, Unk={geom.Unk}, Fl1={geom.Fl1}"); + + // if this mesh is actually skinned, skip its 4‐byte flags + if (mesh.Bones.Count > 0) + reader.ReadBytes(4); + + lod.Geoms.Add(geom); + } + + // — finally read the LOD’s position + bounds — + lod.Pos = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + Logger.Dev(nameof(RfmParser), $" LOD.Pos = {lod.Pos}"); + + // skip the “fl1 + bounds” floats + reader.ReadSingle(); // fl1 + reader.ReadSingle(); reader.ReadSingle(); // min bound + reader.ReadSingle(); + reader.ReadSingle(); reader.ReadSingle(); // max bound + reader.ReadSingle(); + } + + + // helper types + private class RfmObject { public List Lods = new(); public List Flts = new(); } + private class RfmLod { public string Name1, Name2; public uint Dword1, Dword2; public List Textures = new(); public int NumGeom; public List Geoms = new(); public Vector3 Pos; public List SkinnedVertices = new(); } + private class RfmGeom { public List SimpleVertices = new(); public List Normals = new(); public List<(RfmFaceVtx, RfmFaceVtx, RfmFaceVtx)> Triangles = new(); public int TexID, Unk; public float Fl1; } + private struct RfmFaceVtx { public int VtxID; public float u, v; public RfmFaceVtx(int id, float u, float v) { VtxID = id; this.u = u; this.v = v; } }; + private class RfmVertex { public Vector3 Pos, Normal; public ushort Sh1; public List Weights = new(); } + private struct RfmVtxWeight { public int BoneID; public float Weight; } + } } diff --git a/parsers/V3mParser.cs b/parsers/V3mParser.cs index aa39a29..bb9d1c2 100644 --- a/parsers/V3mParser.cs +++ b/parsers/V3mParser.cs @@ -11,651 +11,651 @@ namespace redux.parsers { - // Parser for V3m (static) and V3c (skeletal) mesh files - public static class V3mParser - { - private const string logSrc = "V3mParser"; - - private const int V3M_SIGNATURE = 0x52463344; // 'RF3D' - private const int V3C_SIGNATURE = 0x5246434D; // 'RFCM' - private const int V3D_VERSION = 0x40000; - - private const int SECTION_END = 0x00000000; - private const int SECTION_SUBMESH = 0x5355424D; // 'SUBM' - private const int SECTION_CSPHERE = 0x43535048; // 'CSPH' - private const int SECTION_BONES = 0x424F4E45; // 'BONE' - - public static Mesh ReadV3mAsRflMesh(string path) - { - Logger.Debug(logSrc, $"Opening V3M/V3C file: \"{path}\""); - using var fs = File.OpenRead(path); - using var reader = new BinaryReader(fs); - - // Header - int signature = reader.ReadInt32(); - Logger.Debug(logSrc, $"Signature read: 0x{signature:X8}"); - if (signature != V3M_SIGNATURE && signature != V3C_SIGNATURE) - throw new InvalidDataException($"Not a V3M/V3C file (sig=0x{signature:X8})"); - - int version = reader.ReadInt32(); - Logger.Debug(logSrc, $"V3D version: 0x{version:X8}"); - if (version != V3D_VERSION) - throw new InvalidDataException($"Unsupported V3D version 0x{version:X8}"); - - int numSubmeshes = reader.ReadInt32(); - int numAllVerts = reader.ReadInt32(); // always 0 - int numAllTriangles = reader.ReadInt32(); // always 0 - int unknown0 = reader.ReadInt32(); // zero (normals) - int numAllMaterials = reader.ReadInt32(); - int unknown1 = reader.ReadInt32(); // zero (lod meshes) - int unknown2 = reader.ReadInt32(); // zero (dumb chunks) - int numColSpheres = reader.ReadInt32(); - - Logger.Debug(logSrc, $"Header → numSubmeshes={numSubmeshes}, numAllMaterials={numAllMaterials}, numColSpheres={numColSpheres}"); - - var mesh = new Mesh(); - - // Section parser - while (reader.BaseStream.Position < reader.BaseStream.Length) - { - long sectionHeaderPos = reader.BaseStream.Position; - int secType = reader.ReadInt32(); - int secSize = reader.ReadInt32(); - long secBodyStart = reader.BaseStream.Position; - - Logger.Debug(logSrc, $"Section @0x{sectionHeaderPos:X}: Type=0x{secType:X8}, Size={secSize}"); - - if (secType == SECTION_END) - { - Logger.Debug(logSrc, "Encountered SECTION_END; breaking out of section loop."); - break; - } - else if (secType == SECTION_SUBMESH) - { - Logger.Debug(logSrc, "Found SUBMESH section; parsing submesh → multiple LOD-Brushes"); - var lodBrushes = ParseSubmeshAsBrushes(reader); - foreach (var b in lodBrushes) - { - b.UID = mesh.Brushes.Count; // sequential UIDs for LOD level brushes - mesh.Brushes.Add(b); - } - } - else if (secType == SECTION_CSPHERE) - { - Logger.Debug(logSrc, "Found CSPHERE section; parsing."); - var cs = ParseCsphere(reader, secSize); - mesh.CollisionSpheres.Add(cs); - } - else if (secType == SECTION_BONES) - { - Logger.Debug(logSrc, "Found BONES section; parsing skeleton."); - ParseBones(reader, mesh, secSize); - ComputeBoneWorldPositions(mesh); - NormalizeBonePositions(mesh); - } - else - { - // skip any other chunk - Logger.Debug(logSrc, $"Unknown/ignored section 0x{secType:X8}, skipping its {secSize} bytes."); - reader.BaseStream.Seek(secBodyStart + secSize, SeekOrigin.Begin); - } - } - - Logger.Debug(logSrc, $"Finished reading V3M/V3C file. Total brushes parsed: {mesh.Brushes.Count}"); - - return mesh; - } - - private static List ParseSubmeshAsBrushes(BinaryReader reader) - { - Logger.Debug(logSrc, "Entering ParseSubmeshAsBrushes(...)"); - var brushes = new List(); - - string submeshName = ReadFixedAscii(reader, 24); - string parentName = ReadFixedAscii(reader, 24); - Logger.Debug(logSrc, $"Submesh name = \"{submeshName}\", parent = \"{parentName}\""); - - int submeshVersion = reader.ReadInt32(); // typically “7” - int numLods = reader.ReadInt32(); - Logger.Debug(logSrc, $"Submesh version={submeshVersion}, numLods={numLods}"); - - float[] lodDistances = new float[numLods]; - for (int i = 0; i < numLods; i++) - { - lodDistances[i] = reader.ReadSingle(); - Logger.Debug(logSrc, $" lodDistance[{i}] = {lodDistances[i]}"); - } - - Vector3 offset = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - float radius = reader.ReadSingle(); - Vector3 bboxMin = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - Vector3 bboxMax = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - Logger.Debug(logSrc, $"Offset={offset}, Radius={radius}, AABB_min={bboxMin}, AABB_max={bboxMax}"); - - var allLods = new LodMesh[numLods]; - for (int lodIdx = 0; lodIdx < numLods; lodIdx++) - { - Logger.Debug(logSrc, $"Parsing LOD {lodIdx + 1}/{numLods}"); - var lod = ParseLodMesh(reader); - allLods[lodIdx] = lod; - } - - int numMaterials = reader.ReadInt32(); - Logger.Debug(logSrc, $"numMaterials = {numMaterials}"); - - // Collect diffuse_map_name for each material - var submeshMaterials = new List(numMaterials); - for (int mi = 0; mi < numMaterials; mi++) - { - string diffuse = ReadFixedAscii(reader, 32).TrimEnd(); - float emissive = reader.ReadSingle(); - float specular = reader.ReadSingle(); - float glossiness = reader.ReadSingle(); - float reflection = reader.ReadSingle(); - string reflMap = ReadFixedAscii(reader, 32).TrimEnd(); - uint matFlags = reader.ReadUInt32(); - - Logger.Debug(logSrc, - $" Material[{mi}]: diffuse=\"{diffuse}\", emissive={emissive}, specular={specular}, glossiness={glossiness}, reflection={reflection}, reflMap=\"{reflMap}\", flags=0x{matFlags:X8}"); - - submeshMaterials.Add(diffuse); - } - - int numUnknown1 = reader.ReadInt32(); - Logger.Debug(logSrc, $"numUnknown1 = {numUnknown1}, skipping {numUnknown1 * 28} bytes"); - reader.BaseStream.Seek(numUnknown1 * 28, SeekOrigin.Current); - - // One brush per LOD - for (int lodIdx = 0; lodIdx < numLods; lodIdx++) - { - var lod = allLods[lodIdx]; - var brush = new Brush(); - brush.Solid = new Solid(); - - brush.TextureName = $"{submeshName}_LOD{lodIdx}"; - brush.Vertices = new List(); - brush.UVs = new List(); - brush.Indices = new List(); - brush.PropPoints = new List(); - - // Copy prop points - foreach (var pp in lod.PropPoints) - { - brush.PropPoints.Add(new redux.utilities.PropPoint - { - Name = pp.Name, - Position = pp.Position, - Orientation = pp.Orientation - }); - } - - var positions = new List(); - var uvs = new List(); - var faces = new List(); - var indices = new List(); - var jointIndices = new List(); - var jointWeights = new List(); - - for (int ci = 0; ci < lod.NumChunks; ci++) - { - var info = lod.ChunkInfos[ci]; - var chunkData = lod.Chunks[ci]; - int baseIndex = positions.Count; - - Logger.Debug(logSrc, - $" BUILDING Brush for LOD#{lodIdx}, Chunk[{ci}]: Pos={chunkData.Positions.Length}, UVs={chunkData.UVs.Length}, Triangles={chunkData.Triangles.Length}"); - - foreach (var pos in chunkData.Positions) - positions.Add(pos); - - foreach (var uv in chunkData.UVs) - uvs.Add(uv); - - for (int vi = 0; vi < chunkData.Positions.Length; vi++) - { - if (chunkData.BoneLinks != null && chunkData.BoneLinks.Length > 0) - { - var link = chunkData.BoneLinks[vi]; - // byte[4] each for Weights and Bones - float w0 = link.Weights[0] / 255f; - float w1 = link.Weights[1] / 255f; - float w2 = link.Weights[2] / 255f; - float w3 = link.Weights[3] / 255f; - jointWeights.Add(new Vector4(w0, w1, w2, w3)); - - float j0 = link.Bones[0]; - float j1 = link.Bones[1]; - float j2 = link.Bones[2]; - float j3 = link.Bones[3]; - jointIndices.Add(new Vector4(j0, j1, j2, j3)); - } - else - { - // No bone‐link info → put a “zero” influence on bone 0 - jointWeights.Add(new Vector4(1, 0, 0, 0)); - jointIndices.Add(new Vector4(0, 0, 0, 0)); - } - } - - for (int f = 0; f < chunkData.Triangles.Length; f++) - { - var tri = chunkData.Triangles[f]; - int i0 = baseIndex + tri.I0; - int i1 = baseIndex + tri.I1; - int i2 = baseIndex + tri.I2; - ushort ff = tri.Flags; - int textureIdx = lod.ChunkHeaders[ci]; - - var face = new Face - { - TextureIndex = textureIdx, - Vertices = new List { i0, i1, i2 }, - UVs = new List - { - uvs[baseIndex + tri.I0], - uvs[baseIndex + tri.I1], - uvs[baseIndex + tri.I2] - }, - FaceFlags = ff - }; - - faces.Add(face); - indices.Add(i0); - indices.Add(i1); - indices.Add(i2); - } - } - - brush.Vertices = positions; - brush.UVs = uvs; - brush.Indices = indices; - brush.Solid.Faces = faces; - brush.JointIndices = jointIndices; - brush.JointWeights = jointWeights; - - foreach (var matName in submeshMaterials) - brush.Solid.Textures.Add(matName); - - brushes.Add(brush); - Logger.Debug(logSrc, - $" → Created Brush “{brush.TextureName}”: Vertices={positions.Count}, Faces={faces.Count}, Materials={submeshMaterials.Count}"); - } - - Logger.Debug(logSrc, "Exiting ParseSubmeshAsBrushes(...)"); - return brushes; - } - - private static LodMesh ParseLodMesh(BinaryReader reader) - { - Logger.Debug(logSrc, " -> Entering ParseLodMesh(...)"); - - var lod = new LodMesh(); - lod.Flags = reader.ReadUInt32(); - lod.NumVertices = reader.ReadInt32(); - lod.NumChunks = reader.ReadUInt16(); - int dataBlockSize = reader.ReadInt32(); - - Logger.Debug(logSrc, $" Flags=0x{lod.Flags:X8}, NumVertices={lod.NumVertices}, NumChunks={lod.NumChunks}, DataBlockSize={dataBlockSize}"); - - lod.DataBlock = reader.ReadBytes(dataBlockSize); - lod.Unknown1 = reader.ReadInt32(); - Logger.Debug(logSrc, $" Unknown1 = {lod.Unknown1}"); - - lod.ChunkInfos = new ChunkInfo[lod.NumChunks]; - for (int i = 0; i < lod.NumChunks; i++) - { - var ci = new ChunkInfo - { - NumVertices = reader.ReadUInt16(), - NumFaces = reader.ReadUInt16(), - VecsAlloc = reader.ReadUInt16(), - FacesAlloc = reader.ReadUInt16(), - SamePosVertexOffsetsAlloc = reader.ReadUInt16(), - WiAlloc = reader.ReadUInt16(), - UvsAlloc = reader.ReadUInt16(), - RenderFlags = reader.ReadUInt32() - }; - lod.ChunkInfos[i] = ci; - Logger.Debug(logSrc, $" ChunkInfo[{i}]: NumVertices={ci.NumVertices}, NumFaces={ci.NumFaces}, VecsAlloc={ci.VecsAlloc}, FacesAlloc={ci.FacesAlloc}, SamePosOffsetAlloc={ci.SamePosVertexOffsetsAlloc}, WiAlloc={ci.WiAlloc}, UvsAlloc={ci.UvsAlloc}, RenderFlags=0x{ci.RenderFlags:X8}"); - } - - lod.NumPropPoints = reader.ReadInt32(); - lod.NumTextures = reader.ReadInt32(); - Logger.Debug(logSrc, $" NumPropPoints={lod.NumPropPoints}, NumTextures={lod.NumTextures}"); - - lod.Textures = new LodTexture[lod.NumTextures]; - for (int t = 0; t < lod.NumTextures; t++) - { - var lt = new LodTexture - { - Id = reader.ReadByte(), - Filename = ReadZeroTerminatedAscii(reader) - }; - lod.Textures[t] = lt; - Logger.Debug(logSrc, $" LodTexture[{t}]: Id={lt.Id}, Filename=\"{lt.Filename}\""); - } - - UnpackLodDataBlock(lod); - - Logger.Debug(logSrc, " <- Exiting ParseLodMesh(...)"); - return lod; - } - - private static void UnpackLodDataBlock(LodMesh lod) - { - Logger.Debug(logSrc, " -> Entering UnpackLodDataBlock(...)"); - using var ms = new MemoryStream(lod.DataBlock); - using var r = new BinaryReader(ms); - - lod.ChunkHeaders = new int[lod.NumChunks]; - for (int ci = 0; ci < lod.NumChunks; ci++) - { - r.ReadBytes(0x20); - int texIdx = r.ReadInt32(); - r.ReadBytes(0x14); - lod.ChunkHeaders[ci] = texIdx; - Logger.Debug(logSrc, $" ChunkHeader[{ci}] → textureIdx={texIdx}"); - } - - // align to 0x10 - long pad = (0x10L - (ms.Position % 0x10L)) % 0x10L; - if (pad > 0) - { - Logger.Debug(logSrc, $" Skipping {pad} bytes of padding after chunk headers."); - r.ReadBytes((int)pad); - } - - lod.Chunks = new ChunkData[lod.NumChunks]; - for (int ci = 0; ci < lod.NumChunks; ci++) - { - var info = lod.ChunkInfos[ci]; - var cd = new ChunkData(); - - Logger.Debug(logSrc, $" → Unpacking ChunkData[{ci}]: VecsAlloc={info.VecsAlloc}, UvsAlloc={info.UvsAlloc}, FacesAlloc={info.FacesAlloc}, SamePosOffsetAlloc={info.SamePosVertexOffsetsAlloc}, WiAlloc={info.WiAlloc}"); - - // positions - int numPos = info.VecsAlloc / 12; - cd.Positions = new Vector3[numPos]; - for (int i = 0; i < numPos; i++) - { - cd.Positions[i] = new Vector3(r.ReadSingle(), r.ReadSingle(), r.ReadSingle()); - } - pad = (0x10L - (ms.Position % 0x10L)) % 0x10L; - if (pad > 0) r.ReadBytes((int)pad); - Logger.Debug(logSrc, $" Read {numPos} positions, skipped {pad} bytes padding."); - - // normals - cd.Normals = new Vector3[numPos]; - for (int i = 0; i < numPos; i++) - { - cd.Normals[i] = new Vector3(r.ReadSingle(), r.ReadSingle(), r.ReadSingle()); - } - pad = (0x10L - (ms.Position % 0x10L)) % 0x10L; - if (pad > 0) r.ReadBytes((int)pad); - Logger.Debug(logSrc, $" Read {numPos} normals, skipped {pad} bytes padding."); - - // uvs - int numUvs = info.UvsAlloc / 8; - cd.UVs = new Vector2[numUvs]; - for (int i = 0; i < numUvs; i++) - { - cd.UVs[i] = new Vector2(r.ReadSingle(), r.ReadSingle()); - } - pad = (0x10L - (ms.Position % 0x10L)) % 0x10L; - if (pad > 0) r.ReadBytes((int)pad); - Logger.Debug(logSrc, $" Read {numUvs} UVs, skipped {pad} bytes padding."); - - // faces - int numFaces = info.FacesAlloc / 8; - cd.Triangles = new Triangle[numFaces]; - for (int i = 0; i < numFaces; i++) - { - cd.Triangles[i].I0 = r.ReadUInt16(); - cd.Triangles[i].I1 = r.ReadUInt16(); - cd.Triangles[i].I2 = r.ReadUInt16(); - cd.Triangles[i].Flags = r.ReadUInt16(); - } - pad = (0x10L - (ms.Position % 0x10L)) % 0x10L; - if (pad > 0) r.ReadBytes((int)pad); - Logger.Debug(logSrc, $" Read {numFaces} triangles, skipped {pad} bytes padding."); - - // optional planes - if ((lod.Flags & (uint)LodFlags.TrianglePlanes) != 0) - { - cd.Planes = new RFPlane[numFaces]; - for (int i = 0; i < numFaces; i++) - { - cd.Planes[i].Normal = new Vector3(r.ReadSingle(), r.ReadSingle(), r.ReadSingle()); - cd.Planes[i].Dist = r.ReadSingle(); - } - pad = (0x10L - (ms.Position % 0x10L)) % 0x10L; - if (pad > 0) r.ReadBytes((int)pad); - Logger.Debug(logSrc, $" Read {numFaces} planes, skipped {pad} bytes padding."); - } - else - { - cd.Planes = Array.Empty(); - } - - // same_pos_vertex_offsets - int numOffsets = info.SamePosVertexOffsetsAlloc / 2; - cd.SamePosVertexOffsets = new short[numOffsets]; - for (int i = 0; i < numOffsets; i++) - cd.SamePosVertexOffsets[i] = r.ReadInt16(); - pad = (0x10L - (ms.Position % 0x10L)) % 0x10L; - if (pad > 0) r.ReadBytes((int)pad); - Logger.Debug(logSrc, $" Read {numOffsets} same_pos offsets, skipped {pad} bytes padding."); - - // optional bone_links - if (info.WiAlloc > 0) - { - int numWeights = info.WiAlloc / 8; - cd.BoneLinks = new VertexBoneLink[numWeights]; - for (int i = 0; i < numWeights; i++) - { - var vbl = new VertexBoneLink - { - Weights = new byte[4], - Bones = new byte[4] - }; - for (int w = 0; w < 4; w++) - vbl.Weights[w] = r.ReadByte(); - for (int b = 0; b < 4; b++) - vbl.Bones[b] = r.ReadByte(); - cd.BoneLinks[i] = vbl; - } - pad = (0x10L - (ms.Position % 0x10L)) % 0x10L; - if (pad > 0) r.ReadBytes((int)pad); - } - else - { - cd.BoneLinks = Array.Empty(); - } - - // optional orig_map - if ((lod.Flags & (uint)LodFlags.OrigMap) != 0) - { - int numOrig = lod.NumVertices; - cd.OrigMap = new short[numOrig]; - for (int i = 0; i < numOrig; i++) - cd.OrigMap[i] = r.ReadInt16(); - pad = (0x10L - (ms.Position % 0x10L)) % 0x10L; - if (pad > 0) r.ReadBytes((int)pad); - Logger.Debug(logSrc, $" Read {numOrig} orig_map entries, skipped {pad} bytes padding."); - } - else - { - cd.OrigMap = Array.Empty(); - Logger.Debug(logSrc, $" No orig_map (OrigMap flag not set)."); - } - - lod.Chunks[ci] = cd; - } - - if (lod.NumPropPoints > 0) - { - lod.PropPoints = new PropPoint[lod.NumPropPoints]; - for (int p = 0; p < lod.NumPropPoints; p++) - { - var pp = new PropPoint - { - Name = ReadFixedAscii(r, 0x44), // 68 bytes, strz - Orientation = new Quaternion( - r.ReadSingle(), - r.ReadSingle(), - r.ReadSingle(), - r.ReadSingle()), - Position = new Vector3( - r.ReadSingle(), - r.ReadSingle(), - r.ReadSingle()), - ParentIndex = r.ReadInt32() - }; - lod.PropPoints[p] = pp; - Logger.Debug(logSrc, $" PropPoint[{p}]: Name=\"{pp.Name}\", Position={pp.Position}, ParentIndex={pp.ParentIndex}"); - } - } - else - { - lod.PropPoints = Array.Empty(); - Logger.Debug(logSrc, $" No prop_points in data_block (NumPropPoints=0)."); - } - - Logger.Debug(logSrc, " <- Exiting UnpackLodDataBlock(...)"); - } - - private static CollisionSphere ParseCsphere(BinaryReader reader, int secSize) - { - var cs = new CollisionSphere(); - cs.Name = ReadFixedAscii(reader, 24); - cs.ParentIndex = reader.ReadInt32(); - float x = reader.ReadSingle(); - float y = reader.ReadSingle(); - float z = reader.ReadSingle(); - cs.Position = new Vector3(x, y, z); - cs.Radius = reader.ReadSingle(); - - // Advance if there is leftover padding - long bytesRead = 24 + 4 + 12 + 4; // 44 bytes total - long toSkip = secSize - bytesRead; - if (toSkip > 0) - reader.BaseStream.Seek(toSkip, SeekOrigin.Current); - - return cs; - } - - private static void ParseBones(BinaryReader reader, Mesh mesh, int secSize) - { - long startPos = reader.BaseStream.Position; - int numBones = reader.ReadInt32(); - Logger.Debug(logSrc, $" → numBones = {numBones}"); - - for (int i = 0; i < numBones; i++) - { - string boneName = ReadFixedAscii(reader, 24); - - float qx = reader.ReadSingle(); - float qy = reader.ReadSingle(); - float qz = reader.ReadSingle(); - float qw = reader.ReadSingle(); - var baseRot = new Quaternion(qx, qy, qz, qw); - baseRot = Quaternion.Normalize(baseRot); - - float tx = reader.ReadSingle(); - float ty = reader.ReadSingle(); - float tz = reader.ReadSingle(); - var baseTrans = new Vector3(tx, ty, tz); - - int parentIndex = reader.ReadInt32(); - - var bone = new Bone - { - Name = boneName, - BaseRotation = baseRot, - BaseTranslation = baseTrans, - ParentIndex = parentIndex - }; - mesh.Bones.Add(bone); - - Logger.Debug(logSrc, - $" Bone[{i}]: Name=\"{boneName}\", Parent={parentIndex}, Rot=({qx},{qy},{qz},{qw}), Trans=({tx},{ty},{tz})"); - } - - // Now compute and normalize RH‐corrected positions: - ComputeBoneWorldPositions(mesh); - NormalizeBonePositions(mesh); - - long consumed = reader.BaseStream.Position - startPos; - long toSkip = secSize - consumed; - if (toSkip > 0) - reader.BaseStream.Seek(toSkip, SeekOrigin.Current); - } - - public static void ComputeBoneWorldPositions(Mesh mesh) - { - int count = mesh.Bones.Count; - mesh.BoneWorldPositions = new Vector3[count]; - - for (int i = 0; i < count; i++) - { - Bone b = mesh.Bones[i]; - - // 1) Convert RF quaternion → RH quaternion by flipping X - // flip handedness - Quaternion q_rf = b.BaseRotation; - var q_rh = new Quaternion( - -q_rf.X, // flip X - q_rf.Y, - q_rf.Z, - q_rf.W - ); - q_rh = Quaternion.Normalize(q_rh); - - // 2) Convert RF translation → RH translation by flipping X - Vector3 t_rf = b.BaseTranslation; - var t_rh = new Vector3( - -t_rf.X, // flip X - t_rf.Y, - t_rf.Z - ); - - // 3) Compute bone‐origin in world (RH) = -( R_rh^{-1} * t_rh ) - // Since R_rh is normalized, R_rh^{-1} == Quaternion.Conjugate(R_rh) - Quaternion invRot = Quaternion.Conjugate(q_rh); - Vector3 rotated = Vector3.Transform(t_rh, invRot); - Vector3 worldOrig = -rotated; - - mesh.BoneWorldPositions[i] = worldOrig; - } - } - - public static void NormalizeBonePositions(Mesh mesh) - { - // Find the root bone (ParentIndex < 0) - int rootIndex = -1; - for (int i = 0; i < mesh.Bones.Count; i++) - { - if (mesh.Bones[i].ParentIndex < 0) - { - rootIndex = i; - break; - } - } - - if (rootIndex < 0) - { - Logger.Warn(logSrc, "No root bone found; skipping normalization."); - return; - } - - Vector3 rootPos = mesh.BoneWorldPositions[rootIndex]; - int count = mesh.Bones.Count; - mesh.BoneModelPositions = new Vector3[count]; - - for (int i = 0; i < count; i++) - { - mesh.BoneModelPositions[i] = mesh.BoneWorldPositions[i] - rootPos; - } - } - } + // Parser for V3m (static) and V3c (skeletal) mesh files + public static class V3mParser + { + private const string logSrc = "V3mParser"; + + private const int V3M_SIGNATURE = 0x52463344; // 'RF3D' + private const int V3C_SIGNATURE = 0x5246434D; // 'RFCM' + private const int V3D_VERSION = 0x40000; + + private const int SECTION_END = 0x00000000; + private const int SECTION_SUBMESH = 0x5355424D; // 'SUBM' + private const int SECTION_CSPHERE = 0x43535048; // 'CSPH' + private const int SECTION_BONES = 0x424F4E45; // 'BONE' + + public static Mesh ReadV3mAsRflMesh(string path) + { + Logger.Debug(logSrc, $"Opening V3M/V3C file: \"{path}\""); + using var fs = File.OpenRead(path); + using var reader = new BinaryReader(fs); + + // Header + int signature = reader.ReadInt32(); + Logger.Debug(logSrc, $"Signature read: 0x{signature:X8}"); + if (signature != V3M_SIGNATURE && signature != V3C_SIGNATURE) + throw new InvalidDataException($"Not a V3M/V3C file (sig=0x{signature:X8})"); + + int version = reader.ReadInt32(); + Logger.Debug(logSrc, $"V3D version: 0x{version:X8}"); + if (version != V3D_VERSION) + throw new InvalidDataException($"Unsupported V3D version 0x{version:X8}"); + + int numSubmeshes = reader.ReadInt32(); + int numAllVerts = reader.ReadInt32(); // always 0 + int numAllTriangles = reader.ReadInt32(); // always 0 + int unknown0 = reader.ReadInt32(); // zero (normals) + int numAllMaterials = reader.ReadInt32(); + int unknown1 = reader.ReadInt32(); // zero (lod meshes) + int unknown2 = reader.ReadInt32(); // zero (dumb chunks) + int numColSpheres = reader.ReadInt32(); + + Logger.Debug(logSrc, $"Header → numSubmeshes={numSubmeshes}, numAllMaterials={numAllMaterials}, numColSpheres={numColSpheres}"); + + var mesh = new Mesh(); + + // Section parser + while (reader.BaseStream.Position < reader.BaseStream.Length) + { + long sectionHeaderPos = reader.BaseStream.Position; + int secType = reader.ReadInt32(); + int secSize = reader.ReadInt32(); + long secBodyStart = reader.BaseStream.Position; + + Logger.Debug(logSrc, $"Section @0x{sectionHeaderPos:X}: Type=0x{secType:X8}, Size={secSize}"); + + if (secType == SECTION_END) + { + Logger.Debug(logSrc, "Encountered SECTION_END; breaking out of section loop."); + break; + } + else if (secType == SECTION_SUBMESH) + { + Logger.Debug(logSrc, "Found SUBMESH section; parsing submesh → multiple LOD-Brushes"); + var lodBrushes = ParseSubmeshAsBrushes(reader); + foreach (var b in lodBrushes) + { + b.UID = mesh.Brushes.Count; // sequential UIDs for LOD level brushes + mesh.Brushes.Add(b); + } + } + else if (secType == SECTION_CSPHERE) + { + Logger.Debug(logSrc, "Found CSPHERE section; parsing."); + var cs = ParseCsphere(reader, secSize); + mesh.CollisionSpheres.Add(cs); + } + else if (secType == SECTION_BONES) + { + Logger.Debug(logSrc, "Found BONES section; parsing skeleton."); + ParseBones(reader, mesh, secSize); + ComputeBoneWorldPositions(mesh); + NormalizeBonePositions(mesh); + } + else + { + // skip any other chunk + Logger.Debug(logSrc, $"Unknown/ignored section 0x{secType:X8}, skipping its {secSize} bytes."); + reader.BaseStream.Seek(secBodyStart + secSize, SeekOrigin.Begin); + } + } + + Logger.Debug(logSrc, $"Finished reading V3M/V3C file. Total brushes parsed: {mesh.Brushes.Count}"); + + return mesh; + } + + private static List ParseSubmeshAsBrushes(BinaryReader reader) + { + Logger.Debug(logSrc, "Entering ParseSubmeshAsBrushes(...)"); + var brushes = new List(); + + string submeshName = ReadFixedAscii(reader, 24); + string parentName = ReadFixedAscii(reader, 24); + Logger.Debug(logSrc, $"Submesh name = \"{submeshName}\", parent = \"{parentName}\""); + + int submeshVersion = reader.ReadInt32(); // typically “7” + int numLods = reader.ReadInt32(); + Logger.Debug(logSrc, $"Submesh version={submeshVersion}, numLods={numLods}"); + + float[] lodDistances = new float[numLods]; + for (int i = 0; i < numLods; i++) + { + lodDistances[i] = reader.ReadSingle(); + Logger.Debug(logSrc, $" lodDistance[{i}] = {lodDistances[i]}"); + } + + Vector3 offset = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + float radius = reader.ReadSingle(); + Vector3 bboxMin = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + Vector3 bboxMax = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + Logger.Debug(logSrc, $"Offset={offset}, Radius={radius}, AABB_min={bboxMin}, AABB_max={bboxMax}"); + + var allLods = new LodMesh[numLods]; + for (int lodIdx = 0; lodIdx < numLods; lodIdx++) + { + Logger.Debug(logSrc, $"Parsing LOD {lodIdx + 1}/{numLods}"); + var lod = ParseLodMesh(reader); + allLods[lodIdx] = lod; + } + + int numMaterials = reader.ReadInt32(); + Logger.Debug(logSrc, $"numMaterials = {numMaterials}"); + + // Collect diffuse_map_name for each material + var submeshMaterials = new List(numMaterials); + for (int mi = 0; mi < numMaterials; mi++) + { + string diffuse = ReadFixedAscii(reader, 32).TrimEnd(); + float emissive = reader.ReadSingle(); + float specular = reader.ReadSingle(); + float glossiness = reader.ReadSingle(); + float reflection = reader.ReadSingle(); + string reflMap = ReadFixedAscii(reader, 32).TrimEnd(); + uint matFlags = reader.ReadUInt32(); + + Logger.Debug(logSrc, + $" Material[{mi}]: diffuse=\"{diffuse}\", emissive={emissive}, specular={specular}, glossiness={glossiness}, reflection={reflection}, reflMap=\"{reflMap}\", flags=0x{matFlags:X8}"); + + submeshMaterials.Add(diffuse); + } + + int numUnknown1 = reader.ReadInt32(); + Logger.Debug(logSrc, $"numUnknown1 = {numUnknown1}, skipping {numUnknown1 * 28} bytes"); + reader.BaseStream.Seek(numUnknown1 * 28, SeekOrigin.Current); + + // One brush per LOD + for (int lodIdx = 0; lodIdx < numLods; lodIdx++) + { + var lod = allLods[lodIdx]; + var brush = new Brush(); + brush.Solid = new Solid(); + + brush.TextureName = $"{submeshName}_LOD{lodIdx}"; + brush.Vertices = new List(); + brush.UVs = new List(); + brush.Indices = new List(); + brush.PropPoints = new List(); + + // Copy prop points + foreach (var pp in lod.PropPoints) + { + brush.PropPoints.Add(new redux.utilities.PropPoint + { + Name = pp.Name, + Position = pp.Position, + Orientation = pp.Orientation + }); + } + + var positions = new List(); + var uvs = new List(); + var faces = new List(); + var indices = new List(); + var jointIndices = new List(); + var jointWeights = new List(); + + for (int ci = 0; ci < lod.NumChunks; ci++) + { + var info = lod.ChunkInfos[ci]; + var chunkData = lod.Chunks[ci]; + int baseIndex = positions.Count; + + Logger.Debug(logSrc, + $" BUILDING Brush for LOD#{lodIdx}, Chunk[{ci}]: Pos={chunkData.Positions.Length}, UVs={chunkData.UVs.Length}, Triangles={chunkData.Triangles.Length}"); + + foreach (var pos in chunkData.Positions) + positions.Add(pos); + + foreach (var uv in chunkData.UVs) + uvs.Add(uv); + + for (int vi = 0; vi < chunkData.Positions.Length; vi++) + { + if (chunkData.BoneLinks != null && chunkData.BoneLinks.Length > 0) + { + var link = chunkData.BoneLinks[vi]; + // byte[4] each for Weights and Bones + float w0 = link.Weights[0] / 255f; + float w1 = link.Weights[1] / 255f; + float w2 = link.Weights[2] / 255f; + float w3 = link.Weights[3] / 255f; + jointWeights.Add(new Vector4(w0, w1, w2, w3)); + + float j0 = link.Bones[0]; + float j1 = link.Bones[1]; + float j2 = link.Bones[2]; + float j3 = link.Bones[3]; + jointIndices.Add(new Vector4(j0, j1, j2, j3)); + } + else + { + // No bone‐link info → put a “zero” influence on bone 0 + jointWeights.Add(new Vector4(1, 0, 0, 0)); + jointIndices.Add(new Vector4(0, 0, 0, 0)); + } + } + + for (int f = 0; f < chunkData.Triangles.Length; f++) + { + var tri = chunkData.Triangles[f]; + int i0 = baseIndex + tri.I0; + int i1 = baseIndex + tri.I1; + int i2 = baseIndex + tri.I2; + ushort ff = tri.Flags; + int textureIdx = lod.ChunkHeaders[ci]; + + var face = new Face + { + TextureIndex = textureIdx, + Vertices = new List { i0, i1, i2 }, + UVs = new List + { + uvs[baseIndex + tri.I0], + uvs[baseIndex + tri.I1], + uvs[baseIndex + tri.I2] + }, + FaceFlags = ff + }; + + faces.Add(face); + indices.Add(i0); + indices.Add(i1); + indices.Add(i2); + } + } + + brush.Vertices = positions; + brush.UVs = uvs; + brush.Indices = indices; + brush.Solid.Faces = faces; + brush.JointIndices = jointIndices; + brush.JointWeights = jointWeights; + + foreach (var matName in submeshMaterials) + brush.Solid.Textures.Add(matName); + + brushes.Add(brush); + Logger.Debug(logSrc, + $" → Created Brush “{brush.TextureName}”: Vertices={positions.Count}, Faces={faces.Count}, Materials={submeshMaterials.Count}"); + } + + Logger.Debug(logSrc, "Exiting ParseSubmeshAsBrushes(...)"); + return brushes; + } + + private static LodMesh ParseLodMesh(BinaryReader reader) + { + Logger.Debug(logSrc, " -> Entering ParseLodMesh(...)"); + + var lod = new LodMesh(); + lod.Flags = reader.ReadUInt32(); + lod.NumVertices = reader.ReadInt32(); + lod.NumChunks = reader.ReadUInt16(); + int dataBlockSize = reader.ReadInt32(); + + Logger.Debug(logSrc, $" Flags=0x{lod.Flags:X8}, NumVertices={lod.NumVertices}, NumChunks={lod.NumChunks}, DataBlockSize={dataBlockSize}"); + + lod.DataBlock = reader.ReadBytes(dataBlockSize); + lod.Unknown1 = reader.ReadInt32(); + Logger.Debug(logSrc, $" Unknown1 = {lod.Unknown1}"); + + lod.ChunkInfos = new ChunkInfo[lod.NumChunks]; + for (int i = 0; i < lod.NumChunks; i++) + { + var ci = new ChunkInfo + { + NumVertices = reader.ReadUInt16(), + NumFaces = reader.ReadUInt16(), + VecsAlloc = reader.ReadUInt16(), + FacesAlloc = reader.ReadUInt16(), + SamePosVertexOffsetsAlloc = reader.ReadUInt16(), + WiAlloc = reader.ReadUInt16(), + UvsAlloc = reader.ReadUInt16(), + RenderFlags = reader.ReadUInt32() + }; + lod.ChunkInfos[i] = ci; + Logger.Debug(logSrc, $" ChunkInfo[{i}]: NumVertices={ci.NumVertices}, NumFaces={ci.NumFaces}, VecsAlloc={ci.VecsAlloc}, FacesAlloc={ci.FacesAlloc}, SamePosOffsetAlloc={ci.SamePosVertexOffsetsAlloc}, WiAlloc={ci.WiAlloc}, UvsAlloc={ci.UvsAlloc}, RenderFlags=0x{ci.RenderFlags:X8}"); + } + + lod.NumPropPoints = reader.ReadInt32(); + lod.NumTextures = reader.ReadInt32(); + Logger.Debug(logSrc, $" NumPropPoints={lod.NumPropPoints}, NumTextures={lod.NumTextures}"); + + lod.Textures = new LodTexture[lod.NumTextures]; + for (int t = 0; t < lod.NumTextures; t++) + { + var lt = new LodTexture + { + Id = reader.ReadByte(), + Filename = ReadZeroTerminatedAscii(reader) + }; + lod.Textures[t] = lt; + Logger.Debug(logSrc, $" LodTexture[{t}]: Id={lt.Id}, Filename=\"{lt.Filename}\""); + } + + UnpackLodDataBlock(lod); + + Logger.Debug(logSrc, " <- Exiting ParseLodMesh(...)"); + return lod; + } + + private static void UnpackLodDataBlock(LodMesh lod) + { + Logger.Debug(logSrc, " -> Entering UnpackLodDataBlock(...)"); + using var ms = new MemoryStream(lod.DataBlock); + using var r = new BinaryReader(ms); + + lod.ChunkHeaders = new int[lod.NumChunks]; + for (int ci = 0; ci < lod.NumChunks; ci++) + { + r.ReadBytes(0x20); + int texIdx = r.ReadInt32(); + r.ReadBytes(0x14); + lod.ChunkHeaders[ci] = texIdx; + Logger.Debug(logSrc, $" ChunkHeader[{ci}] → textureIdx={texIdx}"); + } + + // align to 0x10 + long pad = (0x10L - (ms.Position % 0x10L)) % 0x10L; + if (pad > 0) + { + Logger.Debug(logSrc, $" Skipping {pad} bytes of padding after chunk headers."); + r.ReadBytes((int)pad); + } + + lod.Chunks = new ChunkData[lod.NumChunks]; + for (int ci = 0; ci < lod.NumChunks; ci++) + { + var info = lod.ChunkInfos[ci]; + var cd = new ChunkData(); + + Logger.Debug(logSrc, $" → Unpacking ChunkData[{ci}]: VecsAlloc={info.VecsAlloc}, UvsAlloc={info.UvsAlloc}, FacesAlloc={info.FacesAlloc}, SamePosOffsetAlloc={info.SamePosVertexOffsetsAlloc}, WiAlloc={info.WiAlloc}"); + + // positions + int numPos = info.VecsAlloc / 12; + cd.Positions = new Vector3[numPos]; + for (int i = 0; i < numPos; i++) + { + cd.Positions[i] = new Vector3(r.ReadSingle(), r.ReadSingle(), r.ReadSingle()); + } + pad = (0x10L - (ms.Position % 0x10L)) % 0x10L; + if (pad > 0) r.ReadBytes((int)pad); + Logger.Debug(logSrc, $" Read {numPos} positions, skipped {pad} bytes padding."); + + // normals + cd.Normals = new Vector3[numPos]; + for (int i = 0; i < numPos; i++) + { + cd.Normals[i] = new Vector3(r.ReadSingle(), r.ReadSingle(), r.ReadSingle()); + } + pad = (0x10L - (ms.Position % 0x10L)) % 0x10L; + if (pad > 0) r.ReadBytes((int)pad); + Logger.Debug(logSrc, $" Read {numPos} normals, skipped {pad} bytes padding."); + + // uvs + int numUvs = info.UvsAlloc / 8; + cd.UVs = new Vector2[numUvs]; + for (int i = 0; i < numUvs; i++) + { + cd.UVs[i] = new Vector2(r.ReadSingle(), r.ReadSingle()); + } + pad = (0x10L - (ms.Position % 0x10L)) % 0x10L; + if (pad > 0) r.ReadBytes((int)pad); + Logger.Debug(logSrc, $" Read {numUvs} UVs, skipped {pad} bytes padding."); + + // faces + int numFaces = info.FacesAlloc / 8; + cd.Triangles = new Triangle[numFaces]; + for (int i = 0; i < numFaces; i++) + { + cd.Triangles[i].I0 = r.ReadUInt16(); + cd.Triangles[i].I1 = r.ReadUInt16(); + cd.Triangles[i].I2 = r.ReadUInt16(); + cd.Triangles[i].Flags = r.ReadUInt16(); + } + pad = (0x10L - (ms.Position % 0x10L)) % 0x10L; + if (pad > 0) r.ReadBytes((int)pad); + Logger.Debug(logSrc, $" Read {numFaces} triangles, skipped {pad} bytes padding."); + + // optional planes + if ((lod.Flags & (uint)LodFlags.TrianglePlanes) != 0) + { + cd.Planes = new RFPlane[numFaces]; + for (int i = 0; i < numFaces; i++) + { + cd.Planes[i].Normal = new Vector3(r.ReadSingle(), r.ReadSingle(), r.ReadSingle()); + cd.Planes[i].Dist = r.ReadSingle(); + } + pad = (0x10L - (ms.Position % 0x10L)) % 0x10L; + if (pad > 0) r.ReadBytes((int)pad); + Logger.Debug(logSrc, $" Read {numFaces} planes, skipped {pad} bytes padding."); + } + else + { + cd.Planes = Array.Empty(); + } + + // same_pos_vertex_offsets + int numOffsets = info.SamePosVertexOffsetsAlloc / 2; + cd.SamePosVertexOffsets = new short[numOffsets]; + for (int i = 0; i < numOffsets; i++) + cd.SamePosVertexOffsets[i] = r.ReadInt16(); + pad = (0x10L - (ms.Position % 0x10L)) % 0x10L; + if (pad > 0) r.ReadBytes((int)pad); + Logger.Debug(logSrc, $" Read {numOffsets} same_pos offsets, skipped {pad} bytes padding."); + + // optional bone_links + if (info.WiAlloc > 0) + { + int numWeights = info.WiAlloc / 8; + cd.BoneLinks = new VertexBoneLink[numWeights]; + for (int i = 0; i < numWeights; i++) + { + var vbl = new VertexBoneLink + { + Weights = new byte[4], + Bones = new byte[4] + }; + for (int w = 0; w < 4; w++) + vbl.Weights[w] = r.ReadByte(); + for (int b = 0; b < 4; b++) + vbl.Bones[b] = r.ReadByte(); + cd.BoneLinks[i] = vbl; + } + pad = (0x10L - (ms.Position % 0x10L)) % 0x10L; + if (pad > 0) r.ReadBytes((int)pad); + } + else + { + cd.BoneLinks = Array.Empty(); + } + + // optional orig_map + if ((lod.Flags & (uint)LodFlags.OrigMap) != 0) + { + int numOrig = lod.NumVertices; + cd.OrigMap = new short[numOrig]; + for (int i = 0; i < numOrig; i++) + cd.OrigMap[i] = r.ReadInt16(); + pad = (0x10L - (ms.Position % 0x10L)) % 0x10L; + if (pad > 0) r.ReadBytes((int)pad); + Logger.Debug(logSrc, $" Read {numOrig} orig_map entries, skipped {pad} bytes padding."); + } + else + { + cd.OrigMap = Array.Empty(); + Logger.Debug(logSrc, $" No orig_map (OrigMap flag not set)."); + } + + lod.Chunks[ci] = cd; + } + + if (lod.NumPropPoints > 0) + { + lod.PropPoints = new PropPoint[lod.NumPropPoints]; + for (int p = 0; p < lod.NumPropPoints; p++) + { + var pp = new PropPoint + { + Name = ReadFixedAscii(r, 0x44), // 68 bytes, strz + Orientation = new Quaternion( + r.ReadSingle(), + r.ReadSingle(), + r.ReadSingle(), + r.ReadSingle()), + Position = new Vector3( + r.ReadSingle(), + r.ReadSingle(), + r.ReadSingle()), + ParentIndex = r.ReadInt32() + }; + lod.PropPoints[p] = pp; + Logger.Debug(logSrc, $" PropPoint[{p}]: Name=\"{pp.Name}\", Position={pp.Position}, ParentIndex={pp.ParentIndex}"); + } + } + else + { + lod.PropPoints = Array.Empty(); + Logger.Debug(logSrc, $" No prop_points in data_block (NumPropPoints=0)."); + } + + Logger.Debug(logSrc, " <- Exiting UnpackLodDataBlock(...)"); + } + + private static CollisionSphere ParseCsphere(BinaryReader reader, int secSize) + { + var cs = new CollisionSphere(); + cs.Name = ReadFixedAscii(reader, 24); + cs.ParentIndex = reader.ReadInt32(); + float x = reader.ReadSingle(); + float y = reader.ReadSingle(); + float z = reader.ReadSingle(); + cs.Position = new Vector3(x, y, z); + cs.Radius = reader.ReadSingle(); + + // Advance if there is leftover padding + long bytesRead = 24 + 4 + 12 + 4; // 44 bytes total + long toSkip = secSize - bytesRead; + if (toSkip > 0) + reader.BaseStream.Seek(toSkip, SeekOrigin.Current); + + return cs; + } + + private static void ParseBones(BinaryReader reader, Mesh mesh, int secSize) + { + long startPos = reader.BaseStream.Position; + int numBones = reader.ReadInt32(); + Logger.Debug(logSrc, $" → numBones = {numBones}"); + + for (int i = 0; i < numBones; i++) + { + string boneName = ReadFixedAscii(reader, 24); + + float qx = reader.ReadSingle(); + float qy = reader.ReadSingle(); + float qz = reader.ReadSingle(); + float qw = reader.ReadSingle(); + var baseRot = new Quaternion(qx, qy, qz, qw); + baseRot = Quaternion.Normalize(baseRot); + + float tx = reader.ReadSingle(); + float ty = reader.ReadSingle(); + float tz = reader.ReadSingle(); + var baseTrans = new Vector3(tx, ty, tz); + + int parentIndex = reader.ReadInt32(); + + var bone = new Bone + { + Name = boneName, + BaseRotation = baseRot, + BaseTranslation = baseTrans, + ParentIndex = parentIndex + }; + mesh.Bones.Add(bone); + + Logger.Debug(logSrc, + $" Bone[{i}]: Name=\"{boneName}\", Parent={parentIndex}, Rot=({qx},{qy},{qz},{qw}), Trans=({tx},{ty},{tz})"); + } + + // Now compute and normalize RH‐corrected positions: + ComputeBoneWorldPositions(mesh); + NormalizeBonePositions(mesh); + + long consumed = reader.BaseStream.Position - startPos; + long toSkip = secSize - consumed; + if (toSkip > 0) + reader.BaseStream.Seek(toSkip, SeekOrigin.Current); + } + + public static void ComputeBoneWorldPositions(Mesh mesh) + { + int count = mesh.Bones.Count; + mesh.BoneWorldPositions = new Vector3[count]; + + for (int i = 0; i < count; i++) + { + Bone b = mesh.Bones[i]; + + // 1) Convert RF quaternion → RH quaternion by flipping X + // flip handedness + Quaternion q_rf = b.BaseRotation; + var q_rh = new Quaternion( + -q_rf.X, // flip X + q_rf.Y, + q_rf.Z, + q_rf.W + ); + q_rh = Quaternion.Normalize(q_rh); + + // 2) Convert RF translation → RH translation by flipping X + Vector3 t_rf = b.BaseTranslation; + var t_rh = new Vector3( + -t_rf.X, // flip X + t_rf.Y, + t_rf.Z + ); + + // 3) Compute bone‐origin in world (RH) = -( R_rh^{-1} * t_rh ) + // Since R_rh is normalized, R_rh^{-1} == Quaternion.Conjugate(R_rh) + Quaternion invRot = Quaternion.Conjugate(q_rh); + Vector3 rotated = Vector3.Transform(t_rh, invRot); + Vector3 worldOrig = -rotated; + + mesh.BoneWorldPositions[i] = worldOrig; + } + } + + public static void NormalizeBonePositions(Mesh mesh) + { + // Find the root bone (ParentIndex < 0) + int rootIndex = -1; + for (int i = 0; i < mesh.Bones.Count; i++) + { + if (mesh.Bones[i].ParentIndex < 0) + { + rootIndex = i; + break; + } + } + + if (rootIndex < 0) + { + Logger.Warn(logSrc, "No root bone found; skipping normalization."); + return; + } + + Vector3 rootPos = mesh.BoneWorldPositions[rootIndex]; + int count = mesh.Bones.Count; + mesh.BoneModelPositions = new Vector3[count]; + + for (int i = 0; i < count; i++) + { + mesh.BoneModelPositions[i] = mesh.BoneWorldPositions[i] - rootPos; + } + } + } } \ No newline at end of file diff --git a/parsers/parser_utils/RFGeometryParser.cs b/parsers/parser_utils/RFGeometryParser.cs index a94d6e9..40f74f0 100644 --- a/parsers/parser_utils/RFGeometryParser.cs +++ b/parsers/parser_utils/RFGeometryParser.cs @@ -10,557 +10,557 @@ namespace redux.parsers.parser_utils { - class RFGeometryParser - { - private const string logSrc = "RFGeometryParser"; - public static Brush ReadStaticGeometry(BinaryReader reader, int rfl_version) - { - var brush = new Brush(); - var (vertices, uvs, faces, indices, solid) = RFGeometryParser.ReadGeometryBody(reader, rfl_version, true); - - brush.Vertices = vertices; // your per‐vertex positions - brush.UVs = uvs; // your per‐corner UVs - brush.Indices = indices; // the index buffer - brush.Solid = solid; // contains solid.Textures and solid.Faces - - Logger.Dev(logSrc, $"Parsed static geometry {brush.UID} with {brush.Vertices.Count} verticies, {brush.UVs.Count} faces, flags {brush.Solid.Flags}, life {brush.Solid.Life}, and state {brush.Solid.State}"); - - return brush; - } - public static Brush ReadBrush(BinaryReader reader, int rfl_version) - { - var brush = new Brush(); - - // Brush UID - brush.UID = reader.ReadInt32(); - Logger.Dev(logSrc, $"Reading brush {brush.UID}"); - - // TBD - unsure on this approach. It fixes an error parsing RF2 l06s2.rfl but I'm not convinced this is the right way to do it - /*if (brush.UID == 1) - { - reader.BaseStream.Seek(-4, SeekOrigin.Current); - Logger.Warn(logSrc, "Detected either invalid brush or world static geometry inside brush section, skipping"); - return null; - }*/ - - // Brush position - brush.Position = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - Logger.Debug(logSrc, $"Brush {brush.UID} position: {brush.Position}"); - - // Brush rotation matrix - Vector3 fwd = new(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - Vector3 right = new(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - Vector3 up = new(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - brush.RotationMatrix = new Matrix4x4( - right.X, right.Y, right.Z, 0, - up.X, up.Y, up.Z, 0, - fwd.X, fwd.Y, fwd.Z, 0, - 0, 0, 0, 1 - ); - - var (vertices, uvs, faces, indices, solid) = RFGeometryParser.ReadGeometryBody(reader, rfl_version, false); - - brush.Vertices = vertices; // your per‐vertex positions - brush.UVs = uvs; // your per‐corner UVs - brush.Indices = indices; // the index buffer - brush.Solid = solid; // contains solid.Textures and solid.Faces - - if (reader.BaseStream.Position + 12 <= reader.BaseStream.Length) - { - brush.Solid.Flags = reader.ReadUInt32(); - brush.Solid.Life = reader.ReadInt32(); - brush.Solid.State = reader.ReadInt32(); - } - - Logger.Dev(logSrc, $"Parsed brush {brush.UID} with {brush.Vertices.Count} verticies, {brush.UVs.Count} faces, flags {brush.Solid.Flags}, life {brush.Solid.Life}, and state {brush.Solid.State}"); - - // if unk_c is set - // Unsure exactly what this flag is but brushes with it have a bunch of additional fields that need to be figured out - // in dmpc06.rfl this is set for liquid surfaces - const uint UNK_C_MASK = 0x000C; // bits 2+3 - if ((brush.Solid.Flags & UNK_C_MASK) == UNK_C_MASK) - { - var unk1 = reader.ReadUInt32(); - var unk2 = reader.ReadUInt32(); - var unk3 = reader.ReadUInt32(); - var unk4 = reader.ReadUInt32(); - var unk5 = reader.ReadUInt32(); - var unk6r = reader.ReadByte(); // probably a color + class RFGeometryParser + { + private const string logSrc = "RFGeometryParser"; + public static Brush ReadStaticGeometry(BinaryReader reader, int rfl_version) + { + var brush = new Brush(); + var (vertices, uvs, faces, indices, solid) = RFGeometryParser.ReadGeometryBody(reader, rfl_version, true); + + brush.Vertices = vertices; // your per‐vertex positions + brush.UVs = uvs; // your per‐corner UVs + brush.Indices = indices; // the index buffer + brush.Solid = solid; // contains solid.Textures and solid.Faces + + Logger.Dev(logSrc, $"Parsed static geometry {brush.UID} with {brush.Vertices.Count} verticies, {brush.UVs.Count} faces, flags {brush.Solid.Flags}, life {brush.Solid.Life}, and state {brush.Solid.State}"); + + return brush; + } + public static Brush ReadBrush(BinaryReader reader, int rfl_version) + { + var brush = new Brush(); + + // Brush UID + brush.UID = reader.ReadInt32(); + Logger.Dev(logSrc, $"Reading brush {brush.UID}"); + + // TBD - unsure on this approach. It fixes an error parsing RF2 l06s2.rfl but I'm not convinced this is the right way to do it + /*if (brush.UID == 1) + { + reader.BaseStream.Seek(-4, SeekOrigin.Current); + Logger.Warn(logSrc, "Detected either invalid brush or world static geometry inside brush section, skipping"); + return null; + }*/ + + // Brush position + brush.Position = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + Logger.Debug(logSrc, $"Brush {brush.UID} position: {brush.Position}"); + + // Brush rotation matrix + Vector3 fwd = new(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + Vector3 right = new(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + Vector3 up = new(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + brush.RotationMatrix = new Matrix4x4( + right.X, right.Y, right.Z, 0, + up.X, up.Y, up.Z, 0, + fwd.X, fwd.Y, fwd.Z, 0, + 0, 0, 0, 1 + ); + + var (vertices, uvs, faces, indices, solid) = RFGeometryParser.ReadGeometryBody(reader, rfl_version, false); + + brush.Vertices = vertices; // your per‐vertex positions + brush.UVs = uvs; // your per‐corner UVs + brush.Indices = indices; // the index buffer + brush.Solid = solid; // contains solid.Textures and solid.Faces + + if (reader.BaseStream.Position + 12 <= reader.BaseStream.Length) + { + brush.Solid.Flags = reader.ReadUInt32(); + brush.Solid.Life = reader.ReadInt32(); + brush.Solid.State = reader.ReadInt32(); + } + + Logger.Dev(logSrc, $"Parsed brush {brush.UID} with {brush.Vertices.Count} verticies, {brush.UVs.Count} faces, flags {brush.Solid.Flags}, life {brush.Solid.Life}, and state {brush.Solid.State}"); + + // if unk_c is set + // Unsure exactly what this flag is but brushes with it have a bunch of additional fields that need to be figured out + // in dmpc06.rfl this is set for liquid surfaces + const uint UNK_C_MASK = 0x000C; // bits 2+3 + if ((brush.Solid.Flags & UNK_C_MASK) == UNK_C_MASK) + { + var unk1 = reader.ReadUInt32(); + var unk2 = reader.ReadUInt32(); + var unk3 = reader.ReadUInt32(); + var unk4 = reader.ReadUInt32(); + var unk5 = reader.ReadUInt32(); + var unk6r = reader.ReadByte(); // probably a color var unk6g = reader.ReadByte(); var unk6b = reader.ReadByte(); var unk6a = reader.ReadByte(); var unk7 = reader.ReadSingle(); - var unk8 = reader.ReadByte(); - Logger.Warn(logSrc, $"Brush {brush.UID} has unk_c flag set, read unk values: {unk1}, {unk2}, {unk3}, {unk4}, {unk5}, {unk6r},{unk6g},{unk6b},{unk6a}, {unk7}, {unk8}"); - } - - return brush; - } - public static (List vertices, List uvs, List faces, List indices, Solid solid) ReadGeometryBody(BinaryReader reader, int rfl_version, bool static_geo) - { - var solid = new Solid(); // Solid to hold parsed geometry data - - bool isRF1 = RflUtils.IsRF1(rfl_version); - bool isRF2 = RflUtils.IsRF2(rfl_version); - - Logger.Dev(logSrc, $"RF1 {isRF1}, RF2 {isRF2}"); - - // Skip unknown uint + modifiability (RF1 only) - if (isRF1 && rfl_version >= 0xC8) - reader.BaseStream.Seek(8, SeekOrigin.Current); - - string name = ReadVString(reader); // geo name is typically blank - - // Skip unknown data (RF2), old modifiability (RF1 before 0xC8) - if (isRF2 || (isRF1 && rfl_version < 0xC8)) - reader.ReadUInt32(); - - // Textures - int numTextures = reader.ReadInt32(); - Logger.Debug(logSrc, $"numTextures: {numTextures}"); - for (int i = 0; i < numTextures; i++) - { - string tex = ReadVString(reader); - - if (isRF2 && Config.TranslateRF2Textures) - { - string translatedTex = RF2TextureTranslator.TranslateRF2Texture(tex); - Logger.Debug(logSrc, $"Texture {i}: \"{tex}\" → \"{translatedTex}\""); - solid.Textures.Add(translatedTex); - } - else if (isRF2 && Config.InsertRF2TexturePrefix) - { - string translatedTex = RF2TextureTranslator.InsertRxPrefix(tex); - Logger.Debug(logSrc, $"Texture {i}: \"{tex}\" → \"{translatedTex}\""); - solid.Textures.Add(translatedTex); - } - else - { - Logger.Debug(logSrc, $"Texture {i}: \"{tex}\""); - solid.Textures.Add(tex); - } - } - - var faceScrollTable = new Dictionary(); - // Skip face scroll data (RF1 only) - if (isRF1 && rfl_version >= 0xB4) - { - int numFaceScrollData = reader.ReadInt32(); - Logger.Debug(logSrc, $"numFaceScrollData: {numFaceScrollData}"); - for (int i = 0; i < numFaceScrollData; i++) - { - int faceId = reader.ReadInt32(); - float uVel = reader.ReadSingle(); - float vVel = reader.ReadSingle(); - Logger.Dev(logSrc, $"Read face scroll → faceId={faceId}, U={uVel}, V={vVel}"); - faceScrollTable[faceId] = (uVel, vVel); - } - } - else if (isRF1 && rfl_version < 0xB4) - { - int numUnkData = reader.ReadInt32(); - Logger.Debug(logSrc, $"numUnkData: {numUnkData}"); - for (int i = 0; i < numUnkData; i++) - reader.BaseStream.Seek(0x29, SeekOrigin.Current); - } - - // Skip room data (static geometry only) - int numRooms = reader.ReadInt32(); - Logger.Debug(logSrc, $"numRooms: {numRooms}"); - for (int i = 0; i < numRooms; i++) - { - if (isRF2) - { - int id = reader.ReadInt32(); - Vector3 aabbMin = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - Vector3 aabbMax = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - //int unk1 = reader.ReadInt32(); - byte byte1 = reader.ReadByte(); - byte byte2 = reader.ReadByte(); - byte byte3 = reader.ReadByte(); - byte byte4 = reader.ReadByte(); - //byte isLiquidRoom = reader.ReadByte(); - //byte hasAmbientLight = reader.ReadByte(); - //byte isSubroom = reader.ReadByte(); - //byte hasAlpha = reader.ReadByte(); - float life = reader.ReadSingle(); - string eax_name = ReadVString(reader); - float unk3 = reader.ReadSingle(); - float unk4 = reader.ReadSingle(); - float unk5 = reader.ReadSingle(); - int unk6 = reader.ReadInt32(); - float unk7 = reader.ReadSingle(); - int unk8 = reader.ReadInt32(); - //Logger.Warn(logSrc, $"byte1={byte1}, byte2={byte2}, byte3={byte3}, byte4={byte4}"); - - // pre-284 rfls have a conditional check here, but we don't have any of those so don't worry about it - float unk9 = reader.ReadSingle(); - float unk10 = reader.ReadSingle(); - float unk11 = reader.ReadSingle(); - float unk12 = reader.ReadSingle(); - //Logger.Debug(logSrc, $"unk3={unk3}, unk4={unk4}, unk5={unk5}, unk6={unk6}, unk7={unk7}, unk8={unk8}, unk9={unk9}, unk10={unk10}, unk11={unk11}, unk12={unk12}"); - } - else - { - int id = reader.ReadInt32(); - Vector3 aabbMin = new(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - Vector3 aabbMax = new(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); - byte isSkyroom = reader.ReadByte(); - byte isCold = reader.ReadByte(); - byte isOutside = reader.ReadByte(); - byte isAirlock = reader.ReadByte(); - byte isLiquidRoom = reader.ReadByte(); - byte hasAmbientLight = reader.ReadByte(); - byte isSubroom = reader.ReadByte(); - byte hasAlpha = reader.ReadByte(); - float life = reader.ReadSingle(); - - // Read eax_effect if version >= 0xB4 - if (rfl_version >= 0xB4) - { - string eaxEffect = ReadVString(reader); - } - - // Read liquid_properties - if (isLiquidRoom == 1) - { - float depth = reader.ReadSingle(); - byte r = reader.ReadByte(); - byte g = reader.ReadByte(); - byte b = reader.ReadByte(); - byte a = reader.ReadByte(); - string surfaceTexture = ReadVString(reader); - float visibility = reader.ReadSingle(); - int liquidType = reader.ReadInt32(); - int liquidAlpha = reader.ReadInt32(); - byte containsPlankton = reader.ReadByte(); - int ppmU = reader.ReadInt32(); - int ppmV = reader.ReadInt32(); - float angle = reader.ReadSingle(); - int waveform = reader.ReadInt32(); - float scrollU = reader.ReadSingle(); - float scrollV = reader.ReadSingle(); - } - - // Read ambient_color - if (hasAmbientLight == 1) - { - byte r = reader.ReadByte(); - byte g = reader.ReadByte(); - byte b = reader.ReadByte(); - byte a = reader.ReadByte(); - } - } - } - - // Skip subroom data - int numSubroomLinks = reader.ReadInt32(); - Logger.Debug(logSrc, $"numSubroomLinks: {numSubroomLinks}"); - for (int i = 0; i < numSubroomLinks; i++) - { - int roomID = reader.ReadInt32(); - int subroomCount = reader.ReadInt32(); - for (int j = 0; j < subroomCount; j++) - reader.ReadInt32(); - } - - // Skip uroom data (RF2 only) - if (isRF2) { - int numURoomLinks = reader.ReadInt32(); - Logger.Debug(logSrc, $"numURoomLinks: {numURoomLinks}"); - reader.BaseStream.Seek(numURoomLinks * 8, SeekOrigin.Current); - } - - // Skip portal data - int numPortals = reader.ReadInt32(); - Logger.Debug(logSrc, $"numPortals: {numPortals}"); - reader.BaseStream.Seek(numPortals * 32, SeekOrigin.Current); - - // Read raw vertices - int numRawVerts = reader.ReadInt32(); - Logger.Debug(logSrc, $"numRawVerts: {numRawVerts}"); - var rawVerts = new List(numRawVerts); - for (int i = 0; i < numRawVerts; i++) - { - float x = reader.ReadSingle(); - float y = reader.ReadSingle(); - float z = reader.ReadSingle(); - rawVerts.Add(new Vector3(x, y, z)); - } - - // Deduplicated vertex position store - merges verticies on the same brush and avoids tearing - var positionMap = new Dictionary<(int, int, int), int>(); - var uniqueVerts = new List(); - var indices = new List(); - var uvs = new List(); - - // Read raw faces - int numFaces = reader.ReadInt32(); - Logger.Debug(logSrc, $"numFaces: {numFaces}"); - for (int i = 0; i < numFaces; i++) - { - long faceStart = reader.BaseStream.Position; - Logger.Debug(logSrc, $"Reading face {i} at 0x{faceStart:X}"); - - reader.BaseStream.Seek(16, SeekOrigin.Current); // skip plane normal and dist - int textureIndex = reader.ReadInt32(); // texture index - //reader.BaseStream.Seek(12, SeekOrigin.Current); // unused data surface_index, face_id, unk - - int surfaceIndex = reader.ReadInt32(); - int faceId = reader.ReadInt32(); - int unk12 = reader.ReadInt32(); - - Logger.Debug(logSrc, $"RF1 Face {i} surfaceindex {surfaceIndex}, id {faceId}, unk12 {unk12}"); - - if (isRF1) - { - reader.ReadUInt32(); // reserved1 (RF1 only) - int portalIndex = reader.ReadInt32(); // portal_index (RF1 only) - ushort faceFlagsRF1 = reader.ReadUInt16(); // RF1 faceFlags is a 16‐bit value - reader.ReadUInt16(); // reserved2 (RF1 only) - uint smoothingGroups = reader.ReadUInt32(); - int roomIndex = reader.ReadInt32(); - //uint faceFlags32 = faceFlagsRF1; // redux stores flags in a 32-bit structure to keep consistent with RF2 - int vertCount = reader.ReadInt32(); - - Logger.Debug(logSrc, $"RF1 face[{i}]: texture={textureIndex}, faceFlags=0x{faceFlagsRF1:X}, vertCount={vertCount}, smoothingGroups={smoothingGroups}"); - - var faceVerts = new List(vertCount); - var faceUVs = new List(vertCount); - - // set flag bools - bool isInvisible = (faceFlagsRF1 & 0x2000) != 0; - bool isFullbright = (faceFlagsRF1 & 0x20) != 0; - bool isHole = (faceFlagsRF1 & 0x80) != 0; - bool isAlpha = (faceFlagsRF1 & 0x40) != 0; - bool isDetail = (faceFlagsRF1 & 0x0010) != 0; - bool isLiquid = (faceFlagsRF1 & 0x04) != 0; - bool isPortal = (faceFlagsRF1 & 0x1) != 0; - bool isSky = (faceFlagsRF1 & 0x01) != 0; - - for (int vi = 0; vi < vertCount; vi++) - { - int rawIdx = reader.ReadInt32(); // index into rawVerts - float u = reader.ReadSingle(); - float v = reader.ReadSingle(); - - // lmap coords are only present when face is NOT fullbright or invisible - if (static_geo && !isFullbright && !isInvisible) - { - float lmapu = reader.ReadSingle(); - float lmapv = reader.ReadSingle(); - } - - - if (rawIdx < 0 || rawIdx >= rawVerts.Count) - continue; - - Vector3 pos = rawVerts[rawIdx]; - var key = ((int)(pos.X * 1000), (int)(pos.Y * 1000), (int)(pos.Z * 1000)); - if (!positionMap.TryGetValue(key, out int xi)) - { - xi = uniqueVerts.Count; - uniqueVerts.Add(pos); - positionMap[key] = xi; - } - - faceVerts.Add(xi); - faceUVs.Add(new Vector2(u, v)); - uvs.Add(new Vector2(u, v)); - indices.Add(xi); - } - - // Face visibility filtering - if (!Config.IncludeInvisibleFaces && isInvisible) continue; - if (!Config.IncludeHoleFaces && isHole) continue; - if (!Config.IncludeAlphaFaces && isAlpha) continue; - if (!Config.IncludeDetailFaces && isDetail) continue; - if (!Config.IncludeLiquidFaces && isLiquid) continue; - if (!Config.IncludePortalFaces && isPortal) continue; - if (!Config.IncludeSkyFaces && isSky) continue; - - // Lookup scroll speeds from the dictionary (default to (0,0) if not found) - faceScrollTable.TryGetValue(faceId, out var scrollUV); - - // Triangulate or add ngon faces - if (Config.TriangulatePolygons && faceVerts.Count > 3) - { - Logger.Debug(logSrc, $"Triangulating RF1 ngon (count={faceVerts.Count})"); - for (int k = 1; k < faceVerts.Count - 1; k++) - { - solid.Faces.Add(new Face - { - TextureIndex = textureIndex, - FaceId = faceId, - ScrollU = scrollUV.U, - ScrollV = scrollUV.V, - Vertices = new List { faceVerts[0], faceVerts[k], faceVerts[k + 1] }, - UVs = new List { faceUVs[0], faceUVs[k], faceUVs[k + 1] }, - SmoothingGroups = smoothingGroups, - FaceFlags = (ushort)faceFlagsRF1 - }); - } - } - else - { - solid.Faces.Add(new Face - { - TextureIndex = textureIndex, - FaceId = faceId, - ScrollU = scrollUV.U, - ScrollV = scrollUV.V, - Vertices = faceVerts, - UVs = faceUVs, - SmoothingGroups = smoothingGroups, - FaceFlags = (ushort)faceFlagsRF1 - }); - } - - } // end RF1 - else - { - uint faceFlagsRF2 = reader.ReadUInt32(); - uint smoothingGroups = reader.ReadUInt32(); - - if ((faceFlagsRF2 & 0x8000) != 0) - { - reader.ReadSingle(); // skip a float - float tmp = reader.ReadSingle(); - if (Math.Abs(tmp - 1.0f) < 0.001f) faceFlagsRF2 |= 0x100000; - else if (Math.Abs(tmp - 1.35f) < 0.001f) faceFlagsRF2 |= 0x200000; - else if (Math.Abs(tmp - 1.5f) < 0.001f) faceFlagsRF2 |= 0x400000; - else faceFlagsRF2 |= 0x800000; - } - - // ¯\_(ツ)_/¯ - if (rfl_version >= 0x127) - { - reader.BaseStream.Seek(3, SeekOrigin.Current); // skip 3 bytes - float flagDecider = reader.ReadSingle(); - if (Math.Abs(flagDecider) > 0.0001f) - { - faceFlagsRF2 |= 0x4000000; - } - } - - //reader.ReadUInt32(); // room index - - int roomIndex = reader.ReadInt32(); - //int faceId2 = reader.ReadInt32(); - //reader.ReadInt32(); // reserved - //reader.ReadInt32(); // reserved - - int vertCount = reader.ReadInt32(); - - Logger.Debug(logSrc, $"RF2 face[{i}]: faceid2={faceId}, texture={textureIndex}, faceFlags=0x{faceFlagsRF2:X}, vertCount={vertCount}, smoothingGroups={smoothingGroups}"); - - var faceVerts = new List(vertCount); - var faceUVs = new List(vertCount); - - bool isInvisible = (faceFlagsRF2 & 0x2000) != 0; - bool isFullbright = (faceFlagsRF2 & 0x20) != 0; - bool isHole = (faceFlagsRF2 & 0x80) != 0; - bool isAlpha = (faceFlagsRF2 & 0x40) != 0; - bool isDetail = (faceFlagsRF2 & 0x0010) != 0; - bool isLiquid = (faceFlagsRF2 & 0x04) != 0; - bool isPortal = (faceFlagsRF2 & 0x1) != 0; - bool isSky = (faceFlagsRF2 & 0x01) != 0; - bool isMirrored = (faceFlagsRF2 & 0x02) != 0; - - for (int vi = 0; vi < vertCount; vi++) - { - uint rawIdx = reader.ReadUInt32(); - float u = reader.ReadSingle(); - float v = reader.ReadSingle(); - reader.BaseStream.Seek(4, SeekOrigin.Current); // vertex colour (RGBA) - - if (rawIdx >= rawVerts.Count) - continue; - - Vector3 pos = rawVerts[(int)rawIdx]; - var key = ((int)(pos.X * 1000), (int)(pos.Y * 1000), (int)(pos.Z * 1000)); - if (!positionMap.TryGetValue(key, out int xi)) - { - xi = uniqueVerts.Count; - uniqueVerts.Add(pos); - positionMap[key] = xi; - } - - faceVerts.Add(xi); - faceUVs.Add(new Vector2(u, v)); - uvs.Add(new Vector2(u, v)); - indices.Add(xi); - } - - // Face visibility filtering - if (!Config.IncludeInvisibleFaces && isInvisible) continue; - if (!Config.IncludeHoleFaces && isHole) continue; - if (!Config.IncludeAlphaFaces && isAlpha) continue; - if (!Config.IncludeDetailFaces && isDetail) continue; - if (!Config.IncludeLiquidFaces && isLiquid) continue; - if (!Config.IncludePortalFaces && isPortal) continue; - if (!Config.IncludeSkyFaces && isSky) continue; - - faceScrollTable.TryGetValue(faceId, out var scrollUV2); - - // Triangulate or add polygon faces for RF2 - if (Config.TriangulatePolygons && faceVerts.Count > 3) - { - Logger.Debug(logSrc, $"Triangulating RF2 ngon (count={faceVerts.Count})"); - for (int k = 1; k < faceVerts.Count - 1; k++) - { - solid.Faces.Add(new Face - { - TextureIndex = textureIndex, - FaceId = faceId, - ScrollU = scrollUV2.U, - ScrollV = scrollUV2.V, - Vertices = new List { faceVerts[0], faceVerts[k], faceVerts[k + 1] }, - UVs = new List { faceUVs[0], faceUVs[k], faceUVs[k + 1] }, - SmoothingGroups = smoothingGroups, - FaceFlags = (ushort)(faceFlagsRF2 & 0xFFFF) // cast to ushort for storage - }); - } - } - else - { - solid.Faces.Add(new Face - { - TextureIndex = textureIndex, - FaceId = faceId, - ScrollU = scrollUV2.U, - ScrollV = scrollUV2.V, - Vertices = faceVerts, - UVs = faceUVs, - SmoothingGroups = smoothingGroups, - FaceFlags = (ushort)(faceFlagsRF2 & 0xFFFF) - }); - } - } // end RF2 - - long faceEnd = reader.BaseStream.Position; - Logger.Debug(logSrc, $"Face {i} consumed {(faceEnd - faceStart)} bytes"); - } - - // Skip surface data (RF1 only) - if (isRF1) - { - int numSurfaces = reader.ReadInt32(); - Logger.Debug(logSrc, $"numSurfaces: {numSurfaces}"); - for (int i = 0; i < numSurfaces; i++) - reader.BaseStream.Seek(96, SeekOrigin.Current); - } - - // Skip old face scroll data (RF1 only) - if (isRF1 && rfl_version <= 0xB4) - { - int numOldFaceScrollData = reader.ReadInt32(); - Logger.Debug(logSrc, $"numOldFaceScrollData: {numOldFaceScrollData}"); - for (int i = 0; i < numOldFaceScrollData; i++) - reader.BaseStream.Seek(12, SeekOrigin.Current); - } - - return (uniqueVerts, uvs, solid.Faces, indices, solid); - } - } + var unk8 = reader.ReadByte(); + Logger.Warn(logSrc, $"Brush {brush.UID} has unk_c flag set, read unk values: {unk1}, {unk2}, {unk3}, {unk4}, {unk5}, {unk6r},{unk6g},{unk6b},{unk6a}, {unk7}, {unk8}"); + } + + return brush; + } + public static (List vertices, List uvs, List faces, List indices, Solid solid) ReadGeometryBody(BinaryReader reader, int rfl_version, bool static_geo) + { + var solid = new Solid(); // Solid to hold parsed geometry data + + bool isRF1 = RflUtils.IsRF1(rfl_version); + bool isRF2 = RflUtils.IsRF2(rfl_version); + + Logger.Dev(logSrc, $"RF1 {isRF1}, RF2 {isRF2}"); + + // Skip unknown uint + modifiability (RF1 only) + if (isRF1 && rfl_version >= 0xC8) + reader.BaseStream.Seek(8, SeekOrigin.Current); + + string name = ReadVString(reader); // geo name is typically blank + + // Skip unknown data (RF2), old modifiability (RF1 before 0xC8) + if (isRF2 || (isRF1 && rfl_version < 0xC8)) + reader.ReadUInt32(); + + // Textures + int numTextures = reader.ReadInt32(); + Logger.Debug(logSrc, $"numTextures: {numTextures}"); + for (int i = 0; i < numTextures; i++) + { + string tex = ReadVString(reader); + + if (isRF2 && Config.TranslateRF2Textures) + { + string translatedTex = RF2TextureTranslator.TranslateRF2Texture(tex); + Logger.Debug(logSrc, $"Texture {i}: \"{tex}\" → \"{translatedTex}\""); + solid.Textures.Add(translatedTex); + } + else if (isRF2 && Config.InsertRF2TexturePrefix) + { + string translatedTex = RF2TextureTranslator.InsertRxPrefix(tex); + Logger.Debug(logSrc, $"Texture {i}: \"{tex}\" → \"{translatedTex}\""); + solid.Textures.Add(translatedTex); + } + else + { + Logger.Debug(logSrc, $"Texture {i}: \"{tex}\""); + solid.Textures.Add(tex); + } + } + + var faceScrollTable = new Dictionary(); + // Skip face scroll data (RF1 only) + if (isRF1 && rfl_version >= 0xB4) + { + int numFaceScrollData = reader.ReadInt32(); + Logger.Debug(logSrc, $"numFaceScrollData: {numFaceScrollData}"); + for (int i = 0; i < numFaceScrollData; i++) + { + int faceId = reader.ReadInt32(); + float uVel = reader.ReadSingle(); + float vVel = reader.ReadSingle(); + Logger.Dev(logSrc, $"Read face scroll → faceId={faceId}, U={uVel}, V={vVel}"); + faceScrollTable[faceId] = (uVel, vVel); + } + } + else if (isRF1 && rfl_version < 0xB4) + { + int numUnkData = reader.ReadInt32(); + Logger.Debug(logSrc, $"numUnkData: {numUnkData}"); + for (int i = 0; i < numUnkData; i++) + reader.BaseStream.Seek(0x29, SeekOrigin.Current); + } + + // Skip room data (static geometry only) + int numRooms = reader.ReadInt32(); + Logger.Debug(logSrc, $"numRooms: {numRooms}"); + for (int i = 0; i < numRooms; i++) + { + if (isRF2) + { + int id = reader.ReadInt32(); + Vector3 aabbMin = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + Vector3 aabbMax = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + //int unk1 = reader.ReadInt32(); + byte byte1 = reader.ReadByte(); + byte byte2 = reader.ReadByte(); + byte byte3 = reader.ReadByte(); + byte byte4 = reader.ReadByte(); + //byte isLiquidRoom = reader.ReadByte(); + //byte hasAmbientLight = reader.ReadByte(); + //byte isSubroom = reader.ReadByte(); + //byte hasAlpha = reader.ReadByte(); + float life = reader.ReadSingle(); + string eax_name = ReadVString(reader); + float unk3 = reader.ReadSingle(); + float unk4 = reader.ReadSingle(); + float unk5 = reader.ReadSingle(); + int unk6 = reader.ReadInt32(); + float unk7 = reader.ReadSingle(); + int unk8 = reader.ReadInt32(); + //Logger.Warn(logSrc, $"byte1={byte1}, byte2={byte2}, byte3={byte3}, byte4={byte4}"); + + // pre-284 rfls have a conditional check here, but we don't have any of those so don't worry about it + float unk9 = reader.ReadSingle(); + float unk10 = reader.ReadSingle(); + float unk11 = reader.ReadSingle(); + float unk12 = reader.ReadSingle(); + //Logger.Debug(logSrc, $"unk3={unk3}, unk4={unk4}, unk5={unk5}, unk6={unk6}, unk7={unk7}, unk8={unk8}, unk9={unk9}, unk10={unk10}, unk11={unk11}, unk12={unk12}"); + } + else + { + int id = reader.ReadInt32(); + Vector3 aabbMin = new(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + Vector3 aabbMax = new(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); + byte isSkyroom = reader.ReadByte(); + byte isCold = reader.ReadByte(); + byte isOutside = reader.ReadByte(); + byte isAirlock = reader.ReadByte(); + byte isLiquidRoom = reader.ReadByte(); + byte hasAmbientLight = reader.ReadByte(); + byte isSubroom = reader.ReadByte(); + byte hasAlpha = reader.ReadByte(); + float life = reader.ReadSingle(); + + // Read eax_effect if version >= 0xB4 + if (rfl_version >= 0xB4) + { + string eaxEffect = ReadVString(reader); + } + + // Read liquid_properties + if (isLiquidRoom == 1) + { + float depth = reader.ReadSingle(); + byte r = reader.ReadByte(); + byte g = reader.ReadByte(); + byte b = reader.ReadByte(); + byte a = reader.ReadByte(); + string surfaceTexture = ReadVString(reader); + float visibility = reader.ReadSingle(); + int liquidType = reader.ReadInt32(); + int liquidAlpha = reader.ReadInt32(); + byte containsPlankton = reader.ReadByte(); + int ppmU = reader.ReadInt32(); + int ppmV = reader.ReadInt32(); + float angle = reader.ReadSingle(); + int waveform = reader.ReadInt32(); + float scrollU = reader.ReadSingle(); + float scrollV = reader.ReadSingle(); + } + + // Read ambient_color + if (hasAmbientLight == 1) + { + byte r = reader.ReadByte(); + byte g = reader.ReadByte(); + byte b = reader.ReadByte(); + byte a = reader.ReadByte(); + } + } + } + + // Skip subroom data + int numSubroomLinks = reader.ReadInt32(); + Logger.Debug(logSrc, $"numSubroomLinks: {numSubroomLinks}"); + for (int i = 0; i < numSubroomLinks; i++) + { + int roomID = reader.ReadInt32(); + int subroomCount = reader.ReadInt32(); + for (int j = 0; j < subroomCount; j++) + reader.ReadInt32(); + } + + // Skip uroom data (RF2 only) + if (isRF2) { + int numURoomLinks = reader.ReadInt32(); + Logger.Debug(logSrc, $"numURoomLinks: {numURoomLinks}"); + reader.BaseStream.Seek(numURoomLinks * 8, SeekOrigin.Current); + } + + // Skip portal data + int numPortals = reader.ReadInt32(); + Logger.Debug(logSrc, $"numPortals: {numPortals}"); + reader.BaseStream.Seek(numPortals * 32, SeekOrigin.Current); + + // Read raw vertices + int numRawVerts = reader.ReadInt32(); + Logger.Debug(logSrc, $"numRawVerts: {numRawVerts}"); + var rawVerts = new List(numRawVerts); + for (int i = 0; i < numRawVerts; i++) + { + float x = reader.ReadSingle(); + float y = reader.ReadSingle(); + float z = reader.ReadSingle(); + rawVerts.Add(new Vector3(x, y, z)); + } + + // Deduplicated vertex position store - merges verticies on the same brush and avoids tearing + var positionMap = new Dictionary<(int, int, int), int>(); + var uniqueVerts = new List(); + var indices = new List(); + var uvs = new List(); + + // Read raw faces + int numFaces = reader.ReadInt32(); + Logger.Debug(logSrc, $"numFaces: {numFaces}"); + for (int i = 0; i < numFaces; i++) + { + long faceStart = reader.BaseStream.Position; + Logger.Debug(logSrc, $"Reading face {i} at 0x{faceStart:X}"); + + reader.BaseStream.Seek(16, SeekOrigin.Current); // skip plane normal and dist + int textureIndex = reader.ReadInt32(); // texture index + //reader.BaseStream.Seek(12, SeekOrigin.Current); // unused data surface_index, face_id, unk + + int surfaceIndex = reader.ReadInt32(); + int faceId = reader.ReadInt32(); + int unk12 = reader.ReadInt32(); + + Logger.Debug(logSrc, $"RF1 Face {i} surfaceindex {surfaceIndex}, id {faceId}, unk12 {unk12}"); + + if (isRF1) + { + reader.ReadUInt32(); // reserved1 (RF1 only) + int portalIndex = reader.ReadInt32(); // portal_index (RF1 only) + ushort faceFlagsRF1 = reader.ReadUInt16(); // RF1 faceFlags is a 16‐bit value + reader.ReadUInt16(); // reserved2 (RF1 only) + uint smoothingGroups = reader.ReadUInt32(); + int roomIndex = reader.ReadInt32(); + //uint faceFlags32 = faceFlagsRF1; // redux stores flags in a 32-bit structure to keep consistent with RF2 + int vertCount = reader.ReadInt32(); + + Logger.Debug(logSrc, $"RF1 face[{i}]: texture={textureIndex}, faceFlags=0x{faceFlagsRF1:X}, vertCount={vertCount}, smoothingGroups={smoothingGroups}"); + + var faceVerts = new List(vertCount); + var faceUVs = new List(vertCount); + + // set flag bools + bool isInvisible = (faceFlagsRF1 & 0x2000) != 0; + bool isFullbright = (faceFlagsRF1 & 0x20) != 0; + bool isHole = (faceFlagsRF1 & 0x80) != 0; + bool isAlpha = (faceFlagsRF1 & 0x40) != 0; + bool isDetail = (faceFlagsRF1 & 0x0010) != 0; + bool isLiquid = (faceFlagsRF1 & 0x04) != 0; + bool isPortal = (faceFlagsRF1 & 0x1) != 0; + bool isSky = (faceFlagsRF1 & 0x01) != 0; + + for (int vi = 0; vi < vertCount; vi++) + { + int rawIdx = reader.ReadInt32(); // index into rawVerts + float u = reader.ReadSingle(); + float v = reader.ReadSingle(); + + // lmap coords are only present when face is NOT fullbright or invisible + if (static_geo && !isFullbright && !isInvisible) + { + float lmapu = reader.ReadSingle(); + float lmapv = reader.ReadSingle(); + } + + + if (rawIdx < 0 || rawIdx >= rawVerts.Count) + continue; + + Vector3 pos = rawVerts[rawIdx]; + var key = ((int)(pos.X * 1000), (int)(pos.Y * 1000), (int)(pos.Z * 1000)); + if (!positionMap.TryGetValue(key, out int xi)) + { + xi = uniqueVerts.Count; + uniqueVerts.Add(pos); + positionMap[key] = xi; + } + + faceVerts.Add(xi); + faceUVs.Add(new Vector2(u, v)); + uvs.Add(new Vector2(u, v)); + indices.Add(xi); + } + + // Face visibility filtering + if (!Config.IncludeInvisibleFaces && isInvisible) continue; + if (!Config.IncludeHoleFaces && isHole) continue; + if (!Config.IncludeAlphaFaces && isAlpha) continue; + if (!Config.IncludeDetailFaces && isDetail) continue; + if (!Config.IncludeLiquidFaces && isLiquid) continue; + if (!Config.IncludePortalFaces && isPortal) continue; + if (!Config.IncludeSkyFaces && isSky) continue; + + // Lookup scroll speeds from the dictionary (default to (0,0) if not found) + faceScrollTable.TryGetValue(faceId, out var scrollUV); + + // Triangulate or add ngon faces + if (Config.TriangulatePolygons && faceVerts.Count > 3) + { + Logger.Debug(logSrc, $"Triangulating RF1 ngon (count={faceVerts.Count})"); + for (int k = 1; k < faceVerts.Count - 1; k++) + { + solid.Faces.Add(new Face + { + TextureIndex = textureIndex, + FaceId = faceId, + ScrollU = scrollUV.U, + ScrollV = scrollUV.V, + Vertices = new List { faceVerts[0], faceVerts[k], faceVerts[k + 1] }, + UVs = new List { faceUVs[0], faceUVs[k], faceUVs[k + 1] }, + SmoothingGroups = smoothingGroups, + FaceFlags = (ushort)faceFlagsRF1 + }); + } + } + else + { + solid.Faces.Add(new Face + { + TextureIndex = textureIndex, + FaceId = faceId, + ScrollU = scrollUV.U, + ScrollV = scrollUV.V, + Vertices = faceVerts, + UVs = faceUVs, + SmoothingGroups = smoothingGroups, + FaceFlags = (ushort)faceFlagsRF1 + }); + } + + } // end RF1 + else + { + uint faceFlagsRF2 = reader.ReadUInt32(); + uint smoothingGroups = reader.ReadUInt32(); + + if ((faceFlagsRF2 & 0x8000) != 0) + { + reader.ReadSingle(); // skip a float + float tmp = reader.ReadSingle(); + if (Math.Abs(tmp - 1.0f) < 0.001f) faceFlagsRF2 |= 0x100000; + else if (Math.Abs(tmp - 1.35f) < 0.001f) faceFlagsRF2 |= 0x200000; + else if (Math.Abs(tmp - 1.5f) < 0.001f) faceFlagsRF2 |= 0x400000; + else faceFlagsRF2 |= 0x800000; + } + + // ¯\_(ツ)_/¯ + if (rfl_version >= 0x127) + { + reader.BaseStream.Seek(3, SeekOrigin.Current); // skip 3 bytes + float flagDecider = reader.ReadSingle(); + if (Math.Abs(flagDecider) > 0.0001f) + { + faceFlagsRF2 |= 0x4000000; + } + } + + //reader.ReadUInt32(); // room index + + int roomIndex = reader.ReadInt32(); + //int faceId2 = reader.ReadInt32(); + //reader.ReadInt32(); // reserved + //reader.ReadInt32(); // reserved + + int vertCount = reader.ReadInt32(); + + Logger.Debug(logSrc, $"RF2 face[{i}]: faceid2={faceId}, texture={textureIndex}, faceFlags=0x{faceFlagsRF2:X}, vertCount={vertCount}, smoothingGroups={smoothingGroups}"); + + var faceVerts = new List(vertCount); + var faceUVs = new List(vertCount); + + bool isInvisible = (faceFlagsRF2 & 0x2000) != 0; + bool isFullbright = (faceFlagsRF2 & 0x20) != 0; + bool isHole = (faceFlagsRF2 & 0x80) != 0; + bool isAlpha = (faceFlagsRF2 & 0x40) != 0; + bool isDetail = (faceFlagsRF2 & 0x0010) != 0; + bool isLiquid = (faceFlagsRF2 & 0x04) != 0; + bool isPortal = (faceFlagsRF2 & 0x1) != 0; + bool isSky = (faceFlagsRF2 & 0x01) != 0; + bool isMirrored = (faceFlagsRF2 & 0x02) != 0; + + for (int vi = 0; vi < vertCount; vi++) + { + uint rawIdx = reader.ReadUInt32(); + float u = reader.ReadSingle(); + float v = reader.ReadSingle(); + reader.BaseStream.Seek(4, SeekOrigin.Current); // vertex colour (RGBA) + + if (rawIdx >= rawVerts.Count) + continue; + + Vector3 pos = rawVerts[(int)rawIdx]; + var key = ((int)(pos.X * 1000), (int)(pos.Y * 1000), (int)(pos.Z * 1000)); + if (!positionMap.TryGetValue(key, out int xi)) + { + xi = uniqueVerts.Count; + uniqueVerts.Add(pos); + positionMap[key] = xi; + } + + faceVerts.Add(xi); + faceUVs.Add(new Vector2(u, v)); + uvs.Add(new Vector2(u, v)); + indices.Add(xi); + } + + // Face visibility filtering + if (!Config.IncludeInvisibleFaces && isInvisible) continue; + if (!Config.IncludeHoleFaces && isHole) continue; + if (!Config.IncludeAlphaFaces && isAlpha) continue; + if (!Config.IncludeDetailFaces && isDetail) continue; + if (!Config.IncludeLiquidFaces && isLiquid) continue; + if (!Config.IncludePortalFaces && isPortal) continue; + if (!Config.IncludeSkyFaces && isSky) continue; + + faceScrollTable.TryGetValue(faceId, out var scrollUV2); + + // Triangulate or add polygon faces for RF2 + if (Config.TriangulatePolygons && faceVerts.Count > 3) + { + Logger.Debug(logSrc, $"Triangulating RF2 ngon (count={faceVerts.Count})"); + for (int k = 1; k < faceVerts.Count - 1; k++) + { + solid.Faces.Add(new Face + { + TextureIndex = textureIndex, + FaceId = faceId, + ScrollU = scrollUV2.U, + ScrollV = scrollUV2.V, + Vertices = new List { faceVerts[0], faceVerts[k], faceVerts[k + 1] }, + UVs = new List { faceUVs[0], faceUVs[k], faceUVs[k + 1] }, + SmoothingGroups = smoothingGroups, + FaceFlags = (ushort)(faceFlagsRF2 & 0xFFFF) // cast to ushort for storage + }); + } + } + else + { + solid.Faces.Add(new Face + { + TextureIndex = textureIndex, + FaceId = faceId, + ScrollU = scrollUV2.U, + ScrollV = scrollUV2.V, + Vertices = faceVerts, + UVs = faceUVs, + SmoothingGroups = smoothingGroups, + FaceFlags = (ushort)(faceFlagsRF2 & 0xFFFF) + }); + } + } // end RF2 + + long faceEnd = reader.BaseStream.Position; + Logger.Debug(logSrc, $"Face {i} consumed {(faceEnd - faceStart)} bytes"); + } + + // Skip surface data (RF1 only) + if (isRF1) + { + int numSurfaces = reader.ReadInt32(); + Logger.Debug(logSrc, $"numSurfaces: {numSurfaces}"); + for (int i = 0; i < numSurfaces; i++) + reader.BaseStream.Seek(96, SeekOrigin.Current); + } + + // Skip old face scroll data (RF1 only) + if (isRF1 && rfl_version <= 0xB4) + { + int numOldFaceScrollData = reader.ReadInt32(); + Logger.Debug(logSrc, $"numOldFaceScrollData: {numOldFaceScrollData}"); + for (int i = 0; i < numOldFaceScrollData; i++) + reader.BaseStream.Seek(12, SeekOrigin.Current); + } + + return (uniqueVerts, uvs, solid.Faces, indices, solid); + } + } } diff --git a/utilities/Brush.cs b/utilities/Brush.cs index 6c07825..6f00af8 100644 --- a/utilities/Brush.cs +++ b/utilities/Brush.cs @@ -2,657 +2,657 @@ using System.Numerics; namespace redux.utilities -{ public class Mesh - { - public List Brushes { get; set; } = new List(); - public List Lights { get; } = new List(); - public List RoomEffects { get; } = new(); - public List MPRespawnPoints { get; } = new(); - public List Events { get; } = new(); - public List PushRegions { get; } = new(); - public List Triggers { get; } = new(); - public List Items { get; } = new(); - public List ClimbingRegions { get; } = new(); - public List ParticleEmitters { get; } = new(); - public List CollisionSpheres { get; } = new(); - public List Bones { get; } = new List(); - public Vector3[] BoneWorldPositions { get; set; } - public Vector3[] BoneModelPositions { get; set; } - public List Lightmaps { get; set; } = new List(); - public List WaypointLists { get; } = new(); - public List NavPoints { get; } = new(); - public List Decals { get; } = new List(); - public List Movers { get; } = new List(); - public List Groups { get; } = new List(); - public List Clutters { get; } = new List(); - public List Coronas { get; } = new List(); - } - - public class Brush - { - public int UID { get; set; } - public Vector3 Position { get; set; } - public Matrix4x4 RotationMatrix { get; set; } = Matrix4x4.Identity; - public List Vertices { get; set; } = new List(); - public List Normals { get; set; } = new List(); - public List UVs { get; set; } = new List(); - public List Indices { get; set; } = new List(); - public string TextureName { get; set; } - public Solid Solid { get; set; } - public List PropPoints { get; set; } = new List(); - public List JointIndices { get; set; } - public List JointWeights { get; set; } - - } - - public class Solid - { - public List Textures { get; set; } = new List(); - public List Vertices { get; set; } = new List(); - public List Faces { get; set; } = new List(); - public uint Flags { get; set; } - public int Life { get; set; } - public int State { get; set; } - } - - public enum SolidFlags : uint - { - Portal = 0x00000001, - Air = 0x00000002, - Detail = 0x00000004, - unk_c = 0x0000000C, // RF2 only, something to do with liquid? - unk_08 = 0x00000008, - EmitsSteam = 0x00000010, - Geoable = 0x00000020, // RF2 only - unk_40 = 0x00000040, // RF2 only - unk_200 = 0x00000200 // RF2 only, used on AI geo helper brushes - } - - public class Face - { - public List Vertices { get; set; } = new List(); - public List UVs { get; set; } = new(); - public int TextureIndex { get; set; } - public ushort FaceFlags { get; set; } - public int LightmapResolution - { - get => (FaceFlags >> 8) & 0x3; - // (0 = lowest, 1 = low, 2 = high, 3 = highest) - } - public uint SmoothingGroups { get; set; } - public int FaceId { get; set; } - public float ScrollU { get; set; } - public float ScrollV { get; set; } - public Vector3 Normal { get; set; } = Vector3.UnitZ; // Default fallback - public bool HasHoles => (FaceFlags & 0x80) != 0; - public bool HasAlpha => (FaceFlags & 0x40) != 0; - public bool FullBright => (FaceFlags & 0x20) != 0; - public bool ScrollTexture => (FaceFlags & 0x10) != 0; - public bool IsDetail => (FaceFlags & 0x08) != 0; - public bool LiquidSurface => (FaceFlags & 0x04) != 0; - public bool Mirrored => (FaceFlags & 0x02) != 0; - public bool ShowSky => (FaceFlags & 0x01) != 0; - public bool IsInvisible => (FaceFlags & 0x2000) != 0; - - } - public enum LightType : byte - { - Point = 0, - Spot = 1, - } - - public enum LightState : byte - { - Off = 0, - On = 1, - } - public class Light - { - public int UID { get; set; } - public string ClassName { get; set; } - public Vector3 Position { get; set; } - public Matrix4x4 Rotation { get; set; } - public string ScriptName { get; set; } - public bool HiddenInEditor { get; set; } - - // raw bitflags → nice typed props: - public bool Dynamic { get; set; } - public bool Fade { get; set; } - public bool ShadowCasting { get; set; } - public bool IsEnabled { get; set; } - public LightType Type { get; set; } - public LightState InitialState { get; set; } - public bool RuntimeShadow { get; set; } - public Vector4 Color { get; set; } // RGBA - public float Range { get; set; } - public float FOV { get; set; } - public float FOVDropoff { get; set; } - public float IntensityAtMaxRange { get; set; } - public int DropoffType { get; set; } - public float TubeLightWidth { get; set; } - - // on/off timing - public float OnIntensity { get; set; } - public float OnTime { get; set; } - public float OnTimeVariation { get; set; } - public float OffIntensity { get; set; } - public float OffTime { get; set; } - public float OffTimeVariation { get; set; } - } - - public enum RoomEffectType : int - { - SkyRoom = 1, - LiquidRoom = 2, - AmbientLight = 3, - None = 4 - } - - public enum LiquidWaveformType : int { None = 1, Calm = 2, Choppy = 3 } - public enum LiquidType : int { Water = 1, Lava = 2, Acid = 3 } - - public class LiquidProperties - { - public LiquidWaveformType Waveform { get; set; } - public float Depth { get; set; } - public string SurfaceTexture { get; set; } - public Vector4 LiquidColor { get; set; } - public float Visibility { get; set; } - public LiquidType LiquidType { get; set; } - public bool ContainsPlankton { get; set; } - public int TexturePixelsPerMeterU { get; set; } - public int TexturePixelsPerMeterV { get; set; } - public float TextureAngleDegrees { get; set; } - public Vector2 TextureScrollRate { get; set; } - } - - public class RoomEffect - { - public RoomEffectType EffectType { get; set; } - public Vector4 AmbientColor { get; set; } - public LiquidProperties LiquidProps { get; set; } - public bool RoomIsCold { get; set; } - public bool RoomIsOutside { get; set; } - public bool RoomIsAirLock { get; set; } - public int UID { get; set; } - public string ClassName { get; set; } - public Vector3 Position { get; set; } - public Matrix4x4 Rotation { get; set; } - public string ScriptName { get; set; } - public bool HiddenInEditor { get; set; } - } - - public class MpRespawnPoint - { - public int UID { get; set; } - public Vector3 Position { get; set; } - public Matrix4x4 Rotation { get; set; } - public string ScriptName { get; set; } - public bool HiddenInEditor { get; set; } - public int TeamID { get; set; } - public bool RedTeam { get; set; } - public bool BlueTeam { get; set; } - public bool IsBot { get; set; } - } - - public class RflEvent - { - public int UID { get; set; } - public string ClassName { get; set; } - public Vector3 Position { get; set; } - public bool HasRotation { get; set; } - public Matrix4x4 Rotation { get; set; } - public string ScriptName { get; set; } - public bool HiddenInEditor { get; set; } - public float Delay { get; set; } - public bool Bool1 { get; set; } - public bool Bool2 { get; set; } - public int Int1 { get; set; } - public int Int2 { get; set; } - public float Float1 { get; set; } - public float Float2 { get; set; } - public string Str1 { get; set; } - public string Str2 { get; set; } - public List Links { get; set; } - public uint RawColor { get; set; } - // RF2 exclusive below - public Dictionary Properties { get; set; } - public byte[] ParameterBlob { get; set; } - public List Parameters { get; set; } - - } - - public enum PushRegionShape { Sphere = 1, AxisAlignedBox = 2, OrientedBox = 3 } - - public class PushRegion - { - public int UID; - public string ClassName; - public Vector3 Position; - public Matrix4x4 Rotation; - public string ScriptName; - public bool HiddenInEditor; - public PushRegionShape Shape; - public Vector3 Extents; // for boxes - public float Radius; // for spheres - public float Strength; - public bool JumpPad; - public bool DoesntAffectPlayer; - public bool Radial; - public bool GrowsTowardsBoundary; - public bool GrowsTowardsCenter; - public bool Grounded; - public bool MassIndependent; - public ushort Turbulence; - } - public enum TriggerShape { Sphere = 0, Box = 1 } - public enum TriggerActivatedBy - { - PlayersOnly = 0, AllObjects = 1, LinkedObjects = 2, AiOnly = 3, - PlayerVehicleOnly = 4, GeoMods = 5 - } - public enum TriggerTeam { None = -1, Team1 = 0, Team2 = 1 } - - public class Trigger - { - public int UID; - public string ScriptName; - public bool HiddenInEditor; - public TriggerShape Shape; - public float ResetsAfter; - public int ResetsTimes; - public bool UseKeyRequired; - public string KeyName; - public bool WeaponActivates; - public TriggerActivatedBy ActivatedBy; - public bool IsNpc; - public bool IsAuto; - public bool InVehicle; - public Vector3 Position; - public float SphereRadius; - public Matrix4x4 Rotation; - public float BoxHeight, BoxWidth, BoxDepth; - public bool OneWay; - public int AirlockRoomUID; - public int AttachedToUID; - public int UseClutterUID; - public bool Disabled; - public float ButtonActiveTime; - public float InsideTime; - public TriggerTeam Team = TriggerTeam.None; - public List Links; - } - - public class RflItem - { - public int UID { get; set; } - public string ClassName { get; set; } - public Vector3 Position { get; set; } - public Matrix4x4 Rotation { get; set; } - public string ScriptName { get; set; } - public bool HiddenInEditor { get; set; } - public int Count { get; set; } - public int RespawnTime { get; set; } - public int TeamID { get; set; } - } - - public class ClimbingRegion - { - public int UID; - public string ClassName; - public Vector3 Position; - public Matrix4x4 Rotation; - public string ScriptName; - public bool HiddenInEditor; - public int Type; - public Vector3 Extents; - } - - public enum ParticleEmitterShape - { - Sphere = 0, - Plane = 1, - // Add other shape enums as needed - } - - public class ParticleEmitter - { - public int UID { get; set; } - public string ClassName { get; set; } - public Vector3 Position { get; set; } - public Matrix4x4 Rotation { get; set; } - public string ScriptName { get; set; } - public bool HiddenInEditor { get; set; } - - public ParticleEmitterShape Shape { get; set; } - public float SphereRadius { get; set; } - public float PlaneWidth { get; set; } - public float PlaneDepth { get; set; } - - public string Texture { get; set; } - public float SpawnDelay { get; set; } - public float SpawnRandomize { get; set; } - public float Velocity { get; set; } - public float VelocityRandomize { get; set; } - public float Acceleration { get; set; } - public float Decay { get; set; } - public float DecayRandomize { get; set; } - public float ParticleRadius { get; set; } - public float ParticleRadiusRandomize { get; set; } - public float GrowthRate { get; set; } - public float GravityMultiplier { get; set; } - public float RandomDirection { get; set; } - - public Vector4 ParticleColor { get; set; } - public Vector4 FadeToColor { get; set; } - - public uint EmitterFlags { get; set; } - public ushort ParticleFlags { get; set; } - - public byte Stickiness { get; set; } - public byte Bounciness { get; set; } - public byte PushEffect { get; set; } - public byte Swirliness { get; set; } - - public bool InitiallyOn { get; set; } - public float TimeOn { get; set; } - public float TimeOnRandomize { get; set; } - public float TimeOff { get; set; } - public float TimeOffRandomize { get; set; } - public float ActiveDistance { get; set; } - } - - public class LodMesh - { - public uint Flags; - public int NumVertices; - public int NumChunks; - public byte[] DataBlock; - public int Unknown1; // always –1 - - public ChunkInfo[] ChunkInfos; - public int NumPropPoints; - public int NumTextures; - public LodTexture[] Textures; - public PropPoint[] PropPoints; - - public int[] ChunkHeaders; - public ChunkData[] Chunks; - } - - public enum LodFlags : uint - { - OrigMap = 0x01, - Character = 0x02, - Reflection = 0x04, - _10 = 0x10, - TrianglePlanes = 0x20 - } - - public struct ChunkInfo - { - public ushort NumVertices; - public ushort NumFaces; - public ushort VecsAlloc; - public ushort FacesAlloc; - public ushort SamePosVertexOffsetsAlloc; - public ushort WiAlloc; - public ushort UvsAlloc; - public uint RenderFlags; - } - - public struct LodTexture - { - public byte Id; - public string Filename; - } - - public struct PropPoint - { - public string Name; - public Quaternion Orientation; - public Vector3 Position; - public int ParentIndex; - } - - public struct Triangle - { - public ushort I0, I1, I2; - public ushort Flags; - } - - public class ChunkData - { - public Vector3[] Positions; - public Vector3[] Normals; - public Vector2[] UVs; - public Triangle[] Triangles; - public RFPlane[] Planes; - public short[] SamePosVertexOffsets; - public VertexBoneLink[] BoneLinks; - public short[] OrigMap; - } - - public struct VertexBoneLink - { - public byte[] Weights; // 4 - public byte[] Bones; // 4 - } - - public struct RFPlane - { - public Vector3 Normal; - public float Dist; - } - - public class CollisionSphere - { - public string Name { get; set; } - public int ParentIndex { get; set; } - public Vector3 Position { get; set; } - public float Radius { get; set; } - } - - public class Bone - { - public string Name { get; set; } - public Quaternion BaseRotation { get; set; } - public Vector3 BaseTranslation { get; set; } - public int ParentIndex { get; set; } - } - public class RfaFile - { - public RfaHeader Header { get; set; } - public List Bones { get; set; } - public short[] MorphVertexMappings { get; set; } - public List MorphKeyframes { get; set; } - } - public class RfaHeader - { - public byte[] Magic { get; set; } - public int Version { get; set; } - public float PosReduction { get; set; } - public float RotReduction { get; set; } - public int StartTime { get; set; } - public int EndTime { get; set; } - public int NumBones { get; set; } - public int NumMorphVertices { get; set; } - public int NumMorphKeyframes { get; set; } - public int RampInTime { get; set; } - public int RampOutTime { get; set; } - public Quaternion TotalRotation { get; set; } - public Vector3 TotalTranslation { get; set; } - public int MorphVertexMappingsOffset { get; set; } - public int MorphVertexDataOffset { get; set; } - public int[] BoneOffsets { get; set; } - } - public class RfaBone - { - public float Weight { get; set; } - public short NumRotationKeys { get; set; } - public short NumTranslationKeys { get; set; } - public List RotationKeys { get; set; } - public List TranslationKeys { get; set; } - } - public class RfaRotationKey - { - public int Time { get; set; } - public Quaternion Rotation { get; set; } - public byte EaseIn { get; set; } - public byte EaseOut { get; set; } - } - public class RfaTranslationKey - { - public int Time { get; set; } - public Vector3 Translation { get; set; } - public Vector3 InTangent { get; set; } - public Vector3 OutTangent { get; set; } - } - public class MorphKeyframe - { - public int Time { get; set; } - public Vector3[] Positions { get; set; } - } - - public class Lightmap - { - public int Width { get; set; } - public int Height { get; set; } - public byte[] PixelData { get; set; } - } - public class WaypointList - { - public string Name { get; set; } - public List WaypointIndices { get; set; } = new(); - } - public class NavPoint - { - public int UID { get; set; } - public bool HiddenInEditor { get; set; } - public float Height { get; set; } - public Vector3 Position { get; set; } - public float Radius { get; set; } - public NavPointType Type { get; set; } - public bool Directional { get; set; } - public Matrix4x4? Rotation { get; set; } - public bool Cover { get; set; } - public bool Hide { get; set; } - public bool Crunch { get; set; } - public float PauseTime { get; set; } - public List LinkIndices { get; set; } = new(); - } - public enum NavPointType : int - { - Walking = 0, - Flying = 1, - } - - public class Decal - { - public int UID { get; set; } - public string ClassName { get; set; } - public Vector3 Position { get; set; } - public Matrix4x4 Rotation { get; set; } - public string ScriptName { get; set; } - public bool HiddenInEditor { get; set; } - public Vector3 Extents { get; set; } - public string Texture { get; set; } - public int Alpha { get; set; } - public bool SelfIlluminated { get; set; } - public int Tiling { get; set; } // decal_tiling enum - public float Scale { get; set; } - } - public class Group - { - public string Name { get; set; } - - public bool IsMoving { get; set; } - - public MovingGroupData MovingData { get; set; } - - public List Objects { get; set; } = new List(); - - public List Brushes { get; set; } = new List(); - } - public class MovingGroupData - { - public List Keyframes { get; } = new List(); - public List MemberTransforms { get; } = new List(); - - public bool IsDoor { get; set; } - public bool RotateInPlace { get; set; } - public bool StartsBackwards { get; set; } - public bool UseTravelTimeAsSpeed { get; set; } - public bool ForceOrient { get; set; } - public bool NoPlayerCollide { get; set; } - - public int MovementType { get; set; } - public int StartingKeyframe { get; set; } - - public string StartSound { get; set; } - public float StartVol { get; set; } - - public string LoopingSound { get; set; } - public float LoopingVol { get; set; } - - public string StopSound { get; set; } - public float StopVol { get; set; } - - public string CloseSound { get; set; } - public float CloseVol { get; set; } - } - public class Keyframe - { - public int UID { get; set; } - public Vector3 Pos { get; set; } - public Matrix4x4 Rot { get; set; } - public string ScriptName { get; set; } - public bool HiddenInEditor { get; set; } - public float PauseTime { get; set; } - public float DepartTravelTime { get; set; } - public float ReturnTravelTime { get; set; } - public float AccelTime { get; set; } - public float DecelTime { get; set; } - public int EventUID { get; set; } - public int ItemUID1 { get; set; } - public int ItemUID2 { get; set; } - public float DegreesAboutAxis { get; set; } - } - public class MovingGroupMemberTransform - { - public int UID { get; set; } - public Vector3 Pos { get; set; } - public Matrix4x4 Rot { get; set; } - } - - public class Clutter - { - public int UID { get; set; } - public string ClassName { get; set; } - public Vector3 Position { get; set; } - public Matrix4x4 Rotation { get; set; } - public string ScriptName { get; set; } - public bool HiddenInEditor { get; set; } - public string Skin { get; set; } - public List Links { get; set; } - } - - public class Corona - { - public int UID; - public string Name; - public string ScriptName; - public Vector3 Position; - public Vector4 Color { get; set; } // RGBA - public float ConeAngle; - public float Intensity; - public float RadiusDistance; - public float RadiusScale; - public float DiminishDistance; - public float VolumetricHeight; - public float VolumetricLength; - public string CoronaBitmap; - public string VolumetricBitmap; - } +{ public class Mesh + { + public List Brushes { get; set; } = new List(); + public List Lights { get; } = new List(); + public List RoomEffects { get; } = new(); + public List MPRespawnPoints { get; } = new(); + public List Events { get; } = new(); + public List PushRegions { get; } = new(); + public List Triggers { get; } = new(); + public List Items { get; } = new(); + public List ClimbingRegions { get; } = new(); + public List ParticleEmitters { get; } = new(); + public List CollisionSpheres { get; } = new(); + public List Bones { get; } = new List(); + public Vector3[] BoneWorldPositions { get; set; } + public Vector3[] BoneModelPositions { get; set; } + public List Lightmaps { get; set; } = new List(); + public List WaypointLists { get; } = new(); + public List NavPoints { get; } = new(); + public List Decals { get; } = new List(); + public List Movers { get; } = new List(); + public List Groups { get; } = new List(); + public List Clutters { get; } = new List(); + public List Coronas { get; } = new List(); + } + + public class Brush + { + public int UID { get; set; } + public Vector3 Position { get; set; } + public Matrix4x4 RotationMatrix { get; set; } = Matrix4x4.Identity; + public List Vertices { get; set; } = new List(); + public List Normals { get; set; } = new List(); + public List UVs { get; set; } = new List(); + public List Indices { get; set; } = new List(); + public string TextureName { get; set; } + public Solid Solid { get; set; } + public List PropPoints { get; set; } = new List(); + public List JointIndices { get; set; } + public List JointWeights { get; set; } + + } + + public class Solid + { + public List Textures { get; set; } = new List(); + public List Vertices { get; set; } = new List(); + public List Faces { get; set; } = new List(); + public uint Flags { get; set; } + public int Life { get; set; } + public int State { get; set; } + } + + public enum SolidFlags : uint + { + Portal = 0x00000001, + Air = 0x00000002, + Detail = 0x00000004, + unk_c = 0x0000000C, // RF2 only, something to do with liquid? + unk_08 = 0x00000008, + EmitsSteam = 0x00000010, + Geoable = 0x00000020, // RF2 only + unk_40 = 0x00000040, // RF2 only + unk_200 = 0x00000200 // RF2 only, used on AI geo helper brushes + } + + public class Face + { + public List Vertices { get; set; } = new List(); + public List UVs { get; set; } = new(); + public int TextureIndex { get; set; } + public ushort FaceFlags { get; set; } + public int LightmapResolution + { + get => (FaceFlags >> 8) & 0x3; + // (0 = lowest, 1 = low, 2 = high, 3 = highest) + } + public uint SmoothingGroups { get; set; } + public int FaceId { get; set; } + public float ScrollU { get; set; } + public float ScrollV { get; set; } + public Vector3 Normal { get; set; } = Vector3.UnitZ; // Default fallback + public bool HasHoles => (FaceFlags & 0x80) != 0; + public bool HasAlpha => (FaceFlags & 0x40) != 0; + public bool FullBright => (FaceFlags & 0x20) != 0; + public bool ScrollTexture => (FaceFlags & 0x10) != 0; + public bool IsDetail => (FaceFlags & 0x08) != 0; + public bool LiquidSurface => (FaceFlags & 0x04) != 0; + public bool Mirrored => (FaceFlags & 0x02) != 0; + public bool ShowSky => (FaceFlags & 0x01) != 0; + public bool IsInvisible => (FaceFlags & 0x2000) != 0; + + } + public enum LightType : byte + { + Point = 0, + Spot = 1, + } + + public enum LightState : byte + { + Off = 0, + On = 1, + } + public class Light + { + public int UID { get; set; } + public string ClassName { get; set; } + public Vector3 Position { get; set; } + public Matrix4x4 Rotation { get; set; } + public string ScriptName { get; set; } + public bool HiddenInEditor { get; set; } + + // raw bitflags → nice typed props: + public bool Dynamic { get; set; } + public bool Fade { get; set; } + public bool ShadowCasting { get; set; } + public bool IsEnabled { get; set; } + public LightType Type { get; set; } + public LightState InitialState { get; set; } + public bool RuntimeShadow { get; set; } + public Vector4 Color { get; set; } // RGBA + public float Range { get; set; } + public float FOV { get; set; } + public float FOVDropoff { get; set; } + public float IntensityAtMaxRange { get; set; } + public int DropoffType { get; set; } + public float TubeLightWidth { get; set; } + + // on/off timing + public float OnIntensity { get; set; } + public float OnTime { get; set; } + public float OnTimeVariation { get; set; } + public float OffIntensity { get; set; } + public float OffTime { get; set; } + public float OffTimeVariation { get; set; } + } + + public enum RoomEffectType : int + { + SkyRoom = 1, + LiquidRoom = 2, + AmbientLight = 3, + None = 4 + } + + public enum LiquidWaveformType : int { None = 1, Calm = 2, Choppy = 3 } + public enum LiquidType : int { Water = 1, Lava = 2, Acid = 3 } + + public class LiquidProperties + { + public LiquidWaveformType Waveform { get; set; } + public float Depth { get; set; } + public string SurfaceTexture { get; set; } + public Vector4 LiquidColor { get; set; } + public float Visibility { get; set; } + public LiquidType LiquidType { get; set; } + public bool ContainsPlankton { get; set; } + public int TexturePixelsPerMeterU { get; set; } + public int TexturePixelsPerMeterV { get; set; } + public float TextureAngleDegrees { get; set; } + public Vector2 TextureScrollRate { get; set; } + } + + public class RoomEffect + { + public RoomEffectType EffectType { get; set; } + public Vector4 AmbientColor { get; set; } + public LiquidProperties LiquidProps { get; set; } + public bool RoomIsCold { get; set; } + public bool RoomIsOutside { get; set; } + public bool RoomIsAirLock { get; set; } + public int UID { get; set; } + public string ClassName { get; set; } + public Vector3 Position { get; set; } + public Matrix4x4 Rotation { get; set; } + public string ScriptName { get; set; } + public bool HiddenInEditor { get; set; } + } + + public class MpRespawnPoint + { + public int UID { get; set; } + public Vector3 Position { get; set; } + public Matrix4x4 Rotation { get; set; } + public string ScriptName { get; set; } + public bool HiddenInEditor { get; set; } + public int TeamID { get; set; } + public bool RedTeam { get; set; } + public bool BlueTeam { get; set; } + public bool IsBot { get; set; } + } + + public class RflEvent + { + public int UID { get; set; } + public string ClassName { get; set; } + public Vector3 Position { get; set; } + public bool HasRotation { get; set; } + public Matrix4x4 Rotation { get; set; } + public string ScriptName { get; set; } + public bool HiddenInEditor { get; set; } + public float Delay { get; set; } + public bool Bool1 { get; set; } + public bool Bool2 { get; set; } + public int Int1 { get; set; } + public int Int2 { get; set; } + public float Float1 { get; set; } + public float Float2 { get; set; } + public string Str1 { get; set; } + public string Str2 { get; set; } + public List Links { get; set; } + public uint RawColor { get; set; } + // RF2 exclusive below + public Dictionary Properties { get; set; } + public byte[] ParameterBlob { get; set; } + public List Parameters { get; set; } + + } + + public enum PushRegionShape { Sphere = 1, AxisAlignedBox = 2, OrientedBox = 3 } + + public class PushRegion + { + public int UID; + public string ClassName; + public Vector3 Position; + public Matrix4x4 Rotation; + public string ScriptName; + public bool HiddenInEditor; + public PushRegionShape Shape; + public Vector3 Extents; // for boxes + public float Radius; // for spheres + public float Strength; + public bool JumpPad; + public bool DoesntAffectPlayer; + public bool Radial; + public bool GrowsTowardsBoundary; + public bool GrowsTowardsCenter; + public bool Grounded; + public bool MassIndependent; + public ushort Turbulence; + } + public enum TriggerShape { Sphere = 0, Box = 1 } + public enum TriggerActivatedBy + { + PlayersOnly = 0, AllObjects = 1, LinkedObjects = 2, AiOnly = 3, + PlayerVehicleOnly = 4, GeoMods = 5 + } + public enum TriggerTeam { None = -1, Team1 = 0, Team2 = 1 } + + public class Trigger + { + public int UID; + public string ScriptName; + public bool HiddenInEditor; + public TriggerShape Shape; + public float ResetsAfter; + public int ResetsTimes; + public bool UseKeyRequired; + public string KeyName; + public bool WeaponActivates; + public TriggerActivatedBy ActivatedBy; + public bool IsNpc; + public bool IsAuto; + public bool InVehicle; + public Vector3 Position; + public float SphereRadius; + public Matrix4x4 Rotation; + public float BoxHeight, BoxWidth, BoxDepth; + public bool OneWay; + public int AirlockRoomUID; + public int AttachedToUID; + public int UseClutterUID; + public bool Disabled; + public float ButtonActiveTime; + public float InsideTime; + public TriggerTeam Team = TriggerTeam.None; + public List Links; + } + + public class RflItem + { + public int UID { get; set; } + public string ClassName { get; set; } + public Vector3 Position { get; set; } + public Matrix4x4 Rotation { get; set; } + public string ScriptName { get; set; } + public bool HiddenInEditor { get; set; } + public int Count { get; set; } + public int RespawnTime { get; set; } + public int TeamID { get; set; } + } + + public class ClimbingRegion + { + public int UID; + public string ClassName; + public Vector3 Position; + public Matrix4x4 Rotation; + public string ScriptName; + public bool HiddenInEditor; + public int Type; + public Vector3 Extents; + } + + public enum ParticleEmitterShape + { + Sphere = 0, + Plane = 1, + // Add other shape enums as needed + } + + public class ParticleEmitter + { + public int UID { get; set; } + public string ClassName { get; set; } + public Vector3 Position { get; set; } + public Matrix4x4 Rotation { get; set; } + public string ScriptName { get; set; } + public bool HiddenInEditor { get; set; } + + public ParticleEmitterShape Shape { get; set; } + public float SphereRadius { get; set; } + public float PlaneWidth { get; set; } + public float PlaneDepth { get; set; } + + public string Texture { get; set; } + public float SpawnDelay { get; set; } + public float SpawnRandomize { get; set; } + public float Velocity { get; set; } + public float VelocityRandomize { get; set; } + public float Acceleration { get; set; } + public float Decay { get; set; } + public float DecayRandomize { get; set; } + public float ParticleRadius { get; set; } + public float ParticleRadiusRandomize { get; set; } + public float GrowthRate { get; set; } + public float GravityMultiplier { get; set; } + public float RandomDirection { get; set; } + + public Vector4 ParticleColor { get; set; } + public Vector4 FadeToColor { get; set; } + + public uint EmitterFlags { get; set; } + public ushort ParticleFlags { get; set; } + + public byte Stickiness { get; set; } + public byte Bounciness { get; set; } + public byte PushEffect { get; set; } + public byte Swirliness { get; set; } + + public bool InitiallyOn { get; set; } + public float TimeOn { get; set; } + public float TimeOnRandomize { get; set; } + public float TimeOff { get; set; } + public float TimeOffRandomize { get; set; } + public float ActiveDistance { get; set; } + } + + public class LodMesh + { + public uint Flags; + public int NumVertices; + public int NumChunks; + public byte[] DataBlock; + public int Unknown1; // always –1 + + public ChunkInfo[] ChunkInfos; + public int NumPropPoints; + public int NumTextures; + public LodTexture[] Textures; + public PropPoint[] PropPoints; + + public int[] ChunkHeaders; + public ChunkData[] Chunks; + } + + public enum LodFlags : uint + { + OrigMap = 0x01, + Character = 0x02, + Reflection = 0x04, + _10 = 0x10, + TrianglePlanes = 0x20 + } + + public struct ChunkInfo + { + public ushort NumVertices; + public ushort NumFaces; + public ushort VecsAlloc; + public ushort FacesAlloc; + public ushort SamePosVertexOffsetsAlloc; + public ushort WiAlloc; + public ushort UvsAlloc; + public uint RenderFlags; + } + + public struct LodTexture + { + public byte Id; + public string Filename; + } + + public struct PropPoint + { + public string Name; + public Quaternion Orientation; + public Vector3 Position; + public int ParentIndex; + } + + public struct Triangle + { + public ushort I0, I1, I2; + public ushort Flags; + } + + public class ChunkData + { + public Vector3[] Positions; + public Vector3[] Normals; + public Vector2[] UVs; + public Triangle[] Triangles; + public RFPlane[] Planes; + public short[] SamePosVertexOffsets; + public VertexBoneLink[] BoneLinks; + public short[] OrigMap; + } + + public struct VertexBoneLink + { + public byte[] Weights; // 4 + public byte[] Bones; // 4 + } + + public struct RFPlane + { + public Vector3 Normal; + public float Dist; + } + + public class CollisionSphere + { + public string Name { get; set; } + public int ParentIndex { get; set; } + public Vector3 Position { get; set; } + public float Radius { get; set; } + } + + public class Bone + { + public string Name { get; set; } + public Quaternion BaseRotation { get; set; } + public Vector3 BaseTranslation { get; set; } + public int ParentIndex { get; set; } + } + public class RfaFile + { + public RfaHeader Header { get; set; } + public List Bones { get; set; } + public short[] MorphVertexMappings { get; set; } + public List MorphKeyframes { get; set; } + } + public class RfaHeader + { + public byte[] Magic { get; set; } + public int Version { get; set; } + public float PosReduction { get; set; } + public float RotReduction { get; set; } + public int StartTime { get; set; } + public int EndTime { get; set; } + public int NumBones { get; set; } + public int NumMorphVertices { get; set; } + public int NumMorphKeyframes { get; set; } + public int RampInTime { get; set; } + public int RampOutTime { get; set; } + public Quaternion TotalRotation { get; set; } + public Vector3 TotalTranslation { get; set; } + public int MorphVertexMappingsOffset { get; set; } + public int MorphVertexDataOffset { get; set; } + public int[] BoneOffsets { get; set; } + } + public class RfaBone + { + public float Weight { get; set; } + public short NumRotationKeys { get; set; } + public short NumTranslationKeys { get; set; } + public List RotationKeys { get; set; } + public List TranslationKeys { get; set; } + } + public class RfaRotationKey + { + public int Time { get; set; } + public Quaternion Rotation { get; set; } + public byte EaseIn { get; set; } + public byte EaseOut { get; set; } + } + public class RfaTranslationKey + { + public int Time { get; set; } + public Vector3 Translation { get; set; } + public Vector3 InTangent { get; set; } + public Vector3 OutTangent { get; set; } + } + public class MorphKeyframe + { + public int Time { get; set; } + public Vector3[] Positions { get; set; } + } + + public class Lightmap + { + public int Width { get; set; } + public int Height { get; set; } + public byte[] PixelData { get; set; } + } + public class WaypointList + { + public string Name { get; set; } + public List WaypointIndices { get; set; } = new(); + } + public class NavPoint + { + public int UID { get; set; } + public bool HiddenInEditor { get; set; } + public float Height { get; set; } + public Vector3 Position { get; set; } + public float Radius { get; set; } + public NavPointType Type { get; set; } + public bool Directional { get; set; } + public Matrix4x4? Rotation { get; set; } + public bool Cover { get; set; } + public bool Hide { get; set; } + public bool Crunch { get; set; } + public float PauseTime { get; set; } + public List LinkIndices { get; set; } = new(); + } + public enum NavPointType : int + { + Walking = 0, + Flying = 1, + } + + public class Decal + { + public int UID { get; set; } + public string ClassName { get; set; } + public Vector3 Position { get; set; } + public Matrix4x4 Rotation { get; set; } + public string ScriptName { get; set; } + public bool HiddenInEditor { get; set; } + public Vector3 Extents { get; set; } + public string Texture { get; set; } + public int Alpha { get; set; } + public bool SelfIlluminated { get; set; } + public int Tiling { get; set; } // decal_tiling enum + public float Scale { get; set; } + } + public class Group + { + public string Name { get; set; } + + public bool IsMoving { get; set; } + + public MovingGroupData MovingData { get; set; } + + public List Objects { get; set; } = new List(); + + public List Brushes { get; set; } = new List(); + } + public class MovingGroupData + { + public List Keyframes { get; } = new List(); + public List MemberTransforms { get; } = new List(); + + public bool IsDoor { get; set; } + public bool RotateInPlace { get; set; } + public bool StartsBackwards { get; set; } + public bool UseTravelTimeAsSpeed { get; set; } + public bool ForceOrient { get; set; } + public bool NoPlayerCollide { get; set; } + + public int MovementType { get; set; } + public int StartingKeyframe { get; set; } + + public string StartSound { get; set; } + public float StartVol { get; set; } + + public string LoopingSound { get; set; } + public float LoopingVol { get; set; } + + public string StopSound { get; set; } + public float StopVol { get; set; } + + public string CloseSound { get; set; } + public float CloseVol { get; set; } + } + public class Keyframe + { + public int UID { get; set; } + public Vector3 Pos { get; set; } + public Matrix4x4 Rot { get; set; } + public string ScriptName { get; set; } + public bool HiddenInEditor { get; set; } + public float PauseTime { get; set; } + public float DepartTravelTime { get; set; } + public float ReturnTravelTime { get; set; } + public float AccelTime { get; set; } + public float DecelTime { get; set; } + public int EventUID { get; set; } + public int ItemUID1 { get; set; } + public int ItemUID2 { get; set; } + public float DegreesAboutAxis { get; set; } + } + public class MovingGroupMemberTransform + { + public int UID { get; set; } + public Vector3 Pos { get; set; } + public Matrix4x4 Rot { get; set; } + } + + public class Clutter + { + public int UID { get; set; } + public string ClassName { get; set; } + public Vector3 Position { get; set; } + public Matrix4x4 Rotation { get; set; } + public string ScriptName { get; set; } + public bool HiddenInEditor { get; set; } + public string Skin { get; set; } + public List Links { get; set; } + } + + public class Corona + { + public int UID; + public string Name; + public string ScriptName; + public Vector3 Position; + public Vector4 Color { get; set; } // RGBA + public float ConeAngle; + public float Intensity; + public float RadiusDistance; + public float RadiusScale; + public float DiminishDistance; + public float VolumetricHeight; + public float VolumetricLength; + public string CoronaBitmap; + public string VolumetricBitmap; + } } diff --git a/utilities/Utils.cs b/utilities/Utils.cs index ca64d46..950a9a3 100644 --- a/utilities/Utils.cs +++ b/utilities/Utils.cs @@ -5,55 +5,55 @@ namespace redux.utilities { - public static class Config - { - public static bool IncludePortalFaces { get; set; } = true; - public static bool IncludeDetailFaces { get; set; } = true; - public static bool IncludeAlphaFaces { get; set; } = true; - public static bool IncludeHoleFaces { get; set; } = true; - public static bool IncludeLiquidFaces { get; set; } = false; - public static bool IncludeSkyFaces { get; set; } = true; - public static bool IncludeInvisibleFaces { get; set; } = true; - public static bool ParseBrushSectionInstead { get; set; } = false; - public static bool TriangulatePolygons { get; set; } = true; - public static bool SimpleBrushNames { get; set; } = false; - public static bool TranslateRF2Textures { get; set; } = false; - public static bool InsertRF2TexturePrefix { get; set; } = false; - public static bool SetRF2GeoableNonDetail { get; set; } = false; - public static bool DumpLightmaps { get; set; } = false; - public static bool FlipNormals { get; set; } = false; - public static string ReplacementItemName { get; set; } = ""; - public static string CoronaClutterName { get; set; } = ""; - public enum LogLevel - { - Debug, - Dev, - Info, - Warn, - Error - } - - public static ImageFormat ExportImageFormat { get; set; } = ImageFormat.png; - - public enum ImageFormat - { - png, - tga - } - - public static LogLevel Verbosity { get; set; } = LogLevel.Info; - - public static bool ShouldLog(LogLevel level) - { - return level >= Verbosity; - } + public static class Config + { + public static bool IncludePortalFaces { get; set; } = true; + public static bool IncludeDetailFaces { get; set; } = true; + public static bool IncludeAlphaFaces { get; set; } = true; + public static bool IncludeHoleFaces { get; set; } = true; + public static bool IncludeLiquidFaces { get; set; } = false; + public static bool IncludeSkyFaces { get; set; } = true; + public static bool IncludeInvisibleFaces { get; set; } = true; + public static bool ParseBrushSectionInstead { get; set; } = false; + public static bool TriangulatePolygons { get; set; } = true; + public static bool SimpleBrushNames { get; set; } = false; + public static bool TranslateRF2Textures { get; set; } = false; + public static bool InsertRF2TexturePrefix { get; set; } = false; + public static bool SetRF2GeoableNonDetail { get; set; } = false; + public static bool DumpLightmaps { get; set; } = false; + public static bool FlipNormals { get; set; } = false; + public static string ReplacementItemName { get; set; } = ""; + public static string CoronaClutterName { get; set; } = ""; + public enum LogLevel + { + Debug, + Dev, + Info, + Warn, + Error + } + + public static ImageFormat ExportImageFormat { get; set; } = ImageFormat.png; + + public enum ImageFormat + { + png, + tga + } + + public static LogLevel Verbosity { get; set; } = LogLevel.Info; + + public static bool ShouldLog(LogLevel level) + { + return level >= Verbosity; + } public enum MirrorAxis { - None, - X, - Y, - Z - } + None, + X, + Y, + Z + } public static MirrorAxis GeoMirror { get; set; } = MirrorAxis.None; } @@ -97,504 +97,504 @@ public static Matrix4x4 Apply(Matrix4x4 m, Config.MirrorAxis axis) } public static class SavedValues - { - public static float maybeExposure { get; set; } = 1.0f; - } - - public static class Logger - { - public static void Log(Config.LogLevel level, string source, string message) - { - if (!Config.ShouldLog(level)) - return; - - string prefix = level switch - { - Config.LogLevel.Debug => "[DEBUG]", - Config.LogLevel.Info => "[INFO ]", - Config.LogLevel.Warn => "[WARN ]", - Config.LogLevel.Error => "[ERROR]", - _ => "[LOG ]" - }; - - Console.WriteLine($"{prefix} [{source}] {message}"); - } - - public static void Debug(string source, string message) => Log(Config.LogLevel.Debug, source, message); - public static void Dev(string source, string message) => Log(Config.LogLevel.Dev, source, message); - public static void Info(string source, string message) => Log(Config.LogLevel.Info, source, message); - public static void Warn(string source, string message) => Log(Config.LogLevel.Warn, source, message); - public static void Error(string source, string message) => Log(Config.LogLevel.Error, source, message); - } - - public static class Utils - { - public static string ReadVString(BinaryReader reader) - { - if (reader.BaseStream.Position + 2 > reader.BaseStream.Length) - { - Console.WriteLine("Warning: Tried to read string length but hit end of file."); - return string.Empty; - } - - ushort length = reader.ReadUInt16(); - - if (length == 0 || length == 0xFFFF) - return string.Empty; - - if (reader.BaseStream.Position + length > reader.BaseStream.Length) - { - Console.WriteLine($"Warning: String length {length} exceeds remaining file size at position {reader.BaseStream.Position}."); - return string.Empty; - } - - byte[] data = reader.ReadBytes(length); - return Encoding.ASCII.GetString(data); - } - public static string ReadVStringPlain(BinaryReader r) - { - byte len = r.ReadByte(); - if (len == 0) return ""; - return Encoding.UTF8.GetString(r.ReadBytes(len)); - } - public static string ReadVStringFixedLength(BinaryReader reader, long sectionEnd) - { - // read the 2‑byte length prefix - if (reader.BaseStream.Position + 2 > reader.BaseStream.Length) - return string.Empty; - ushort length = reader.ReadUInt16(); - - // 0 or 0xFFFF → no string data, just skip nothing - if (length == 0 || length == 0xFFFF) - return string.Empty; - - // figure out how many bytes *we can* safely skip - long remainingInSection = sectionEnd - reader.BaseStream.Position; - int toSkip = (int)Math.Min(length, Math.Max(0, remainingInSection)); - - // actually consume exactly that many bytes - byte[] data = reader.ReadBytes(toSkip); - - return Encoding.ASCII.GetString(data); - } - - - public static void WriteVString(BinaryWriter writer, string str) - { - str ??= string.Empty; - byte[] bytes = Encoding.ASCII.GetBytes(str); - - if (bytes.Length > ushort.MaxValue) - throw new ArgumentException("String too long for vstring format."); - - writer.Write((ushort)bytes.Length); - writer.Write(bytes); - } - - public static string ReadFixedAscii(BinaryReader r, int length) - { - var bytes = r.ReadBytes(length); - int end = Array.IndexOf(bytes, 0); - if (end < 0) end = length; - return System.Text.Encoding.ASCII.GetString(bytes, 0, end); - } - - public static string ReadZeroTerminatedAscii(BinaryReader r) - { - var sb = new System.Text.StringBuilder(); - while (true) - { - byte b = r.ReadByte(); - if (b == 0) break; - sb.Append((char)b); - } - return sb.ToString(); - } - - public enum EditorViewType : int - { - FreeLook = 0, - TopDown = 1, - SideView = 2 - } - } - - // used for static geometry in RFL format - public static class FaceFlags - { - public const ushort ShowSky = 0x01; - public const ushort Mirrored = 0x02; - public const ushort LiquidSurface = 0x04; - public const ushort IsDetail = 0x08; - public const ushort ScrollTexture = 0x10; - public const ushort FullBright = 0x20; - public const ushort HasAlpha = 0x40; - public const ushort HasHoles = 0x80; - public const ushort LightmapResolutionMask = 0x0300; - public const ushort IsInvisible = 0x2000; - } - - // used for static geometry in RFL format - public class SubroomList - { - public int RoomIndex { get; set; } - public int NumSubrooms { get; set; } - public List SubroomIndices { get; set; } = new(); - } - - // used for static geometry in RFL format - public class Portal - { - public int RoomIndex1 { get; set; } - public int RoomIndex2 { get; set; } - public Vector3 Point1 { get; set; } - public Vector3 Point2 { get; set; } - } - - public static class ObjUtils - { - public static int ParseUidFromObjectName(string objectName, int fallback) - { - var parts = objectName.Split('_', StringSplitOptions.RemoveEmptyEntries); - if (parts.Length >= 2 && parts[0].Equals("Brush", StringComparison.OrdinalIgnoreCase)) - { - if (int.TryParse(parts[1], out int uid)) - return uid; - } - - //Logger.Warn(logSrc, $"Could not parse UID from object name '{objectName}', using fallback UID {fallback}"); - return fallback; - } - - public static uint ParseFlagsFromObjectName(string objectName) - { - uint flags = 0; - var parts = objectName.Split('_', StringSplitOptions.RemoveEmptyEntries); - - foreach (string part in parts) - { - switch (part.ToLowerInvariant()) - { - case "air": flags |= 0x02; break; - case "solid": break; - case "detail": flags |= 0x04; break; - case "nodetail": break; - case "portal": flags |= 0x01; break; - case "noportal": break; - case "emit": flags |= 0x10; break; - case "noemit": break; - } - } - - return flags; - } - } - - public static class RflUtils - { - public static bool IsRF2 (int rfl_version) - { - // 295 - return rfl_version == 0x127; - } - public static bool IsRF1(int rfl_version) - { - // >= 300 || <= 180 - return rfl_version >= 0x12C || rfl_version <= 0xC8; - } - public readonly struct VertexKey : IEquatable - { - public readonly Vector3 Position; - public readonly Vector2 UV; - public readonly Vector3 Normal; - - public VertexKey(Vector3 pos, Vector2 uv, Vector3 norm) - { - Position = pos; UV = uv; Normal = norm; - } - - public bool Equals(VertexKey o) => - Position.Equals(o.Position) - && UV.Equals(o.UV) - && Normal.Equals(o.Normal); - - public override int GetHashCode() => - HashCode.Combine(Position, UV, Normal); - } - - private static void SafeSkip(BinaryReader reader, int entrySize) - { - if (reader.BaseStream.Position + 4 > reader.BaseStream.Length) - return; - int count = reader.ReadInt32(); - long bytesToSkip = (long)entrySize * count; - if (reader.BaseStream.Position + bytesToSkip > reader.BaseStream.Length) - reader.BaseStream.Seek(0, SeekOrigin.End); - else - reader.BaseStream.Seek(bytesToSkip, SeekOrigin.Current); - } - public static void SkipLights(BinaryReader reader) => SafeSkip(reader, 100); - public static void SkipCutsceneCameras(BinaryReader reader) => SafeSkip(reader, 84); - public static void SkipCutscenePathNodes(BinaryReader reader) => SafeSkip(reader, 84); - public static void SkipAmbientSounds(BinaryReader reader) => SafeSkip(reader, 48); - public static void SkipEvents(BinaryReader reader) => SafeSkip(reader, 112); - public static void SkipMpRespawnPoints(BinaryReader reader) => SafeSkip(reader, 80); - public static void SkipNavPoints(BinaryReader reader) => SafeSkip(reader, 100); - public static void SkipEntities(BinaryReader reader) => SafeSkip(reader, 220); - public static void SkipItems(BinaryReader reader) => SafeSkip(reader, 100); - public static void SkipClutters(BinaryReader reader) => SafeSkip(reader, 100); - public static void SkipTriggers(BinaryReader reader) => SafeSkip(reader, 120); - public static void SkipParticleEmitters(BinaryReader reader) => SafeSkip(reader, 180); - public static void SkipGasRegions(BinaryReader reader) => SafeSkip(reader, 84); - public static void SkipDecals(BinaryReader reader) => SafeSkip(reader, 80); - public static void SkipClimbingRegions(BinaryReader reader) => SafeSkip(reader, 64); - public static void SkipRoomEffects(BinaryReader reader) => SafeSkip(reader, 64); - public static void SkipEaxEffects(BinaryReader reader) => SafeSkip(reader, 80); - public static void SkipBoltEmitters(BinaryReader reader) => SafeSkip(reader, 100); - public static void SkipTargets(BinaryReader reader) => SafeSkip(reader, 64); - public static void SkipPushRegions(BinaryReader reader) => SafeSkip(reader, 64); - public static void SkipGeoRegions(BinaryReader reader) - { - if (reader.BaseStream.Position + 4 > reader.BaseStream.Length) - return; - int count = reader.ReadInt32(); - long bytesToSkip = 4L + count * 4L; - if (reader.BaseStream.Position + bytesToSkip > reader.BaseStream.Length) - reader.BaseStream.Seek(0, SeekOrigin.End); - else - reader.BaseStream.Seek(bytesToSkip, SeekOrigin.Current); - } - - public static int FindNextValidUID(Mesh mesh) - { - // collect every existing UID - var used = new HashSet(); - used.UnionWith(mesh.Brushes.Select(b => b.UID)); - used.UnionWith(mesh.Movers.Select(b => b.UID)); - used.UnionWith(mesh.Lights.Select(l => l.UID)); - used.UnionWith(mesh.RoomEffects.Select(r => r.UID)); - used.UnionWith(mesh.MPRespawnPoints.Select(p => p.UID)); - used.UnionWith(mesh.Events.Select(e => e.UID)); - used.UnionWith(mesh.PushRegions.Select(p => p.UID)); - used.UnionWith(mesh.Triggers.Select(t => t.UID)); - used.UnionWith(mesh.Items.Select(i => i.UID)); - used.UnionWith(mesh.ClimbingRegions.Select(c => c.UID)); - used.UnionWith(mesh.ParticleEmitters.Select(pe => pe.UID)); - used.UnionWith(mesh.NavPoints.Select(n => n.UID)); - used.UnionWith(mesh.Decals.Select(d => d.UID)); - used.UnionWith(mesh.Clutters.Select(c => c.UID)); - used.UnionWith(mesh.Coronas.Select(c => c.UID)); - - // scan upward from 1 until we find a free UID - int candidate = 1; - while (used.Contains(candidate)) - candidate++; - - return candidate; - } - } - - public static class SolidFlagUtils - { - public static SolidFlags MakeRF1SafeFlags(SolidFlags flags) - { - const SolidFlags allowed = - SolidFlags.Portal | - SolidFlags.Air | - SolidFlags.Detail | - SolidFlags.EmitsSteam; - - return flags & allowed; - } - - public static SolidFlags StripRF2Geoable(SolidFlags flags) - { - bool isAir = (flags & SolidFlags.Air) != 0; - bool isPortal = (flags & SolidFlags.Portal) != 0; - bool isDetail = (flags & SolidFlags.Detail) != 0; - bool isGeoable = (flags & SolidFlags.Geoable) != 0; - - if (!isAir && !isPortal && isDetail && isGeoable) - { - flags &= ~SolidFlags.Detail; - flags &= ~SolidFlags.Geoable; - } - - return flags; - } - } - - public static class EmbeddedResourceLoader - { - public static string[] LoadLines(string resourceName) - { - var assembly = Assembly.GetExecutingAssembly(); - string fullName = assembly.GetManifestResourceNames() - .FirstOrDefault(n => n.EndsWith(resourceName, StringComparison.OrdinalIgnoreCase)); - - if (fullName == null) - throw new FileNotFoundException($"Embedded resource '{resourceName}' not found."); - - using var stream = assembly.GetManifestResourceStream(fullName); - using var reader = new StreamReader(stream!); - var lines = new List(); - - while (!reader.EndOfStream) - lines.Add(reader.ReadLine()!); - - return lines.ToArray(); - } - } - - public static class RF2TextureTranslator - { - public static string InsertRxPrefix(string textureFilename) - { - // 1) Extract just the filename and extension - string fileName = Path.GetFileName(textureFilename) ?? textureFilename; - string extension = Path.GetExtension(fileName); - string nameWithoutExt = Path.GetFileNameWithoutExtension(fileName); - - // 2) Check if there is at least one underscore in the "nameWithoutExt" - int underscoreIndex = nameWithoutExt.IndexOf('_'); - string originalPrefix = ""; - string remainder; - - if (underscoreIndex > 0) - { - // include the underscore itself in the prefix - originalPrefix = nameWithoutExt.Substring(0, underscoreIndex + 1); - remainder = nameWithoutExt.Substring(underscoreIndex + 1); - } - else - { - // no prefix detected - originalPrefix = ""; - remainder = nameWithoutExt; - } - - // 3) Normalize comparisons to lowercase for prefix‐replacement logic, - // but keep the “shape” of the final prefix in lowercase (texture names - // are typically lowercase anyway). - string lowerPrefix = originalPrefix.ToLowerInvariant(); - - string finalPrefix = originalPrefix; - switch (lowerPrefix) - { - case "drt_": - finalPrefix = "rck_"; - break; - case "woo_": - case "cpt_": - case "mar_": - case "sp0_": - finalPrefix = "sld_"; - break; - case "tec_": - finalPrefix = "mtl_"; - break; - // any other prefix stays as‐is (including if originalPrefix was empty) - } - - string withRx; - if (!string.IsNullOrEmpty(finalPrefix)) - { - withRx = finalPrefix + "rx_" + remainder; - } - else - { - withRx = "rx_" + remainder; - } - - return withRx + extension; - } - - private static Dictionary? _translationMap; - - public static int TranslationCount => _translationMap?.Count ?? 0; - - public static void LoadRF2TextureTranslations() - { - var realTextures = EmbeddedResourceLoader.LoadLines("real_texture_filenames.txt").ToList(); - var translatedTextures = new HashSet( - EmbeddedResourceLoader.LoadLines("translated_texture_filenames.txt"), - StringComparer.OrdinalIgnoreCase - ); - - string[] materialPrefixes = new[] - { - "rck_", "mtl_", "wtr_", "pls_", "gls_", "drt_", "woo_", "tec_", "cpt_", "mar_", "sp0_" - }; - - _translationMap = new Dictionary(StringComparer.OrdinalIgnoreCase); - - foreach (var real in realTextures) - { - string baseName = real; - - // Remove known material prefix - foreach (var prefix in materialPrefixes) - { - if (real.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) - { - baseName = real.Substring(prefix.Length); - break; - } - } - - // Find a translated texture that ends with the base name - string? match = translatedTextures.FirstOrDefault(t => - t.EndsWith(baseName, StringComparison.OrdinalIgnoreCase)); - - if (match != null) - _translationMap[real] = match; - } - - // Hardcoded translations - AddManualTranslation("cpt_invisible.tga", "rck_invisible04.tga"); - AddManualTranslation("drt_invisible.tga", "rck_invisible04.tga"); - AddManualTranslation("mar_invisible.tga", "sld_invisible01.tga"); - AddManualTranslation("mtl_invisible.tga", "mtl_invisible02.tga"); - AddManualTranslation("pls_invisible.tga", "sld_invisible01.tga"); - AddManualTranslation("rck_invisible.tga", "rck_invisible04.tga"); - AddManualTranslation("woo_invisible.tga", "sld_invisible01.tga"); - AddManualTranslation("wtr_invisible.tga", "cem_invisible03.tga"); - AddManualTranslation("mtl_jpad_oct1.tga", "mtl_L15S2_lift.tga"); - AddManualTranslation("mtl_jpad_oct2.tga", "mtl_jumppad01.tga"); - AddManualTranslation("mtl_jpad_oct4.tga", "mtl_jumppad02.tga"); - AddManualTranslation("cpt_012red01.tga", "sld_grf2012red01a.tga"); - AddManualTranslation("mtl_122_grate2.tga", "mtl_grf2122_grate.tga"); - } - - public static void AddManualTranslation(string original, string translated) - { - if (_translationMap == null) - _translationMap = new Dictionary(StringComparer.OrdinalIgnoreCase); - - _translationMap[original] = translated; - } - - public static string TranslateRF2Texture(string textureFilename) - { - if (_translationMap == null) - throw new InvalidOperationException("Translation map not loaded. Call LoadRF2TextureTranslations() first."); - - if (_translationMap.TryGetValue(textureFilename, out var translated)) - return translated; - - return textureFilename; // No translation found - } - - public static void DebugPrintAllTranslations() - { - if (_translationMap == null) - { - Console.WriteLine("Translation map not loaded."); - return; - } - - foreach (var pair in _translationMap) - Console.WriteLine($"{pair.Key} -> {pair.Value}"); - } - } + { + public static float maybeExposure { get; set; } = 1.0f; + } + + public static class Logger + { + public static void Log(Config.LogLevel level, string source, string message) + { + if (!Config.ShouldLog(level)) + return; + + string prefix = level switch + { + Config.LogLevel.Debug => "[DEBUG]", + Config.LogLevel.Info => "[INFO ]", + Config.LogLevel.Warn => "[WARN ]", + Config.LogLevel.Error => "[ERROR]", + _ => "[LOG ]" + }; + + Console.WriteLine($"{prefix} [{source}] {message}"); + } + + public static void Debug(string source, string message) => Log(Config.LogLevel.Debug, source, message); + public static void Dev(string source, string message) => Log(Config.LogLevel.Dev, source, message); + public static void Info(string source, string message) => Log(Config.LogLevel.Info, source, message); + public static void Warn(string source, string message) => Log(Config.LogLevel.Warn, source, message); + public static void Error(string source, string message) => Log(Config.LogLevel.Error, source, message); + } + + public static class Utils + { + public static string ReadVString(BinaryReader reader) + { + if (reader.BaseStream.Position + 2 > reader.BaseStream.Length) + { + Console.WriteLine("Warning: Tried to read string length but hit end of file."); + return string.Empty; + } + + ushort length = reader.ReadUInt16(); + + if (length == 0 || length == 0xFFFF) + return string.Empty; + + if (reader.BaseStream.Position + length > reader.BaseStream.Length) + { + Console.WriteLine($"Warning: String length {length} exceeds remaining file size at position {reader.BaseStream.Position}."); + return string.Empty; + } + + byte[] data = reader.ReadBytes(length); + return Encoding.ASCII.GetString(data); + } + public static string ReadVStringPlain(BinaryReader r) + { + byte len = r.ReadByte(); + if (len == 0) return ""; + return Encoding.UTF8.GetString(r.ReadBytes(len)); + } + public static string ReadVStringFixedLength(BinaryReader reader, long sectionEnd) + { + // read the 2‑byte length prefix + if (reader.BaseStream.Position + 2 > reader.BaseStream.Length) + return string.Empty; + ushort length = reader.ReadUInt16(); + + // 0 or 0xFFFF → no string data, just skip nothing + if (length == 0 || length == 0xFFFF) + return string.Empty; + + // figure out how many bytes *we can* safely skip + long remainingInSection = sectionEnd - reader.BaseStream.Position; + int toSkip = (int)Math.Min(length, Math.Max(0, remainingInSection)); + + // actually consume exactly that many bytes + byte[] data = reader.ReadBytes(toSkip); + + return Encoding.ASCII.GetString(data); + } + + + public static void WriteVString(BinaryWriter writer, string str) + { + str ??= string.Empty; + byte[] bytes = Encoding.ASCII.GetBytes(str); + + if (bytes.Length > ushort.MaxValue) + throw new ArgumentException("String too long for vstring format."); + + writer.Write((ushort)bytes.Length); + writer.Write(bytes); + } + + public static string ReadFixedAscii(BinaryReader r, int length) + { + var bytes = r.ReadBytes(length); + int end = Array.IndexOf(bytes, 0); + if (end < 0) end = length; + return System.Text.Encoding.ASCII.GetString(bytes, 0, end); + } + + public static string ReadZeroTerminatedAscii(BinaryReader r) + { + var sb = new System.Text.StringBuilder(); + while (true) + { + byte b = r.ReadByte(); + if (b == 0) break; + sb.Append((char)b); + } + return sb.ToString(); + } + + public enum EditorViewType : int + { + FreeLook = 0, + TopDown = 1, + SideView = 2 + } + } + + // used for static geometry in RFL format + public static class FaceFlags + { + public const ushort ShowSky = 0x01; + public const ushort Mirrored = 0x02; + public const ushort LiquidSurface = 0x04; + public const ushort IsDetail = 0x08; + public const ushort ScrollTexture = 0x10; + public const ushort FullBright = 0x20; + public const ushort HasAlpha = 0x40; + public const ushort HasHoles = 0x80; + public const ushort LightmapResolutionMask = 0x0300; + public const ushort IsInvisible = 0x2000; + } + + // used for static geometry in RFL format + public class SubroomList + { + public int RoomIndex { get; set; } + public int NumSubrooms { get; set; } + public List SubroomIndices { get; set; } = new(); + } + + // used for static geometry in RFL format + public class Portal + { + public int RoomIndex1 { get; set; } + public int RoomIndex2 { get; set; } + public Vector3 Point1 { get; set; } + public Vector3 Point2 { get; set; } + } + + public static class ObjUtils + { + public static int ParseUidFromObjectName(string objectName, int fallback) + { + var parts = objectName.Split('_', StringSplitOptions.RemoveEmptyEntries); + if (parts.Length >= 2 && parts[0].Equals("Brush", StringComparison.OrdinalIgnoreCase)) + { + if (int.TryParse(parts[1], out int uid)) + return uid; + } + + //Logger.Warn(logSrc, $"Could not parse UID from object name '{objectName}', using fallback UID {fallback}"); + return fallback; + } + + public static uint ParseFlagsFromObjectName(string objectName) + { + uint flags = 0; + var parts = objectName.Split('_', StringSplitOptions.RemoveEmptyEntries); + + foreach (string part in parts) + { + switch (part.ToLowerInvariant()) + { + case "air": flags |= 0x02; break; + case "solid": break; + case "detail": flags |= 0x04; break; + case "nodetail": break; + case "portal": flags |= 0x01; break; + case "noportal": break; + case "emit": flags |= 0x10; break; + case "noemit": break; + } + } + + return flags; + } + } + + public static class RflUtils + { + public static bool IsRF2 (int rfl_version) + { + // 295 + return rfl_version == 0x127; + } + public static bool IsRF1(int rfl_version) + { + // >= 300 || <= 180 + return rfl_version >= 0x12C || rfl_version <= 0xC8; + } + public readonly struct VertexKey : IEquatable + { + public readonly Vector3 Position; + public readonly Vector2 UV; + public readonly Vector3 Normal; + + public VertexKey(Vector3 pos, Vector2 uv, Vector3 norm) + { + Position = pos; UV = uv; Normal = norm; + } + + public bool Equals(VertexKey o) => + Position.Equals(o.Position) + && UV.Equals(o.UV) + && Normal.Equals(o.Normal); + + public override int GetHashCode() => + HashCode.Combine(Position, UV, Normal); + } + + private static void SafeSkip(BinaryReader reader, int entrySize) + { + if (reader.BaseStream.Position + 4 > reader.BaseStream.Length) + return; + int count = reader.ReadInt32(); + long bytesToSkip = (long)entrySize * count; + if (reader.BaseStream.Position + bytesToSkip > reader.BaseStream.Length) + reader.BaseStream.Seek(0, SeekOrigin.End); + else + reader.BaseStream.Seek(bytesToSkip, SeekOrigin.Current); + } + public static void SkipLights(BinaryReader reader) => SafeSkip(reader, 100); + public static void SkipCutsceneCameras(BinaryReader reader) => SafeSkip(reader, 84); + public static void SkipCutscenePathNodes(BinaryReader reader) => SafeSkip(reader, 84); + public static void SkipAmbientSounds(BinaryReader reader) => SafeSkip(reader, 48); + public static void SkipEvents(BinaryReader reader) => SafeSkip(reader, 112); + public static void SkipMpRespawnPoints(BinaryReader reader) => SafeSkip(reader, 80); + public static void SkipNavPoints(BinaryReader reader) => SafeSkip(reader, 100); + public static void SkipEntities(BinaryReader reader) => SafeSkip(reader, 220); + public static void SkipItems(BinaryReader reader) => SafeSkip(reader, 100); + public static void SkipClutters(BinaryReader reader) => SafeSkip(reader, 100); + public static void SkipTriggers(BinaryReader reader) => SafeSkip(reader, 120); + public static void SkipParticleEmitters(BinaryReader reader) => SafeSkip(reader, 180); + public static void SkipGasRegions(BinaryReader reader) => SafeSkip(reader, 84); + public static void SkipDecals(BinaryReader reader) => SafeSkip(reader, 80); + public static void SkipClimbingRegions(BinaryReader reader) => SafeSkip(reader, 64); + public static void SkipRoomEffects(BinaryReader reader) => SafeSkip(reader, 64); + public static void SkipEaxEffects(BinaryReader reader) => SafeSkip(reader, 80); + public static void SkipBoltEmitters(BinaryReader reader) => SafeSkip(reader, 100); + public static void SkipTargets(BinaryReader reader) => SafeSkip(reader, 64); + public static void SkipPushRegions(BinaryReader reader) => SafeSkip(reader, 64); + public static void SkipGeoRegions(BinaryReader reader) + { + if (reader.BaseStream.Position + 4 > reader.BaseStream.Length) + return; + int count = reader.ReadInt32(); + long bytesToSkip = 4L + count * 4L; + if (reader.BaseStream.Position + bytesToSkip > reader.BaseStream.Length) + reader.BaseStream.Seek(0, SeekOrigin.End); + else + reader.BaseStream.Seek(bytesToSkip, SeekOrigin.Current); + } + + public static int FindNextValidUID(Mesh mesh) + { + // collect every existing UID + var used = new HashSet(); + used.UnionWith(mesh.Brushes.Select(b => b.UID)); + used.UnionWith(mesh.Movers.Select(b => b.UID)); + used.UnionWith(mesh.Lights.Select(l => l.UID)); + used.UnionWith(mesh.RoomEffects.Select(r => r.UID)); + used.UnionWith(mesh.MPRespawnPoints.Select(p => p.UID)); + used.UnionWith(mesh.Events.Select(e => e.UID)); + used.UnionWith(mesh.PushRegions.Select(p => p.UID)); + used.UnionWith(mesh.Triggers.Select(t => t.UID)); + used.UnionWith(mesh.Items.Select(i => i.UID)); + used.UnionWith(mesh.ClimbingRegions.Select(c => c.UID)); + used.UnionWith(mesh.ParticleEmitters.Select(pe => pe.UID)); + used.UnionWith(mesh.NavPoints.Select(n => n.UID)); + used.UnionWith(mesh.Decals.Select(d => d.UID)); + used.UnionWith(mesh.Clutters.Select(c => c.UID)); + used.UnionWith(mesh.Coronas.Select(c => c.UID)); + + // scan upward from 1 until we find a free UID + int candidate = 1; + while (used.Contains(candidate)) + candidate++; + + return candidate; + } + } + + public static class SolidFlagUtils + { + public static SolidFlags MakeRF1SafeFlags(SolidFlags flags) + { + const SolidFlags allowed = + SolidFlags.Portal | + SolidFlags.Air | + SolidFlags.Detail | + SolidFlags.EmitsSteam; + + return flags & allowed; + } + + public static SolidFlags StripRF2Geoable(SolidFlags flags) + { + bool isAir = (flags & SolidFlags.Air) != 0; + bool isPortal = (flags & SolidFlags.Portal) != 0; + bool isDetail = (flags & SolidFlags.Detail) != 0; + bool isGeoable = (flags & SolidFlags.Geoable) != 0; + + if (!isAir && !isPortal && isDetail && isGeoable) + { + flags &= ~SolidFlags.Detail; + flags &= ~SolidFlags.Geoable; + } + + return flags; + } + } + + public static class EmbeddedResourceLoader + { + public static string[] LoadLines(string resourceName) + { + var assembly = Assembly.GetExecutingAssembly(); + string fullName = assembly.GetManifestResourceNames() + .FirstOrDefault(n => n.EndsWith(resourceName, StringComparison.OrdinalIgnoreCase)); + + if (fullName == null) + throw new FileNotFoundException($"Embedded resource '{resourceName}' not found."); + + using var stream = assembly.GetManifestResourceStream(fullName); + using var reader = new StreamReader(stream!); + var lines = new List(); + + while (!reader.EndOfStream) + lines.Add(reader.ReadLine()!); + + return lines.ToArray(); + } + } + + public static class RF2TextureTranslator + { + public static string InsertRxPrefix(string textureFilename) + { + // 1) Extract just the filename and extension + string fileName = Path.GetFileName(textureFilename) ?? textureFilename; + string extension = Path.GetExtension(fileName); + string nameWithoutExt = Path.GetFileNameWithoutExtension(fileName); + + // 2) Check if there is at least one underscore in the "nameWithoutExt" + int underscoreIndex = nameWithoutExt.IndexOf('_'); + string originalPrefix = ""; + string remainder; + + if (underscoreIndex > 0) + { + // include the underscore itself in the prefix + originalPrefix = nameWithoutExt.Substring(0, underscoreIndex + 1); + remainder = nameWithoutExt.Substring(underscoreIndex + 1); + } + else + { + // no prefix detected + originalPrefix = ""; + remainder = nameWithoutExt; + } + + // 3) Normalize comparisons to lowercase for prefix‐replacement logic, + // but keep the “shape” of the final prefix in lowercase (texture names + // are typically lowercase anyway). + string lowerPrefix = originalPrefix.ToLowerInvariant(); + + string finalPrefix = originalPrefix; + switch (lowerPrefix) + { + case "drt_": + finalPrefix = "rck_"; + break; + case "woo_": + case "cpt_": + case "mar_": + case "sp0_": + finalPrefix = "sld_"; + break; + case "tec_": + finalPrefix = "mtl_"; + break; + // any other prefix stays as‐is (including if originalPrefix was empty) + } + + string withRx; + if (!string.IsNullOrEmpty(finalPrefix)) + { + withRx = finalPrefix + "rx_" + remainder; + } + else + { + withRx = "rx_" + remainder; + } + + return withRx + extension; + } + + private static Dictionary? _translationMap; + + public static int TranslationCount => _translationMap?.Count ?? 0; + + public static void LoadRF2TextureTranslations() + { + var realTextures = EmbeddedResourceLoader.LoadLines("real_texture_filenames.txt").ToList(); + var translatedTextures = new HashSet( + EmbeddedResourceLoader.LoadLines("translated_texture_filenames.txt"), + StringComparer.OrdinalIgnoreCase + ); + + string[] materialPrefixes = new[] + { + "rck_", "mtl_", "wtr_", "pls_", "gls_", "drt_", "woo_", "tec_", "cpt_", "mar_", "sp0_" + }; + + _translationMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var real in realTextures) + { + string baseName = real; + + // Remove known material prefix + foreach (var prefix in materialPrefixes) + { + if (real.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + baseName = real.Substring(prefix.Length); + break; + } + } + + // Find a translated texture that ends with the base name + string? match = translatedTextures.FirstOrDefault(t => + t.EndsWith(baseName, StringComparison.OrdinalIgnoreCase)); + + if (match != null) + _translationMap[real] = match; + } + + // Hardcoded translations + AddManualTranslation("cpt_invisible.tga", "rck_invisible04.tga"); + AddManualTranslation("drt_invisible.tga", "rck_invisible04.tga"); + AddManualTranslation("mar_invisible.tga", "sld_invisible01.tga"); + AddManualTranslation("mtl_invisible.tga", "mtl_invisible02.tga"); + AddManualTranslation("pls_invisible.tga", "sld_invisible01.tga"); + AddManualTranslation("rck_invisible.tga", "rck_invisible04.tga"); + AddManualTranslation("woo_invisible.tga", "sld_invisible01.tga"); + AddManualTranslation("wtr_invisible.tga", "cem_invisible03.tga"); + AddManualTranslation("mtl_jpad_oct1.tga", "mtl_L15S2_lift.tga"); + AddManualTranslation("mtl_jpad_oct2.tga", "mtl_jumppad01.tga"); + AddManualTranslation("mtl_jpad_oct4.tga", "mtl_jumppad02.tga"); + AddManualTranslation("cpt_012red01.tga", "sld_grf2012red01a.tga"); + AddManualTranslation("mtl_122_grate2.tga", "mtl_grf2122_grate.tga"); + } + + public static void AddManualTranslation(string original, string translated) + { + if (_translationMap == null) + _translationMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + + _translationMap[original] = translated; + } + + public static string TranslateRF2Texture(string textureFilename) + { + if (_translationMap == null) + throw new InvalidOperationException("Translation map not loaded. Call LoadRF2TextureTranslations() first."); + + if (_translationMap.TryGetValue(textureFilename, out var translated)) + return translated; + + return textureFilename; // No translation found + } + + public static void DebugPrintAllTranslations() + { + if (_translationMap == null) + { + Console.WriteLine("Translation map not loaded."); + return; + } + + foreach (var pair in _translationMap) + Console.WriteLine($"{pair.Key} -> {pair.Value}"); + } + } }