From 1252d2e007d9b004e0e1f987e097729b57fcd1cd Mon Sep 17 00:00:00 2001 From: Scobalula Date: Sat, 24 Nov 2018 12:56:37 +0000 Subject: [PATCH] IW + WW2 Support, UI, other stuff --- README.md | 28 +- src/Husky/Husky.sln | 6 + src/Husky/Husky/FileFormats/IWMap.cs | 20 +- src/Husky/Husky/FodyWeavers.xml | 6 +- src/Husky/Husky/FodyWeavers.xsd | 21 + src/Husky/Husky/GameStructures/Shared.cs | 19 +- src/Husky/Husky/Games/AdvancedWarfare.cs | 38 +- src/Husky/Husky/Games/BlackOps.cs | 38 +- src/Husky/Husky/Games/BlackOps2.cs | 51 +- src/Husky/Husky/Games/Ghosts.cs | 43 +- src/Husky/Husky/Games/InfiniteWarfare.cs | 561 +++++++++++++++ src/Husky/Husky/Games/ModernWarfare.cs | 38 +- src/Husky/Husky/Games/ModernWarfare2.cs | 38 +- src/Husky/Husky/Games/ModernWarfare3.cs | 38 +- src/Husky/Husky/Games/ModernWarfareRM.cs | 39 +- src/Husky/Husky/Games/WorldAtWar.cs | 38 +- src/Husky/Husky/Games/WorldWarII.cs | 643 ++++++++++++++++++ src/Husky/Husky/Husky.csproj | 25 +- src/Husky/Husky/{Program.cs => HuskyUtil.cs} | 61 +- src/Husky/Husky/Utility/Color.cs | 6 - src/Husky/Husky/Utility/FloatToInt.cs | 25 +- src/Husky/Husky/Utility/HalfFloats.cs | 150 ++++ src/Husky/Husky/Utility/Vectors.cs | 20 +- src/Husky/Husky/Utility/Vertex.cs | 25 +- src/Husky/Husky/Utility/VertexNormal.cs | 28 +- src/Husky/Husky/packages.config | 2 +- src/Husky/HuskyUI/AboutWindow.xaml | 18 + src/Husky/HuskyUI/AboutWindow.xaml.cs | 43 ++ src/Husky/HuskyUI/App.config | 6 + src/Husky/HuskyUI/App.xaml | 40 ++ src/Husky/HuskyUI/App.xaml.cs | 11 + src/Husky/HuskyUI/FodyWeavers.xml | 4 + src/Husky/HuskyUI/HuskyUI.csproj | 132 ++++ src/Husky/HuskyUI/MainWindow.xaml | 140 ++++ src/Husky/HuskyUI/MainWindow.xaml.cs | 100 +++ src/Husky/HuskyUI/Properties/AssemblyInfo.cs | 53 ++ .../HuskyUI/Properties/Resources.Designer.cs | 63 ++ src/Husky/HuskyUI/Properties/Resources.resx | 117 ++++ .../HuskyUI/Properties/Settings.Designer.cs | 26 + .../HuskyUI/Properties/Settings.settings | 7 + src/Husky/HuskyUI/icon.ico | Bin 0 -> 108241 bytes src/Husky/HuskyUI/icon.png | Bin 0 -> 12053 bytes src/Husky/HuskyUI/packages.config | 5 + 43 files changed, 2492 insertions(+), 280 deletions(-) create mode 100644 src/Husky/Husky/FodyWeavers.xsd create mode 100644 src/Husky/Husky/Games/InfiniteWarfare.cs create mode 100644 src/Husky/Husky/Games/WorldWarII.cs rename src/Husky/Husky/{Program.cs => HuskyUtil.cs} (70%) delete mode 100644 src/Husky/Husky/Utility/Color.cs create mode 100644 src/Husky/Husky/Utility/HalfFloats.cs create mode 100644 src/Husky/HuskyUI/AboutWindow.xaml create mode 100644 src/Husky/HuskyUI/AboutWindow.xaml.cs create mode 100644 src/Husky/HuskyUI/App.config create mode 100644 src/Husky/HuskyUI/App.xaml create mode 100644 src/Husky/HuskyUI/App.xaml.cs create mode 100644 src/Husky/HuskyUI/FodyWeavers.xml create mode 100644 src/Husky/HuskyUI/HuskyUI.csproj create mode 100644 src/Husky/HuskyUI/MainWindow.xaml create mode 100644 src/Husky/HuskyUI/MainWindow.xaml.cs create mode 100644 src/Husky/HuskyUI/Properties/AssemblyInfo.cs create mode 100644 src/Husky/HuskyUI/Properties/Resources.Designer.cs create mode 100644 src/Husky/HuskyUI/Properties/Resources.resx create mode 100644 src/Husky/HuskyUI/Properties/Settings.Designer.cs create mode 100644 src/Husky/HuskyUI/Properties/Settings.settings create mode 100644 src/Husky/HuskyUI/icon.ico create mode 100644 src/Husky/HuskyUI/icon.png create mode 100644 src/Husky/HuskyUI/packages.config diff --git a/README.md b/README.md index 8b94470..8ba7401 100644 --- a/README.md +++ b/README.md @@ -3,23 +3,25 @@ Husky is a BSP Extractor for Call of Duty. It can rip the raw vertex/face data t ### Supported Games -* Call of Duty: World at War -* Call of Duty: Black Ops -* Call of Duty: Black Ops 2 * Call of Duty: Modern Warfare * Call of Duty: Modern Warfare 2 * Call of Duty: Modern Warfare 3 -* Call of Duty: Advanced Warfare * Call of Duty: Ghosts +* Call of Duty: Infinite Warfare +* Call of Duty: World at War +* Call of Duty: Black Ops +* Call of Duty: Black Ops 2 +* Call of Duty: Advanced Warfare +* Call of DUty: World War 2 * Call of Duty: Modern Warfare Remastered -**Call of Duty: Black Ops 3 will never be supported, for obvious reasons** +**Call of Duty: Black Ops 3 will never be supported, to avoid people ripping custom maps, etc.** ### Downloading/Using Husky To download Husky, go to the [Releases](https://github.com/Scobalula/Husky/releases) and download the latest build. -To use Husky, simply run the game, load the map you want to extract, and run Husky. In some cases you may need to run Husky as an administator. +To use Husky, simply run the game, load the map you want to extract, and run Husky, then click the paper airplane to export the loaded map. In some cases you may need to run Husky as an administator. Once the map is exported, you will have 3 files for it: @@ -28,17 +30,19 @@ Once the map is exported, you will have 3 files for it: * **mapname**.txt - A search string for Wraith/Greyhound (only contains color maps) * **mapname**.map - Map file with **static** model locations and rotations -If you wish to use textures (be warned they can result in high RAM usage) then make sure to have the _images folder in the same location as the obj/mtl file and export PNGs (do not ask for other formats, it's staying as PNG, do a find/replace if you want to use other formats). +If you wish to use textures (be warned they can result in high RAM usage) then make sure to have the _images folder (use Wraith/Greyhound to export the required images) in the same location as the obj/mtl file and export PNGs (do not ask for other formats, it's staying as PNG, do a find/replace if you want to use other formats). ### License/Disclaimers -Husky is licensed under the GPL license and it and its source code is free to use and modify under the. Husky comes with NO warranty, any damages caused are solely the responsibility of the user. See the LICENSE file for more information. +Husky is licensed under the GPL license and it and its source code is free to use and modify under the terms of the GPL. Husky comes with NO warranty, any damages caused are solely the responsibility of the user. See the LICENSE file for more information. -All BSP data extracted using Husky is property of the developers, etc. Husky simply parses the data out, what you do with the data is your reponsibility. +**All** BSP data extracted using Husky is property of the developers, etc. and with this in mind you need to understand the limitation of what you can do with the data. Husky simply parses it out, what you do with it, is your responsibility. Some of the exported models can get pretty big. While all have loaded in Maya with no issue, make sure to have available resources available to load and view them. -**Husky is currently in alpha, and with that in mind, bugs, errors, you know, the bad stuff.** +Husky is provided ***AS IS***, do not ask how to do XYZ, how to import it into X program, etc. You need to know what you're doing, we can't hold your hand through it. + +Also I've noticed several people asking how to import it into Unity/Unreal, there is a fine line between using the Geo to remake a map in another Call of Duty Game using its own SDK **vs** using the Geo as it is in your game/IP, for example, so ensure you know what you're doing, copyright wise. ## FAQ @@ -56,11 +60,11 @@ Some of the exported models can get pretty big. While all have loaded in Maya wi * Q: Why is there a bunch of geo at the origin? -* A: It appears in all games, script brushmodels are at the origin, and I assume the map_ents assets or some other data is used to tell the game where to move them to on load. +* A: It appears in all games, script brushmodels are at the origin, and I assume the map_ents assets or some other data is used to tell the game where to move them to on load. This will remain as is. ## Credits -* DTZxPorter - Normal Unpacking Code from Wraith + Other misc info from Wraith, SELib +* DTZxPorter - Normal Unpacking Code from Wraith, Half Floats code, Other misc info from Wraith. * Anna Baker - [Icon](https://thenounproject.com/term/husky/1121992/) ([https://thenounproject.com/anna.baker194/](https://thenounproject.com/anna.baker194/)) ## Support Me diff --git a/src/Husky/Husky.sln b/src/Husky/Husky.sln index 30ebd61..9f00691 100644 --- a/src/Husky/Husky.sln +++ b/src/Husky/Husky.sln @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Husky", "Husky\Husky.csproj EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PhilLibX", "PhilLibX\PhilLibX.csproj", "{8F5C1BA4-88C1-4177-B91B-DD093DC849B9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HuskyUI", "HuskyUI\HuskyUI.csproj", "{5BC7C6FE-F65C-4F32-BC26-FAC69926F10E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -21,6 +23,10 @@ Global {8F5C1BA4-88C1-4177-B91B-DD093DC849B9}.Debug|Any CPU.Build.0 = Debug|Any CPU {8F5C1BA4-88C1-4177-B91B-DD093DC849B9}.Release|Any CPU.ActiveCfg = Release|Any CPU {8F5C1BA4-88C1-4177-B91B-DD093DC849B9}.Release|Any CPU.Build.0 = Release|Any CPU + {5BC7C6FE-F65C-4F32-BC26-FAC69926F10E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5BC7C6FE-F65C-4F32-BC26-FAC69926F10E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5BC7C6FE-F65C-4F32-BC26-FAC69926F10E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5BC7C6FE-F65C-4F32-BC26-FAC69926F10E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Husky/Husky/FileFormats/IWMap.cs b/src/Husky/Husky/FileFormats/IWMap.cs index 94e4c2b..19999d4 100644 --- a/src/Husky/Husky/FileFormats/IWMap.cs +++ b/src/Husky/Husky/FileFormats/IWMap.cs @@ -1,6 +1,24 @@ -using System; +// ------------------------------------------------------------------------ +// Husky - Call of Duty BSP Extractor +// Copyright (C) 2018 Philip/Scobalula +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ------------------------------------------------------------------------ +using System; using System.Collections.Generic; using System.IO; + namespace Husky { /// diff --git a/src/Husky/Husky/FodyWeavers.xml b/src/Husky/Husky/FodyWeavers.xml index 481d18f..a5dcf04 100644 --- a/src/Husky/Husky/FodyWeavers.xml +++ b/src/Husky/Husky/FodyWeavers.xml @@ -1,4 +1,4 @@ - - - + + + \ No newline at end of file diff --git a/src/Husky/Husky/FodyWeavers.xsd b/src/Husky/Husky/FodyWeavers.xsd new file mode 100644 index 0000000..d320e92 --- /dev/null +++ b/src/Husky/Husky/FodyWeavers.xsd @@ -0,0 +1,21 @@ + + + + + + + + + + + 'true' to run assembly verification on the target assembly after all weavers have been finished. + + + + + A comma separated list of error codes that can be safely ignored in assembly verification. + + + + + \ No newline at end of file diff --git a/src/Husky/Husky/GameStructures/Shared.cs b/src/Husky/Husky/GameStructures/Shared.cs index 1713f45..a43a174 100644 --- a/src/Husky/Husky/GameStructures/Shared.cs +++ b/src/Husky/Husky/GameStructures/Shared.cs @@ -1,4 +1,21 @@ -using System.Runtime.InteropServices; +// ------------------------------------------------------------------------ +// Husky - Call of Duty BSP Extractor +// Copyright (C) 2018 Philip/Scobalula +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ------------------------------------------------------------------------ +using System.Runtime.InteropServices; namespace Husky { diff --git a/src/Husky/Husky/Games/AdvancedWarfare.cs b/src/Husky/Husky/Games/AdvancedWarfare.cs index 841b1ad..79c69a3 100644 --- a/src/Husky/Husky/Games/AdvancedWarfare.cs +++ b/src/Husky/Husky/Games/AdvancedWarfare.cs @@ -270,10 +270,10 @@ public unsafe struct GfxStaticModel /// /// Reads BSP Data /// - public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, long assetSizesAddress, string gameType) + public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, long assetSizesAddress, string gameType, Action printCallback = null) { // Found her - Printer.WriteLine("INFO", "Found supported game: Call of Duty: Advanced Warfare"); + printCallback?.Invoke("Found supported game: Call of Duty: Advanced Warfare"); // Validate by XModel Name if (reader.ReadNullTerminatedString(reader.ReadInt64(reader.ReadInt64(assetPoolsAddress + 0x38) + 8)) == "fx") @@ -288,19 +288,19 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l // Verify a BSP is actually loaded (if in base menu, etc, no map is loaded) if (String.IsNullOrWhiteSpace(gfxMapName)) { - Printer.WriteLine("ERROR", "No BSP loaded. Enter Main Menu or a Map to load in the required assets.", ConsoleColor.DarkRed); + printCallback?.Invoke("No BSP loaded. Enter Main Menu or a Map to load in the required assets."); } else { // New IW Map var mapFile = new IWMap(); // Print Info - Printer.WriteLine("INFO", String.Format("Loaded Gfx Map - {0}", gfxMapName)); - Printer.WriteLine("INFO", String.Format("Loaded Map - {0}", mapName)); - Printer.WriteLine("INFO", String.Format("Vertex Count - {0}", gfxMapAsset.GfxVertexCount)); - Printer.WriteLine("INFO", String.Format("Indices Count - {0}", gfxMapAsset.GfxIndicesCount)); - Printer.WriteLine("INFO", String.Format("Surface Count - {0}", gfxMapAsset.SurfaceCount)); - Printer.WriteLine("INFO", String.Format("Model Count - {0}", gfxMapAsset.GfxStaticModelsCount)); + printCallback?.Invoke(String.Format("Loaded Gfx Map - {0}", gfxMapName)); + printCallback?.Invoke(String.Format("Loaded Map - {0}", mapName)); + printCallback?.Invoke(String.Format("Vertex Count - {0}", gfxMapAsset.GfxVertexCount)); + printCallback?.Invoke(String.Format("Indices Count - {0}", gfxMapAsset.GfxIndicesCount)); + printCallback?.Invoke(String.Format("Surface Count - {0}", gfxMapAsset.SurfaceCount)); + printCallback?.Invoke(String.Format("Model Count - {0}", gfxMapAsset.GfxStaticModelsCount)); // Build output Folder string outputName = Path.Combine("exported_maps", "advanced_warfare", gameType, mapName, mapName); @@ -310,31 +310,31 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l var stopWatch = Stopwatch.StartNew(); // Read Vertices - Printer.WriteLine("INFO", "Parsing vertex data...."); + printCallback?.Invoke("Parsing vertex data...."); var vertices = ReadGfxVertices(reader, gfxMapAsset.GfxVerticesPointer, (int)gfxMapAsset.GfxVertexCount); - Printer.WriteLine("INFO", String.Format("Parsed vertex data in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed vertex data in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Read Indices - Printer.WriteLine("INFO", "Parsing surface indices...."); + printCallback?.Invoke("Parsing surface indices...."); var indices = ReadGfxIndices(reader, gfxMapAsset.GfxIndicesPointer, (int)gfxMapAsset.GfxIndicesCount); - Printer.WriteLine("INFO", String.Format("Parsed indices in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed indices in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Read Indices - Printer.WriteLine("INFO", "Parsing surfaces...."); + printCallback?.Invoke("Parsing surfaces...."); var surfaces = ReadGfxSufaces(reader, gfxMapAsset.GfxSurfacesPointer, gfxMapAsset.SurfaceCount); - Printer.WriteLine("INFO", String.Format("Parsed surfaces in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed surfaces in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Write OBJ - Printer.WriteLine("INFO", "Converting to OBJ...."); + printCallback?.Invoke("Converting to OBJ...."); // Create new OBJ var obj = new WavefrontOBJ(); @@ -402,13 +402,13 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l mapFile.DumpToMap(outputName + ".map"); // Done - Printer.WriteLine("INFO", String.Format("Converted to OBJ in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Converted to OBJ in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); } } else { - Printer.WriteLine("ERROR", "Call of Duty: Advanced Warfare is supported, but this EXE is not.", ConsoleColor.DarkRed); + printCallback?.Invoke("Call of Duty: Advanced Warfare is supported, but this EXE is not."); } } @@ -469,7 +469,7 @@ public static Vertex[] ReadGfxVertices(ProcessReader reader, long address, int c gfxVertex.Y * 2.54, gfxVertex.Z * 2.54), // Decode and set normal (from DTZxPorter - Wraith, same as XModels) - Normal = VertexNormal.UnpackB(gfxVertex.Normal), + Normal = VertexNormalUnpacking.MethodB(gfxVertex.Normal), // Set UV UV = new Vector2(gfxVertex.U, 1 - gfxVertex.V) }; diff --git a/src/Husky/Husky/Games/BlackOps.cs b/src/Husky/Husky/Games/BlackOps.cs index f3d2b38..4d1eefc 100644 --- a/src/Husky/Husky/Games/BlackOps.cs +++ b/src/Husky/Husky/Games/BlackOps.cs @@ -251,10 +251,10 @@ public unsafe struct GfxStaticModel /// /// Reads BSP Data /// - public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, long assetSizesAddress, string gameType) + public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, long assetSizesAddress, string gameType, Action printCallback = null) { // Found her - Printer.WriteLine("INFO", "Found supported game: Call of Duty: Black Ops"); + printCallback?.Invoke("Found supported game: Call of Duty: Black Ops"); // Get XModel Name var firstXModelName = reader.ReadNullTerminatedString(reader.ReadInt32(reader.ReadInt32(assetPoolsAddress + 0x14) + 4)); @@ -272,19 +272,19 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l // Verify a BSP is actually loaded (if in base menu, etc, no map is loaded) if (String.IsNullOrWhiteSpace(gfxMapName)) { - Printer.WriteLine("ERROR", "No BSP loaded. Enter Main Menu or a Map to load in the required assets.", ConsoleColor.DarkRed); + printCallback?.Invoke("No BSP loaded. Enter Main Menu or a Map to load in the required assets."); } else { // New IW Map var mapFile = new IWMap(); // Print Info - Printer.WriteLine("INFO", String.Format("Loaded Gfx Map - {0}", gfxMapName)); - Printer.WriteLine("INFO", String.Format("Loaded Map - {0}", mapName)); - Printer.WriteLine("INFO", String.Format("Vertex Count - {0}", gfxMapAsset.GfxVertexCount)); - Printer.WriteLine("INFO", String.Format("Indices Count - {0}", gfxMapAsset.GfxIndicesCount)); - Printer.WriteLine("INFO", String.Format("Surface Count - {0}", gfxMapAsset.SurfaceCount)); - Printer.WriteLine("INFO", String.Format("Model Count - {0}", gfxMapAsset.GfxStaticModelsCount)); + printCallback?.Invoke(String.Format("Loaded Gfx Map - {0}", gfxMapName)); + printCallback?.Invoke(String.Format("Loaded Map - {0}", mapName)); + printCallback?.Invoke(String.Format("Vertex Count - {0}", gfxMapAsset.GfxVertexCount)); + printCallback?.Invoke(String.Format("Indices Count - {0}", gfxMapAsset.GfxIndicesCount)); + printCallback?.Invoke(String.Format("Surface Count - {0}", gfxMapAsset.SurfaceCount)); + printCallback?.Invoke(String.Format("Model Count - {0}", gfxMapAsset.GfxStaticModelsCount)); // Build output Folder string outputName = Path.Combine("exported_maps", "black_ops", gameType, mapName, mapName); @@ -294,31 +294,31 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l var stopWatch = Stopwatch.StartNew(); // Read Vertices - Printer.WriteLine("INFO", "Parsing vertex data...."); + printCallback?.Invoke("Parsing vertex data...."); var vertices = ReadGfxVertices(reader, gfxMapAsset.GfxVerticesPointer, gfxMapAsset.GfxVertexCount); - Printer.WriteLine("INFO", String.Format("Parsed vertex data in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed vertex data in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Read Indices - Printer.WriteLine("INFO", "Parsing surface indices...."); + printCallback?.Invoke("Parsing surface indices...."); var indices = ReadGfxIndices(reader, gfxMapAsset.GfxIndicesPointer, gfxMapAsset.GfxIndicesCount); - Printer.WriteLine("INFO", String.Format("Parsed indices in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed indices in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Read Indices - Printer.WriteLine("INFO", "Parsing surfaces...."); + printCallback?.Invoke("Parsing surfaces...."); var surfaces = ReadGfxSufaces(reader, gfxMapAsset.GfxSurfacesPointer, gfxMapAsset.SurfaceCount); - Printer.WriteLine("INFO", String.Format("Parsed surfaces in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed surfaces in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Write OBJ - Printer.WriteLine("INFO", "Converting to OBJ...."); + printCallback?.Invoke("Converting to OBJ...."); // Create new OBJ var obj = new WavefrontOBJ(); @@ -386,13 +386,13 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l mapFile.DumpToMap(outputName + ".map"); // Done - Printer.WriteLine("INFO", String.Format("Converted to OBJ in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Converted to OBJ in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); } } else { - Printer.WriteLine("ERROR", "Call of Duty: Black Ops is supported, but this EXE is not.", ConsoleColor.DarkRed); + printCallback?.Invoke("Call of Duty: Black Ops is supported, but this EXE is not."); } } @@ -453,7 +453,7 @@ public static Vertex[] ReadGfxVertices(ProcessReader reader, long address, int c gfxVertex.Y * 2.54, gfxVertex.Z * 2.54), // Decode and set normal (from DTZxPorter - Wraith, same as XModels) - Normal = VertexNormal.UnpackA(gfxVertex.Normal), + Normal = VertexNormalUnpacking.MethodA(gfxVertex.Normal), // Set UV UV = new Vector2(gfxVertex.U, 1 - gfxVertex.V) }; diff --git a/src/Husky/Husky/Games/BlackOps2.cs b/src/Husky/Husky/Games/BlackOps2.cs index db25869..e88ef63 100644 --- a/src/Husky/Husky/Games/BlackOps2.cs +++ b/src/Husky/Husky/Games/BlackOps2.cs @@ -322,10 +322,10 @@ public unsafe struct Material /// /// Reads BSP Data /// - public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, long assetSizesAddress, string gameType) + public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, long assetSizesAddress, string gameType, Action printCallback = null) { // Found her - Printer.WriteLine("INFO", "Found supported game: Call of Duty: Black Ops 2"); + printCallback?.Invoke("Found supported game: Call of Duty: Black Ops 2"); // Validate by XModel Name if (reader.ReadNullTerminatedString(reader.ReadInt32(reader.ReadInt32(assetPoolsAddress + 0x14) + 4)) == "defaultvehicle") { @@ -339,19 +339,19 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l // Verify a BSP is actually loaded (if in base menu, etc, no map is loaded) if (String.IsNullOrWhiteSpace(gfxMapName)) { - Printer.WriteLine("ERROR", "No BSP loaded. Enter Main Menu or a Map to load in the required assets.", ConsoleColor.DarkRed); + printCallback?.Invoke("No BSP loaded. Enter Main Menu or a Map to load in the required assets."); } else { // New IW Map var mapFile = new IWMap(); // Print Info - Printer.WriteLine("INFO", String.Format("Loaded Gfx Map - {0}", gfxMapName)); - Printer.WriteLine("INFO", String.Format("Loaded Map - {0}", mapName)); - Printer.WriteLine("INFO", String.Format("Vertex Count - {0}", gfxMapAsset.GfxVertexCount)); - Printer.WriteLine("INFO", String.Format("Indices Count - {0}", gfxMapAsset.GfxIndicesCount)); - Printer.WriteLine("INFO", String.Format("Surface Count - {0}", gfxMapAsset.SurfaceCount)); - Printer.WriteLine("INFO", String.Format("Model Count - {0}", gfxMapAsset.GfxStaticModelsCount)); + printCallback?.Invoke(String.Format("Loaded Gfx Map - {0}", gfxMapName)); + printCallback?.Invoke(String.Format("Loaded Map - {0}", mapName)); + printCallback?.Invoke(String.Format("Vertex Count - {0}", gfxMapAsset.GfxVertexCount)); + printCallback?.Invoke(String.Format("Indices Count - {0}", gfxMapAsset.GfxIndicesCount)); + printCallback?.Invoke(String.Format("Surface Count - {0}", gfxMapAsset.SurfaceCount)); + printCallback?.Invoke(String.Format("Model Count - {0}", gfxMapAsset.GfxStaticModelsCount)); // Build output Folder string outputName = Path.Combine("exported_maps", "black_ops_2", gameType, mapName, mapName); @@ -361,31 +361,31 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l var stopWatch = Stopwatch.StartNew(); // Read Vertices - Printer.WriteLine("INFO", "Parsing vertex data...."); - var vertexBuffer = ReadGfxVertices(reader, gfxMapAsset.GfxVerticesPointer, gfxMapAsset.GfxVertexBufferSize); - Printer.WriteLine("INFO", String.Format("Parsed vertex data in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke("Parsing vertex data...."); + var vertexBuffer = reader.ReadBytes(gfxMapAsset.GfxVerticesPointer, gfxMapAsset.GfxVertexBufferSize); + printCallback?.Invoke(String.Format("Parsed vertex data in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Read Indices - Printer.WriteLine("INFO", "Parsing surface indices...."); + printCallback?.Invoke("Parsing surface indices...."); var indices = ReadGfxIndices(reader, gfxMapAsset.GfxIndicesPointer, gfxMapAsset.GfxIndicesCount); - Printer.WriteLine("INFO", String.Format("Parsed indices in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed indices in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Read Indices - Printer.WriteLine("INFO", "Parsing surfaces...."); + printCallback?.Invoke("Parsing surfaces...."); var surfaces = ReadGfxSufaces(reader, gfxMapAsset.GfxSurfacesPointer, gfxMapAsset.SurfaceCount); - Printer.WriteLine("INFO", String.Format("Parsed surfaces in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed surfaces in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Write OBJ - Printer.WriteLine("INFO", "Converting to OBJ...."); + printCallback?.Invoke("Converting to OBJ...."); // Create new OBJ var obj = new WavefrontOBJ(); @@ -474,13 +474,13 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l mapFile.DumpToMap(outputName + ".map"); // Done - Printer.WriteLine("INFO", String.Format("Converted to OBJ in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Converted to OBJ in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); } } else { - Printer.WriteLine("ERROR", "Call of Duty: Black Ops 2 is supported, but this EXE is not.", ConsoleColor.DarkRed); + printCallback?.Invoke("Call of Duty: Black Ops 2 is supported, but this EXE is not."); } } @@ -517,15 +517,6 @@ public static ushort[] ReadGfxIndices(ProcessReader reader, long address, int co return indices; } - /// - /// Reads Gfx Vertices - /// - public static byte[] ReadGfxVertices(ProcessReader reader, long address, int size) - { - // Done - return reader.ReadBytes(address, size); - } - /// /// Unpacks a vertex /// @@ -541,9 +532,9 @@ public static Vertex UnpackVertex(GfxVertex packedVertex) packedVertex.Y * 2.54, packedVertex.Z * 2.54), // Decode and set normal (from DTZxPorter - Wraith, same as XModels) - Normal = VertexNormal.UnpackC(packedVertex.Normal), + Normal = VertexNormalUnpacking.MethodC(packedVertex.Normal), // Set UV - UV = new Vector2(packedVertex.U, 1 - packedVertex.V) + UV = new Vector2(HalfFloats.ToFloat(packedVertex.U), 1 - HalfFloats.ToFloat(packedVertex.V)) }; } diff --git a/src/Husky/Husky/Games/Ghosts.cs b/src/Husky/Husky/Games/Ghosts.cs index 130e231..0fffb57 100644 --- a/src/Husky/Husky/Games/Ghosts.cs +++ b/src/Husky/Husky/Games/Ghosts.cs @@ -270,10 +270,10 @@ public unsafe struct GfxStaticModel /// /// Reads BSP Data /// - public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, long assetSizesAddress, string gameType) + public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, long assetSizesAddress, string gameType, Action printCallback = null) { // Found her - Printer.WriteLine("INFO", "Found supported game: Call of Duty: Ghosts"); + printCallback?.Invoke("Found supported game: Call of Duty: Ghosts"); // Validate by XModel Name if (reader.ReadNullTerminatedString(reader.ReadInt64(reader.ReadInt64(assetPoolsAddress + 0x20) + 8)) == "void") @@ -288,19 +288,19 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l // Verify a BSP is actually loaded (if in base menu, etc, no map is loaded) if (String.IsNullOrWhiteSpace(gfxMapName)) { - Printer.WriteLine("ERROR", "No BSP loaded. Enter Main Menu or a Map to load in the required assets.", ConsoleColor.DarkRed); + printCallback?.Invoke("No BSP loaded. Enter Main Menu or a Map to load in the required assets."); } else { // New IW Map var mapFile = new IWMap(); // Print Info - Printer.WriteLine("INFO", String.Format("Loaded Gfx Map - {0}", gfxMapName)); - Printer.WriteLine("INFO", String.Format("Loaded Map - {0}", mapName)); - Printer.WriteLine("INFO", String.Format("Vertex Count - {0}", gfxMapAsset.GfxVertexCount)); - Printer.WriteLine("INFO", String.Format("Indices Count - {0}", gfxMapAsset.GfxIndicesCount)); - Printer.WriteLine("INFO", String.Format("Surface Count - {0}", gfxMapAsset.SurfaceCount)); - Printer.WriteLine("INFO", String.Format("Model Count - {0}", gfxMapAsset.GfxStaticModelsCount)); + printCallback?.Invoke(String.Format("Loaded Gfx Map - {0}", gfxMapName)); + printCallback?.Invoke(String.Format("Loaded Map - {0}", mapName)); + printCallback?.Invoke(String.Format("Vertex Count - {0}", gfxMapAsset.GfxVertexCount)); + printCallback?.Invoke(String.Format("Indices Count - {0}", gfxMapAsset.GfxIndicesCount)); + printCallback?.Invoke(String.Format("Surface Count - {0}", gfxMapAsset.SurfaceCount)); + printCallback?.Invoke(String.Format("Model Count - {0}", gfxMapAsset.GfxStaticModelsCount)); // Build output Folder string outputName = Path.Combine("exported_maps", "ghosts", gameType, mapName, mapName); @@ -310,31 +310,31 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l var stopWatch = Stopwatch.StartNew(); // Read Vertices - Printer.WriteLine("INFO", "Parsing vertex data...."); + printCallback?.Invoke("Parsing vertex data...."); var vertices = ReadGfxVertices(reader, gfxMapAsset.GfxVerticesPointer, gfxMapAsset.GfxVertexCount); - Printer.WriteLine("INFO", String.Format("Parsed vertex data in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed vertex data in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Read Indices - Printer.WriteLine("INFO", "Parsing surface indices...."); + printCallback?.Invoke("Parsing surface indices...."); var indices = ReadGfxIndices(reader, gfxMapAsset.GfxIndicesPointer, (int)gfxMapAsset.GfxIndicesCount); - Printer.WriteLine("INFO", String.Format("Parsed indices in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed indices in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Read Indices - Printer.WriteLine("INFO", "Parsing surfaces...."); + printCallback?.Invoke("Parsing surfaces...."); var surfaces = ReadGfxSufaces(reader, gfxMapAsset.GfxSurfacesPointer, gfxMapAsset.SurfaceCount); - Printer.WriteLine("INFO", String.Format("Parsed surfaces in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed surfaces in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Write OBJ - Printer.WriteLine("INFO", "Converting to OBJ...."); + printCallback?.Invoke("Converting to OBJ...."); // Create new OBJ var obj = new WavefrontOBJ(); @@ -402,13 +402,13 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l mapFile.DumpToMap(outputName + ".map"); // Done - Printer.WriteLine("INFO", String.Format("Converted to OBJ in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Converted to OBJ in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); } } else { - Printer.WriteLine("ERROR", "Call of Duty: Ghosts is supported, but this EXE is not.", ConsoleColor.DarkRed); + printCallback?.Invoke("Call of Duty: Ghosts is supported, but this EXE is not."); } } @@ -469,7 +469,7 @@ public static Vertex[] ReadGfxVertices(ProcessReader reader, long address, int c gfxVertex.Y * 2.54, gfxVertex.Z * 2.54), // Decode and set normal (from DTZxPorter - Wraith, same as XModels) - Normal = VertexNormal.UnpackB(gfxVertex.Normal), + Normal = VertexNormalUnpacking.MethodB(gfxVertex.Normal), // Set UV UV = new Vector2(gfxVertex.U, 1 - gfxVertex.V) }; @@ -539,5 +539,10 @@ public static WavefrontOBJ.Material ReadMaterial(ProcessReader reader, long addr // Done return entities; } + + public static void ResolveAddresses(ProcessReader reader, string gameType) + { + + } } } \ No newline at end of file diff --git a/src/Husky/Husky/Games/InfiniteWarfare.cs b/src/Husky/Husky/Games/InfiniteWarfare.cs new file mode 100644 index 0000000..a895979 --- /dev/null +++ b/src/Husky/Husky/Games/InfiniteWarfare.cs @@ -0,0 +1,561 @@ +// ------------------------------------------------------------------------ +// Husky - Call of Duty BSP Extractor +// Copyright (C) 2018 Philip/Scobalula +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ------------------------------------------------------------------------ +using PhilLibX; +using PhilLibX.IO; +using System; +using System.IO; +using System.Diagnostics; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace Husky +{ + /// + /// IW Logic + /// + public class InfiniteWarfare + { + /// + /// IW GfxMap TRZone Asset (some pointers we skip over point to DirectX routines, etc. if that means anything to anyone) + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public unsafe struct GfxMapTRZone + { + /// + /// Unknown Bytes + /// + public fixed byte Padding[0x50]; + + /// + /// A pointer to the name of the map + /// + public long MapNamePointer { get; set; } + + /// + /// Unknown Bytes + /// + public int Padding1 { get; set; } + + /// + /// Number of Gfx Vertices (XYZ, etc.) + /// + public int GfxVertexCount { get; set; } + + /// + /// Pointer to the Gfx Vertex Data + /// + public long GfxVerticesPointer { get; set; } + + /// + /// Unknown Bytes + /// + public fixed byte Padding2[0x38]; + } + + /// + /// IW GfxMap Asset (some pointers we skip over point to DirectX routines, etc. if that means anything to anyone) + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public unsafe struct GfxMap + { + /// + /// A pointer to the name of this GfxMap Asset + /// + public long NamePointer { get; set; } + + /// + /// A pointer to the name of the map + /// + public long MapNamePointer { get; set; } + + /// + /// Unknown Bytes (Possibly counts for other data we don't care about) + /// + public fixed byte Padding[0xC]; + + /// + /// Number of Surfaces + /// + public int SurfaceCount { get; set; } + + /// + /// Unknown Bytes (Possibly counts, pointers, etc. for other data we don't care about) + /// + public fixed byte Padding1[0x278]; + + /// + /// Number of Gfx Indices (for Faces) + /// + public long GfxIndicesCount { get; set; } + + /// + /// Pointer to the Gfx Index Data + /// + public long GfxIndicesPointer { get; set; } + + /// + /// Points, etc. + /// + public fixed byte Padding2[0x730]; + + /// + /// Number of Static Models + /// + public long GfxStaticModelsCount { get; set; } + + /// + /// Unknown Bytes (more BSP data we probably don't care for) + /// + public fixed byte Padding3[0x508]; + + /// + /// Pointer to the Gfx Surfaces + /// + public long GfxSurfacesPointer { get; set; } + + /// + /// Unknown Bytes (more BSP data we probably don't care for) + /// + public fixed byte Padding4[8]; + + /// + /// Pointer to the Gfx Static Models + /// + public long GfxStaticModelsPointer { get; set; } + } + + /// + /// Gfx Map Surface + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public unsafe struct GfxSurface + { + /// + /// Unknown Int (I know which pointer in the GfxMap it correlates it, but doesn't seem to interest us) + /// + public int UnknownBaseIndex { get; set; } + + /// + /// Base Vertex Index (this is what allows the GfxMap to have 65k+ verts with only 2 byte indices) + /// + public int VertexIndex { get; set; } + + /// + /// Unknown Bytes + /// + public fixed byte Padding[4]; + + /// + /// Number of Vertices this surface has + /// + public ushort VertexCount { get; set; } + + /// + /// Number of Faces this surface has + /// + public ushort FaceCount { get; set; } + + /// + /// Base Face Index (this is what allows the GfxMap to have 65k+ faces with only 2 byte indices) + /// + public int FaceIndex { get; set; } + + /// + /// Unknown Bytes + /// + public fixed byte Padding2[4]; + + /// + /// Pointer to the Material Asset of this Surface + /// + public long MaterialPointer { get; set; } + + /// + /// Unknown Bytes + /// + public fixed byte Padding3[0x10]; + } + + /// + /// Material Asset Info + /// + public unsafe struct Material + { + /// + /// A pointer to the name of this material + /// + public long NamePointer { get; set; } + + /// + /// Unknown Bytes (Flags, settings, etc.) + /// + public fixed byte UnknownBytes[0x28]; + + /// + /// Number of Images this Material has + /// + public byte ImageCount { get; set; } + + /// + /// Unknown Bytes (Flags, settings, etc.) + /// + public fixed byte UnknownBytes1[0x7]; + + /// + /// A pointer to the Tech Set this Material uses + /// + public long TechniqueSetPointer { get; set; } + + /// + /// A pointer to this Material's Image table + /// + public long ImageTablePointer { get; set; } + + /// + /// UnknownPointer (Probably settings that changed based off TechSet) + /// + public long UnknownPointer1 { get; set; } + + /// + /// Unknown Bytes (Flags, settings, etc.) + /// + public fixed byte UnknownBytes2[0xC8]; + } + + /// + /// Gfx Static Model + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public unsafe struct GfxStaticModel + { + /// + /// X Origin + /// + public float X { get; set; } + + /// + /// Y Origin + /// + public float Y { get; set; } + + /// + /// Z Origin + /// + public float Z { get; set; } + + /// + /// 3x3 Rotation Matrix + /// + public fixed float Matrix[9]; + + /// + /// Model Scale + /// + public float ModelScale { get; set; } + + /// + /// Unknown Bytes + /// + public fixed byte UnknownBytes2[4]; + + /// + /// Pointer to the XModel Asset + /// + public long ModelPointer { get; set; } + + /// + /// Unknown Bytes + /// + public fixed byte UnknownBytes3[0x78]; + } + + /// + /// Reads BSP Data + /// + public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, long assetSizesAddress, string gameType, Action printCallback = null) + { + // Found her + printCallback?.Invoke("Found supported game: Call of Duty: Infinite Warfare"); + + // Validate by XModel Name + if (reader.ReadNullTerminatedString(reader.ReadInt64(reader.ReadInt64(assetPoolsAddress + 0x40) + 8)) == "viewmodel_default") + { + // Load BSP Pools (they only have a size of 1 so we don't care about reading more than 1) + var gfxMapAsset = reader.ReadStruct(reader.ReadInt64(assetPoolsAddress + 0xE8)); + var gfxMapTrZoneAsset = reader.ReadStruct(reader.ReadInt64(assetPoolsAddress + 0xF0) + 8); + + // Name + string gfxMapName = reader.ReadNullTerminatedString(gfxMapAsset.NamePointer); + string mapName = reader.ReadNullTerminatedString(gfxMapAsset.MapNamePointer); + + // Verify a BSP is actually loaded (if in base menu, etc, no map is loaded) + if (String.IsNullOrWhiteSpace(gfxMapName)) + { + printCallback?.Invoke("No BSP loaded. Enter Main Menu or a Map to load in the required assets."); + } + else + { + // New IW Map + var mapFile = new IWMap(); + // Print Info + printCallback?.Invoke(String.Format("Loaded Gfx Map - {0}", gfxMapName)); + printCallback?.Invoke(String.Format("Loaded Map - {0}", mapName)); + printCallback?.Invoke(String.Format("Vertex Count - {0}", gfxMapTrZoneAsset.GfxVertexCount)); + printCallback?.Invoke(String.Format("Indices Count - {0}", gfxMapAsset.GfxIndicesCount)); + printCallback?.Invoke(String.Format("Surface Count - {0}", gfxMapAsset.SurfaceCount)); + printCallback?.Invoke(String.Format("Model Count - {0}", gfxMapAsset.GfxStaticModelsCount)); + + // Build output Folder + string outputName = Path.Combine("exported_maps", "infinite_warfare", gameType, mapName, mapName); + Directory.CreateDirectory(Path.GetDirectoryName(outputName)); + + // Stop watch + var stopWatch = Stopwatch.StartNew(); + + // Read Vertices + printCallback?.Invoke("Parsing vertex data...."); + var vertices = ReadGfxVertices(reader, gfxMapTrZoneAsset.GfxVerticesPointer, (int)gfxMapTrZoneAsset.GfxVertexCount); + printCallback?.Invoke(String.Format("Parsed vertex data in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + + // Reset timer + stopWatch.Restart(); + + // Read Indices + printCallback?.Invoke("Parsing surface indices...."); + var indices = ReadGfxIndices(reader, gfxMapAsset.GfxIndicesPointer, (int)gfxMapAsset.GfxIndicesCount); + printCallback?.Invoke(String.Format("Parsed indices in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + + // Reset timer + stopWatch.Restart(); + + // Read Indices + printCallback?.Invoke("Parsing surfaces...."); + var surfaces = ReadGfxSufaces(reader, gfxMapAsset.GfxSurfacesPointer, gfxMapAsset.SurfaceCount); + printCallback?.Invoke(String.Format("Parsed surfaces in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + + // Reset timer + stopWatch.Restart(); + + // Write OBJ + printCallback?.Invoke("Converting to OBJ...."); + + // Create new OBJ + var obj = new WavefrontOBJ(); + + // Append Vertex Data + foreach (var vertex in vertices) + { + obj.Vertices.Add(vertex.Position); + obj.Normals.Add(vertex.Normal); + obj.UVs.Add(vertex.UV); + } + + // Image Names (for Search String) + HashSet imageNames = new HashSet(); + + // Append Faces + foreach (var surface in surfaces) + { + // Create new Material + var material = ReadMaterial(reader, surface.MaterialPointer); + // Add to images + imageNames.Add(material.DiffuseMap); + // Add it + obj.AddMaterial(material); + // Add points + for (ushort i = 0; i < surface.FaceCount; i++) + { + // Face Indices + var faceIndex1 = indices[i * 3 + surface.FaceIndex] + surface.VertexIndex; + var faceIndex2 = indices[i * 3 + surface.FaceIndex + 1] + surface.VertexIndex; + var faceIndex3 = indices[i * 3 + surface.FaceIndex + 2] + surface.VertexIndex; + + // Validate unique points, and write to OBJ + if (faceIndex1 != faceIndex2 && faceIndex1 != faceIndex3 && faceIndex2 != faceIndex3) + { + // new Obj Face + var objFace = new WavefrontOBJ.Face(material.Name); + + // Add points + objFace.Vertices[0] = new WavefrontOBJ.Face.Vertex(faceIndex1, faceIndex1, faceIndex1); + objFace.Vertices[2] = new WavefrontOBJ.Face.Vertex(faceIndex2, faceIndex2, faceIndex2); + objFace.Vertices[1] = new WavefrontOBJ.Face.Vertex(faceIndex3, faceIndex3, faceIndex3); + + // Add to OBJ + obj.Faces.Add(objFace); + } + } + } + + // Save it + obj.Save(outputName + ".obj"); + + // Build search strinmg + string searchString = ""; + + // Loop through images, and append each to the search string (for Wraith/Greyhound) + foreach (string imageName in imageNames) + searchString += String.Format("{0},", Path.GetFileNameWithoutExtension(imageName)); + + // Dump it + File.WriteAllText(outputName + "_search_string.txt", searchString); + + // Read entities and dump to map + mapFile.Entities.AddRange(ReadStaticModels(reader, gfxMapAsset.GfxStaticModelsPointer, (int)gfxMapAsset.GfxStaticModelsCount)); + mapFile.DumpToMap(outputName + ".map"); + + // Done + printCallback?.Invoke(String.Format("Converted to OBJ in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + } + + } + else + { + printCallback?.Invoke("Call of Duty: Infinite Warfare is supported, but this EXE is not."); + } + } + + /// + /// Reads Gfx Surfaces + /// + public static GfxSurface[] ReadGfxSufaces(ProcessReader reader, long address, int count) + { + // Preallocate array + GfxSurface[] surfaces = new GfxSurface[count]; + + // Loop number of indices we have + for (int i = 0; i < count; i++) + // Add it + surfaces[i] = reader.ReadStruct(address + i * 48); + + // Done + return surfaces; + } + + + /// + /// Reads Gfx Vertex Indices + /// + public static ushort[] ReadGfxIndices(ProcessReader reader, long address, int count) + { + // Preallocate short array + ushort[] indices = new ushort[count]; + // Read buffer + var byteBuffer = reader.ReadBytes(address, count * 2); + // Copy buffer + Buffer.BlockCopy(byteBuffer, 0, indices, 0, byteBuffer.Length); + // Done + return indices; + } + + /// + /// Reads Gfx Vertices + /// + public static Vertex[] ReadGfxVertices(ProcessReader reader, long address, int count) + { + // Preallocate vertex array + Vertex[] vertices = new Vertex[count]; + // Read buffer + var byteBuffer = reader.ReadBytes(address, count * 44); + // Loop number of vertices we have + for (int i = 0; i < count; i++) + { + // Read Struct + var gfxVertex = ByteUtil.BytesToStruct(byteBuffer, i * 44); + + // Create new SEModel Vertex + vertices[i] = new Vertex() + { + // Set offset + Position = new Vector3( + gfxVertex.X * 2.54, + gfxVertex.Y * 2.54, + gfxVertex.Z * 2.54), + // Decode and set normal (from DTZxPorter - Wraith, same as XModels) + Normal = VertexNormalUnpacking.MethodB(gfxVertex.Normal), + // Set UV + UV = new Vector2(gfxVertex.U, 1 - gfxVertex.V) + }; + } + + // Done + return vertices; + } + + /// + /// Reads a material for the given surface and its associated images + /// + public static WavefrontOBJ.Material ReadMaterial(ProcessReader reader, long address) + { + // Read Material + var material = reader.ReadStruct(address); + // Create new OBJ Image + var objMaterial = new WavefrontOBJ.Material(Path.GetFileNameWithoutExtension(reader.ReadNullTerminatedString(reader.ReadInt64(address)).Replace("*", ""))); + // Loop over images + for (byte i = 0; i < material.ImageCount; i++) + { + // Read Material Image + var materialImage = reader.ReadStruct(material.ImageTablePointer + i * Marshal.SizeOf()); + // Check for color map for now + if (materialImage.SemanticHash == 0xA0AB1041) + objMaterial.DiffuseMap = "_images\\\\" + reader.ReadNullTerminatedString(reader.ReadInt64(materialImage.ImagePointer + 104)) + ".png"; + } + // Done + return objMaterial; + } + + /// + /// Reads Static Models + /// + public unsafe static List ReadStaticModels(ProcessReader reader, long address, int count) + { + // Resulting Entities + List entities = new List(count); + // Read buffer + var byteBuffer = reader.ReadBytes(address, count * Marshal.SizeOf()); + // Loop number of models we have + for (int i = 0; i < count; i++) + { + // Read Struct + var staticModel = ByteUtil.BytesToStruct(byteBuffer, i * Marshal.SizeOf()); + // Model Name + var modelName = reader.ReadNullTerminatedString(reader.ReadInt64(staticModel.ModelPointer)); + // New Matrix + var matrix = new Rotation.Matrix(); + // Copy X Values + matrix.Values[0] = staticModel.Matrix[0]; + matrix.Values[1] = staticModel.Matrix[1]; + matrix.Values[2] = staticModel.Matrix[2]; + // Copy Y Values + matrix.Values[4] = staticModel.Matrix[3]; + matrix.Values[5] = staticModel.Matrix[4]; + matrix.Values[6] = staticModel.Matrix[5]; + // Copy Z Values + matrix.Values[8] = staticModel.Matrix[6]; + matrix.Values[9] = staticModel.Matrix[7]; + matrix.Values[10] = staticModel.Matrix[8]; + // Convert to Euler + var euler = matrix.ToEuler(); + // Add it + entities.Add(IWMap.Entity.CreateMiscModel(modelName, new Vector3(staticModel.X, staticModel.Y, staticModel.Z), Rotation.ToDegrees(euler), staticModel.ModelScale)); + } + // Done + return entities; + } + } +} diff --git a/src/Husky/Husky/Games/ModernWarfare.cs b/src/Husky/Husky/Games/ModernWarfare.cs index 7fe6ea1..95cfed3 100644 --- a/src/Husky/Husky/Games/ModernWarfare.cs +++ b/src/Husky/Husky/Games/ModernWarfare.cs @@ -235,10 +235,10 @@ public unsafe struct GfxStaticModel /// /// Reads BSP Data /// - public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, long assetSizesAddress, string gameType) + public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, long assetSizesAddress, string gameType, Action printCallback = null) { // Found her - Printer.WriteLine("INFO", "Found supported game: Call of Duty: Modern Warfare"); + printCallback?.Invoke("Found supported game: Call of Duty: Modern Warfare"); // Get XModel Name var firstXModelName = reader.ReadNullTerminatedString(reader.ReadInt32(reader.ReadInt32(assetPoolsAddress + 0xC) + 4)); // Validate by XModel Name @@ -254,19 +254,19 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l // Verify a BSP is actually loaded (if in base menu, etc, no map is loaded) if (String.IsNullOrWhiteSpace(gfxMapName)) { - Printer.WriteLine("ERROR", "No BSP loaded. Enter Main Menu or a Map to load in the required assets.", ConsoleColor.DarkRed); + printCallback?.Invoke("No BSP loaded. Enter Main Menu or a Map to load in the required assets."); } else { // New IW Map var mapFile = new IWMap(); // Print Info - Printer.WriteLine("INFO", String.Format("Loaded Gfx Map - {0}", gfxMapName)); - Printer.WriteLine("INFO", String.Format("Loaded Map - {0}", mapName)); - Printer.WriteLine("INFO", String.Format("Vertex Count - {0}", gfxMapAsset.GfxVertexCount)); - Printer.WriteLine("INFO", String.Format("Indices Count - {0}", gfxMapAsset.GfxIndicesCount)); - Printer.WriteLine("INFO", String.Format("Surface Count - {0}", gfxMapAsset.SurfaceCount)); - Printer.WriteLine("INFO", String.Format("Model Count - {0}", gfxMapAsset.GfxStaticModelsCount)); + printCallback?.Invoke(String.Format("Loaded Gfx Map - {0}", gfxMapName)); + printCallback?.Invoke(String.Format("Loaded Map - {0}", mapName)); + printCallback?.Invoke(String.Format("Vertex Count - {0}", gfxMapAsset.GfxVertexCount)); + printCallback?.Invoke(String.Format("Indices Count - {0}", gfxMapAsset.GfxIndicesCount)); + printCallback?.Invoke(String.Format("Surface Count - {0}", gfxMapAsset.SurfaceCount)); + printCallback?.Invoke(String.Format("Model Count - {0}", gfxMapAsset.GfxStaticModelsCount)); // Build output Folder string outputName = Path.Combine("exported_maps", "modern_warfare", gameType, mapName, mapName); @@ -276,31 +276,31 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l var stopWatch = Stopwatch.StartNew(); // Read Vertices - Printer.WriteLine("INFO", "Parsing vertex data...."); + printCallback?.Invoke("Parsing vertex data...."); var vertices = ReadGfxVertices(reader, gfxMapAsset.GfxVerticesPointer, gfxMapAsset.GfxVertexCount); - Printer.WriteLine("INFO", String.Format("Parsed vertex data in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed vertex data in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Read Indices - Printer.WriteLine("INFO", "Parsing surface indices...."); + printCallback?.Invoke("Parsing surface indices...."); var indices = ReadGfxIndices(reader, gfxMapAsset.GfxIndicesPointer, gfxMapAsset.GfxIndicesCount); - Printer.WriteLine("INFO", String.Format("Parsed indices in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed indices in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Read Indices - Printer.WriteLine("INFO", "Parsing surfaces...."); + printCallback?.Invoke("Parsing surfaces...."); var surfaces = ReadGfxSufaces(reader, gfxMapAsset.GfxSurfacesPointer, gfxMapAsset.SurfaceCount); - Printer.WriteLine("INFO", String.Format("Parsed surfaces in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed surfaces in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Write OBJ - Printer.WriteLine("INFO", "Converting to OBJ...."); + printCallback?.Invoke("Converting to OBJ...."); // Create new OBJ var obj = new WavefrontOBJ(); @@ -368,13 +368,13 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l mapFile.DumpToMap(outputName + ".map"); // Done - Printer.WriteLine("INFO", String.Format("Converted to OBJ in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Converted to OBJ in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); } } else { - Printer.WriteLine("ERROR", "Call of Duty: Modern Warfare is supported, but this EXE is not.", ConsoleColor.DarkRed); + printCallback?.Invoke("Call of Duty: Modern Warfare is supported, but this EXE is not."); } } @@ -434,7 +434,7 @@ public static Vertex[] ReadGfxVertices(ProcessReader reader, long address, int c gfxVertex.Y * 2.54, gfxVertex.Z * 2.54), // Decode and set normal (from DTZxPorter - Wraith, same as XModels) - Normal = VertexNormal.UnpackA(gfxVertex.Normal), + Normal = VertexNormalUnpacking.MethodA(gfxVertex.Normal), // Set UV UV = new Vector2(gfxVertex.U, 1 - gfxVertex.V) }; diff --git a/src/Husky/Husky/Games/ModernWarfare2.cs b/src/Husky/Husky/Games/ModernWarfare2.cs index ed22727..a589c40 100644 --- a/src/Husky/Husky/Games/ModernWarfare2.cs +++ b/src/Husky/Husky/Games/ModernWarfare2.cs @@ -193,10 +193,10 @@ public unsafe struct Material /// /// Reads BSP Data /// - public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, long assetSizesAddress, string gameType) + public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, long assetSizesAddress, string gameType, Action printCallback = null) { // Found her - Printer.WriteLine("INFO", "Found supported game: Call of Duty: Modern Warfare 2"); + printCallback?.Invoke("Found supported game: Call of Duty: Modern Warfare 2"); // Validate by XModel Name if (reader.ReadNullTerminatedString(reader.ReadInt32(reader.ReadInt32(assetPoolsAddress + 0x10) + 4)) == "void") { @@ -210,19 +210,19 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l // Verify a BSP is actually loaded (if in base menu, etc, no map is loaded) if (String.IsNullOrWhiteSpace(gfxMapName)) { - Printer.WriteLine("ERROR", "No BSP loaded. Enter Main Menu or a Map to load in the required assets.", ConsoleColor.DarkRed); + printCallback?.Invoke("No BSP loaded. Enter Main Menu or a Map to load in the required assets."); } else { // New IW Map var mapFile = new IWMap(); // Print Info - Printer.WriteLine("INFO", String.Format("Loaded Gfx Map - {0}", gfxMapName)); - Printer.WriteLine("INFO", String.Format("Loaded Map - {0}", mapName)); - Printer.WriteLine("INFO", String.Format("Vertex Count - {0}", gfxMapAsset.GfxVertexCount)); - Printer.WriteLine("INFO", String.Format("Indices Count - {0}", gfxMapAsset.GfxIndicesCount)); - Printer.WriteLine("INFO", String.Format("Surface Count - {0}", gfxMapAsset.SurfaceCount)); - Printer.WriteLine("INFO", String.Format("Model Count - {0}", gfxMapAsset.GfxStaticModelsCount)); + printCallback?.Invoke(String.Format("Loaded Gfx Map - {0}", gfxMapName)); + printCallback?.Invoke(String.Format("Loaded Map - {0}", mapName)); + printCallback?.Invoke(String.Format("Vertex Count - {0}", gfxMapAsset.GfxVertexCount)); + printCallback?.Invoke(String.Format("Indices Count - {0}", gfxMapAsset.GfxIndicesCount)); + printCallback?.Invoke(String.Format("Surface Count - {0}", gfxMapAsset.SurfaceCount)); + printCallback?.Invoke(String.Format("Model Count - {0}", gfxMapAsset.GfxStaticModelsCount)); // Build output Folder string outputName = Path.Combine("exported_maps", "modern_warfare_2", gameType, mapName, mapName); @@ -232,31 +232,31 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l var stopWatch = Stopwatch.StartNew(); // Read Vertices - Printer.WriteLine("INFO", "Parsing vertex data...."); + printCallback?.Invoke("Parsing vertex data...."); var vertices = ReadGfxVertices(reader, gfxMapAsset.GfxVerticesPointer, gfxMapAsset.GfxVertexCount); - Printer.WriteLine("INFO", String.Format("Parsed vertex data in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed vertex data in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Read Indices - Printer.WriteLine("INFO", "Parsing surface indices...."); + printCallback?.Invoke("Parsing surface indices...."); var indices = ReadGfxIndices(reader, gfxMapAsset.GfxIndicesPointer, gfxMapAsset.GfxIndicesCount); - Printer.WriteLine("INFO", String.Format("Parsed indices in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed indices in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Read Indices - Printer.WriteLine("INFO", "Parsing surfaces...."); + printCallback?.Invoke("Parsing surfaces...."); var surfaces = ReadGfxSufaces(reader, gfxMapAsset.GfxSurfacesPointer, gfxMapAsset.SurfaceCount); - Printer.WriteLine("INFO", String.Format("Parsed surfaces in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed surfaces in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Write OBJ - Printer.WriteLine("INFO", "Converting to OBJ...."); + printCallback?.Invoke("Converting to OBJ...."); // Create new OBJ var obj = new WavefrontOBJ(); @@ -324,13 +324,13 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l mapFile.DumpToMap(outputName + ".map"); // Done - Printer.WriteLine("INFO", String.Format("Converted to OBJ in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Converted to OBJ in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); } } else { - Printer.WriteLine("ERROR", "Call of Duty: Modern Warfare 2 is supported, but this EXE is not.", ConsoleColor.DarkRed); + printCallback?.Invoke("Call of Duty: Modern Warfare 2 is supported, but this EXE is not."); } } @@ -390,7 +390,7 @@ public static Vertex[] ReadGfxVertices(ProcessReader reader, long address, int c gfxVertex.Y * 2.54, gfxVertex.Z * 2.54), // Decode and set normal (from DTZxPorter - Wraith, same as XModels) - Normal = VertexNormal.UnpackA(gfxVertex.Normal), + Normal = VertexNormalUnpacking.MethodA(gfxVertex.Normal), // Set UV UV = new Vector2(gfxVertex.U, 1 - gfxVertex.V) }; diff --git a/src/Husky/Husky/Games/ModernWarfare3.cs b/src/Husky/Husky/Games/ModernWarfare3.cs index ba2e326..34c3226 100644 --- a/src/Husky/Husky/Games/ModernWarfare3.cs +++ b/src/Husky/Husky/Games/ModernWarfare3.cs @@ -193,10 +193,10 @@ public unsafe struct GfxStaticModel /// /// Reads BSP Data /// - public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, long assetSizesAddress, string gameType) + public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, long assetSizesAddress, string gameType, Action printCallback = null) { // Found her - Printer.WriteLine("INFO", "Found supported game: Call of Duty: Modern Warfare 3"); + printCallback?.Invoke("Found supported game: Call of Duty: Modern Warfare 3"); // Validate by XModel Name if (reader.ReadNullTerminatedString(reader.ReadInt32(reader.ReadInt32(assetPoolsAddress + 0x10) + 4)) == "void") { @@ -210,19 +210,19 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l // Verify a BSP is actually loaded (if in base menu, etc, no map is loaded) if (String.IsNullOrWhiteSpace(gfxMapName)) { - Printer.WriteLine("ERROR", "No BSP loaded. Enter Main Menu or a Map to load in the required assets.", ConsoleColor.DarkRed); + printCallback?.Invoke("No BSP loaded. Enter Main Menu or a Map to load in the required assets."); } else { // New IW Map var mapFile = new IWMap(); // Print Info - Printer.WriteLine("INFO", String.Format("Loaded Gfx Map - {0}", gfxMapName)); - Printer.WriteLine("INFO", String.Format("Loaded Map - {0}", mapName)); - Printer.WriteLine("INFO", String.Format("Vertex Count - {0}", gfxMapAsset.GfxVertexCount)); - Printer.WriteLine("INFO", String.Format("Indices Count - {0}", gfxMapAsset.GfxIndicesCount)); - Printer.WriteLine("INFO", String.Format("Surface Count - {0}", gfxMapAsset.SurfaceCount)); - Printer.WriteLine("INFO", String.Format("Model Count - {0}", gfxMapAsset.GfxStaticModelsCount)); + printCallback?.Invoke(String.Format("Loaded Gfx Map - {0}", gfxMapName)); + printCallback?.Invoke(String.Format("Loaded Map - {0}", mapName)); + printCallback?.Invoke(String.Format("Vertex Count - {0}", gfxMapAsset.GfxVertexCount)); + printCallback?.Invoke(String.Format("Indices Count - {0}", gfxMapAsset.GfxIndicesCount)); + printCallback?.Invoke(String.Format("Surface Count - {0}", gfxMapAsset.SurfaceCount)); + printCallback?.Invoke(String.Format("Model Count - {0}", gfxMapAsset.GfxStaticModelsCount)); // Build output Folder string outputName = Path.Combine("exported_maps", "modern_warfare_3", gameType, mapName, mapName); @@ -232,31 +232,31 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l var stopWatch = Stopwatch.StartNew(); // Read Vertices - Printer.WriteLine("INFO", "Parsing vertex data...."); + printCallback?.Invoke("Parsing vertex data...."); var vertices = ReadGfxVertices(reader, gfxMapAsset.GfxVerticesPointer, gfxMapAsset.GfxVertexCount); - Printer.WriteLine("INFO", String.Format("Parsed vertex data in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed vertex data in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Read Indices - Printer.WriteLine("INFO", "Parsing surface indices...."); + printCallback?.Invoke("Parsing surface indices...."); var indices = ReadGfxIndices(reader, gfxMapAsset.GfxIndicesPointer, gfxMapAsset.GfxIndicesCount); - Printer.WriteLine("INFO", String.Format("Parsed indices in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed indices in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Read Indices - Printer.WriteLine("INFO", "Parsing surfaces...."); + printCallback?.Invoke("Parsing surfaces...."); var surfaces = ReadGfxSufaces(reader, gfxMapAsset.GfxSurfacesPointer, gfxMapAsset.SurfaceCount); - Printer.WriteLine("INFO", String.Format("Parsed surfaces in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed surfaces in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Write OBJ - Printer.WriteLine("INFO", "Converting to OBJ...."); + printCallback?.Invoke("Converting to OBJ...."); // Create new OBJ var obj = new WavefrontOBJ(); @@ -324,13 +324,13 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l mapFile.DumpToMap(outputName + ".map"); // Done - Printer.WriteLine("INFO", String.Format("Converted to OBJ in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Converted to OBJ in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); } } else { - Printer.WriteLine("ERROR", "Call of Duty: Modern Warfare 3 is supported, but this EXE is not.", ConsoleColor.DarkRed); + printCallback?.Invoke("Call of Duty: Modern Warfare 3 is supported, but this EXE is not."); } } @@ -389,7 +389,7 @@ public static Vertex[] ReadGfxVertices(ProcessReader reader, long address, int c gfxVertex.Y * 2.54, gfxVertex.Z * 2.54), // Decode and set normal (from DTZxPorter - Wraith, same as XModels) - Normal = VertexNormal.UnpackA(gfxVertex.Normal), + Normal = VertexNormalUnpacking.MethodA(gfxVertex.Normal), // Set UV UV = new Vector2(gfxVertex.U, 1 - gfxVertex.V) }; diff --git a/src/Husky/Husky/Games/ModernWarfareRM.cs b/src/Husky/Husky/Games/ModernWarfareRM.cs index 6b7b481..6cd32e6 100644 --- a/src/Husky/Husky/Games/ModernWarfareRM.cs +++ b/src/Husky/Husky/Games/ModernWarfareRM.cs @@ -106,7 +106,6 @@ public unsafe struct GfxMap /// public long GfxSurfacesPointer { get; set; } - /// /// Unknown Bytes (more BSP data we probably don't care for) /// @@ -266,10 +265,10 @@ public unsafe struct GfxStaticModel /// /// Reads BSP Data /// - public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, long assetSizesAddress, string gameType) + public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, long assetSizesAddress, string gameType, Action printCallback = null) { // Found her - Printer.WriteLine("INFO", "Found supported game: Call of Duty: Modern Warfare Remastered"); + printCallback?.Invoke("Found supported game: Call of Duty: Modern Warfare Remastered"); // Validate by XModel Name if (reader.ReadNullTerminatedString(reader.ReadInt64(reader.ReadInt64(reader.GetBaseAddress() + assetPoolsAddress + 0x38) + 8)) == "fx") @@ -284,19 +283,19 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l // Verify a BSP is actually loaded (if in base menu, etc, no map is loaded) if (String.IsNullOrWhiteSpace(gfxMapName)) { - Printer.WriteLine("ERROR", "No BSP loaded. Enter Main Menu or a Map to load in the required assets.", ConsoleColor.DarkRed); + printCallback?.Invoke("No BSP loaded. Enter Main Menu or a Map to load in the required assets."); } else { // New IW Map var mapFile = new IWMap(); // Print Info - Printer.WriteLine("INFO", String.Format("Loaded Gfx Map - {0}", gfxMapName)); - Printer.WriteLine("INFO", String.Format("Loaded Map - {0}", mapName)); - Printer.WriteLine("INFO", String.Format("Vertex Count - {0}", gfxMapAsset.GfxVertexCount)); - Printer.WriteLine("INFO", String.Format("Indices Count - {0}", gfxMapAsset.GfxIndicesCount)); - Printer.WriteLine("INFO", String.Format("Surface Count - {0}", gfxMapAsset.SurfaceCount)); - Printer.WriteLine("INFO", String.Format("Model Count - {0}", gfxMapAsset.GfxStaticModelsCount)); + printCallback?.Invoke(String.Format("Loaded Gfx Map - {0}", gfxMapName)); + printCallback?.Invoke(String.Format("Loaded Map - {0}", mapName)); + printCallback?.Invoke(String.Format("Vertex Count - {0}", gfxMapAsset.GfxVertexCount)); + printCallback?.Invoke(String.Format("Indices Count - {0}", gfxMapAsset.GfxIndicesCount)); + printCallback?.Invoke(String.Format("Surface Count - {0}", gfxMapAsset.SurfaceCount)); + printCallback?.Invoke(String.Format("Model Count - {0}", gfxMapAsset.GfxStaticModelsCount)); // Build output Folder string outputName = Path.Combine("exported_maps", "modern_warfare_rm", gameType, mapName, mapName); @@ -306,31 +305,31 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l var stopWatch = Stopwatch.StartNew(); // Read Vertices - Printer.WriteLine("INFO", "Parsing vertex data...."); + printCallback?.Invoke("Parsing vertex data...."); var vertices = ReadGfxVertices(reader, gfxMapAsset.GfxVerticesPointer, (int)gfxMapAsset.GfxVertexCount); - Printer.WriteLine("INFO", String.Format("Parsed vertex data in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed vertex data in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Read Indices - Printer.WriteLine("INFO", "Parsing surface indices...."); + printCallback?.Invoke("Parsing surface indices...."); var indices = ReadGfxIndices(reader, gfxMapAsset.GfxIndicesPointer, (int)gfxMapAsset.GfxIndicesCount); - Printer.WriteLine("INFO", String.Format("Parsed indices in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed indices in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Read Indices - Printer.WriteLine("INFO", "Parsing surfaces...."); + printCallback?.Invoke("Parsing surfaces...."); var surfaces = ReadGfxSufaces(reader, gfxMapAsset.GfxSurfacesPointer, gfxMapAsset.SurfaceCount); - Printer.WriteLine("INFO", String.Format("Parsed surfaces in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed surfaces in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Write OBJ - Printer.WriteLine("INFO", "Converting to OBJ...."); + printCallback?.Invoke("Converting to OBJ...."); // Create new OBJ var obj = new WavefrontOBJ(); @@ -398,13 +397,13 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l mapFile.DumpToMap(outputName + ".map"); // Done - Printer.WriteLine("INFO", String.Format("Converted to OBJ in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Converted to OBJ in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); } } else { - Printer.WriteLine("ERROR", "Call of Duty: Modern Warfare Remastered is supported, but this EXE is not.", ConsoleColor.DarkRed); + printCallback?.Invoke("Call of Duty: Modern Warfare Remastered is supported, but this EXE is not."); } } @@ -465,7 +464,7 @@ public static Vertex[] ReadGfxVertices(ProcessReader reader, long address, int c gfxVertex.Y * 2.54, gfxVertex.Z * 2.54), // Decode and set normal (from DTZxPorter - Wraith, same as XModels) - Normal = VertexNormal.UnpackB(gfxVertex.Normal), + Normal = VertexNormalUnpacking.MethodB(gfxVertex.Normal), // Set UV UV = new Vector2(gfxVertex.U, 1 - gfxVertex.V) }; diff --git a/src/Husky/Husky/Games/WorldAtWar.cs b/src/Husky/Husky/Games/WorldAtWar.cs index 9bab2bd..501bc41 100644 --- a/src/Husky/Husky/Games/WorldAtWar.cs +++ b/src/Husky/Husky/Games/WorldAtWar.cs @@ -250,10 +250,10 @@ public unsafe struct GfxStaticModel /// /// Reads BSP Data /// - public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, long assetSizesAddress, string gameType) + public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, long assetSizesAddress, string gameType, Action printCallback = null) { // Found her - Printer.WriteLine("INFO", "Found supported game: Call of Duty: World At War"); + printCallback?.Invoke("Found supported game: Call of Duty: World At War"); // Get XModel Name var firstXModelName = reader.ReadNullTerminatedString(reader.ReadInt32(reader.ReadInt32(assetPoolsAddress + 0x14) + 4)); @@ -271,19 +271,19 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l // Verify a BSP is actually loaded (if in base menu, etc, no map is loaded) if (String.IsNullOrWhiteSpace(gfxMapName)) { - Printer.WriteLine("ERROR", "No BSP loaded. Enter Main Menu or a Map to load in the required assets.", ConsoleColor.DarkRed); + printCallback?.Invoke("No BSP loaded. Enter Main Menu or a Map to load in the required assets."); } else { // New IW Map var mapFile = new IWMap(); // Print Info - Printer.WriteLine("INFO", String.Format("Loaded Gfx Map - {0}", gfxMapName)); - Printer.WriteLine("INFO", String.Format("Loaded Map - {0}", mapName)); - Printer.WriteLine("INFO", String.Format("Vertex Count - {0}", gfxMapAsset.GfxVertexCount)); - Printer.WriteLine("INFO", String.Format("Indices Count - {0}", gfxMapAsset.GfxIndicesCount)); - Printer.WriteLine("INFO", String.Format("Surface Count - {0}", gfxMapAsset.SurfaceCount)); - Printer.WriteLine("INFO", String.Format("Model Count - {0}", gfxMapAsset.GfxStaticModelsCount)); + printCallback?.Invoke(String.Format("Loaded Gfx Map - {0}", gfxMapName)); + printCallback?.Invoke(String.Format("Loaded Map - {0}", mapName)); + printCallback?.Invoke(String.Format("Vertex Count - {0}", gfxMapAsset.GfxVertexCount)); + printCallback?.Invoke(String.Format("Indices Count - {0}", gfxMapAsset.GfxIndicesCount)); + printCallback?.Invoke(String.Format("Surface Count - {0}", gfxMapAsset.SurfaceCount)); + printCallback?.Invoke(String.Format("Model Count - {0}", gfxMapAsset.GfxStaticModelsCount)); // Build output Folder string outputName = Path.Combine("exported_maps", "world_at_war", gameType, mapName, mapName); @@ -293,31 +293,31 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l var stopWatch = Stopwatch.StartNew(); // Read Vertices - Printer.WriteLine("INFO", "Parsing vertex data...."); + printCallback?.Invoke("Parsing vertex data...."); var vertices = ReadGfxVertices(reader, gfxMapAsset.GfxVerticesPointer, gfxMapAsset.GfxVertexCount); - Printer.WriteLine("INFO", String.Format("Parsed vertex data in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed vertex data in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Read Indices - Printer.WriteLine("INFO", "Parsing surface indices...."); + printCallback?.Invoke("Parsing surface indices...."); var indices = ReadGfxIndices(reader, gfxMapAsset.GfxIndicesPointer, gfxMapAsset.GfxIndicesCount); - Printer.WriteLine("INFO", String.Format("Parsed indices in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed indices in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Read Indices - Printer.WriteLine("INFO", "Parsing surfaces...."); + printCallback?.Invoke("Parsing surfaces...."); var surfaces = ReadGfxSufaces(reader, gfxMapAsset.GfxSurfacesPointer, gfxMapAsset.SurfaceCount); - Printer.WriteLine("INFO", String.Format("Parsed surfaces in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Parsed surfaces in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); // Reset timer stopWatch.Restart(); // Write OBJ - Printer.WriteLine("INFO", "Converting to OBJ...."); + printCallback?.Invoke("Converting to OBJ...."); // Create new OBJ var obj = new WavefrontOBJ(); @@ -385,13 +385,13 @@ public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, l mapFile.DumpToMap(outputName + ".map"); // Done - Printer.WriteLine("INFO", String.Format("Converted to OBJ in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + printCallback?.Invoke(String.Format("Converted to OBJ in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); } } else { - Printer.WriteLine("ERROR", "Call of Duty: World At War is supported, but this EXE is not.", ConsoleColor.DarkRed); + printCallback?.Invoke("Call of Duty: World At War is supported, but this EXE is not."); } } @@ -451,7 +451,7 @@ public static Vertex[] ReadGfxVertices(ProcessReader reader, long address, int c gfxVertex.Y * 2.54, gfxVertex.Z * 2.54), // Decode and set normal (from DTZxPorter - Wraith, same as XModels) - Normal = VertexNormal.UnpackA(gfxVertex.Normal), + Normal = VertexNormalUnpacking.MethodA(gfxVertex.Normal), // Set UV UV = new Vector2(gfxVertex.U, 1 - gfxVertex.V) }; diff --git a/src/Husky/Husky/Games/WorldWarII.cs b/src/Husky/Husky/Games/WorldWarII.cs new file mode 100644 index 0000000..5c206fd --- /dev/null +++ b/src/Husky/Husky/Games/WorldWarII.cs @@ -0,0 +1,643 @@ +// ------------------------------------------------------------------------ +// Husky - Call of Duty BSP Extractor +// Copyright (C) 2018 Philip/Scobalula +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ------------------------------------------------------------------------ +using PhilLibX; +using PhilLibX.IO; +using System; +using System.IO; +using System.Diagnostics; +using System.Collections.Generic; +using System.Runtime.InteropServices; + +namespace Husky +{ + /// + /// WW2 Logic + /// + public class WorldWarII + { + /// + /// WW2 GfxMap TRZone Asset (some pointers we skip over point to DirectX routines, etc. if that means anything to anyone) + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public unsafe struct GfxMapTRZone + { + /// + /// Unknown Bytes + /// + public fixed byte Padding[0x98]; + + /// + /// A pointer to the name of the map + /// + public long MapNamePointer { get; set; } + + /// + /// Unknown Bytes + /// + public int Padding1 { get; set; } + + /// + /// Number of Gfx Vertices (XYZ, etc.) + /// + public int GfxVertexCount { get; set; } + + /// + /// Pointer to the Gfx Vertex Positions Data + /// + public long GfxVertexPositionsPointer { get; set; } + + /// + /// Pointer to the Gfx Vertex Positions Data + /// + public long GfxVertexUnknownPointer { get; set; } + + /// + /// Pointer to the Gfx Vertex Colors Data + /// + public long GfxVertexColorsPointer { get; set; } + + /// + /// Pointer to the Gfx Vertex UVs Data + /// + public long GfxVertexUVsPointer { get; set; } + + /// + /// Unknown Pointer + /// + public long GfxVertexUnknown2Pointer { get; set; } + + /// + /// Pointer to the Gfx Vertex Normals Data + /// + public long GfxVertexNormalsPointer { get; set; } + + /// + /// Unknown Pointer (Probably tangents) + /// + public long GfxVertexUnknown3Pointer { get; set; } + + /// + /// Unknown Bytes + /// + public fixed byte Padding2[0x50]; + } + + /// + /// IW GfxMap Asset (some pointers we skip over point to DirectX routines, etc. if that means anything to anyone) + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public unsafe struct GfxMap + { + /// + /// A pointer to the name of this GfxMap Asset + /// + public long NamePointer { get; set; } + + /// + /// A pointer to the name of the map + /// + public long MapNamePointer { get; set; } + + /// + /// Unknown Bytes (Possibly counts for other data we don't care about) + /// + public fixed byte Padding[0xC]; + + /// + /// Number of Surfaces + /// + public int SurfaceCount { get; set; } + + /// + /// Unknown Bytes (Possibly counts, pointers, etc. for other data we don't care about) + /// + public fixed byte Padding1[0x3A8]; + + /// + /// Number of Gfx Indices (for Faces) + /// + public long GfxIndicesCount { get; set; } + + /// + /// Pointer to the Gfx Index Data + /// + public long GfxIndicesPointer { get; set; } + + /// + /// Points, etc. + /// + public fixed byte Padding2[0x750]; + + /// + /// Number of Static Models + /// + public long GfxStaticModelsCount { get; set; } + + /// + /// Unknown Bytes (more BSP data we probably don't care for) + /// + public fixed byte Padding3[0x468]; + + /// + /// Pointer to the Gfx Surfaces + /// + public long GfxSurfacesPointer { get; set; } + + /// + /// Unknown Bytes (more BSP data we probably don't care for) + /// + public fixed byte Padding4[0x18]; + + /// + /// Pointer to the Gfx Static Models + /// + public long GfxStaticModelsPointer { get; set; } + } + + /// + /// Gfx Map Surface + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public unsafe struct GfxSurface + { + /// + /// Unknown Int (I know which pointer in the GfxMap it correlates it, but doesn't seem to interest us) + /// + public int UnknownBaseIndex { get; set; } + + /// + /// Base Vertex Index (this is what allows the GfxMap to have 65k+ verts with only 2 byte indices) + /// + public int VertexIndex { get; set; } + + /// + /// Number of Vertices this surface has + /// + public ushort VertexCount { get; set; } + + /// + /// Number of Faces this surface has + /// + public ushort FaceCount { get; set; } + + /// + /// Unknown Bytes + /// + public fixed byte Padding[0xC]; + + /// + /// Base Face Index (this is what allows the GfxMap to have 65k+ faces with only 2 byte indices) + /// + public int FaceIndex { get; set; } + + /// + /// Unknown Bytes + /// + public fixed byte Padding2[4]; + + /// + /// Pointer to the Material Asset of this Surface + /// + public long MaterialPointer { get; set; } + + /// + /// Unknown Bytes + /// + public fixed byte Padding3[0x10]; + } + + /// + /// Material Asset Info + /// + public unsafe struct Material + { + /// + /// A pointer to the name of this material + /// + public long NamePointer { get; set; } + + /// + /// Unknown Bytes (Flags, settings, etc.) + /// + public fixed byte UnknownBytes[0x9A]; + + /// + /// Number of Images this Material has + /// + public byte ImageCount { get; set; } + + /// + /// Unknown Bytes (Flags, settings, etc.) + /// + public fixed byte UnknownBytes1[0x15]; + + /// + /// A pointer to the Tech Set this Material uses + /// + public long TechniqueSetPointer { get; set; } + + /// + /// A pointer to this Material's Image table + /// + public long ImageTablePointer { get; set; } + + /// + /// UnknownPointer (Probably settings that changed based off TechSet) + /// + public long UnknownPointer { get; set; } + + /// + /// Null Bytes + /// + public long Padding { get; set; } + + /// + /// Unknown Bytes (Flags, settings, etc.) + /// + public fixed byte UnknownBytes2[0x90]; + } + + /// + /// Vertex Position + /// + public struct GfxVertexPosition + { + /// + /// X Origin + /// + public float X { get; set; } + + /// + /// Y Origin + /// + public float Y { get; set; } + + /// + /// Z Origin + /// + public float Z { get; set; } + } + + /// + /// Vertex Position + /// + public struct GfxVertexUV + { + /// + /// X Origin + /// + public float U { get; set; } + + /// + /// Y Origin + /// + public float V { get; set; } + } + + /// + /// Gfx Static Model + /// + [StructLayout(LayoutKind.Sequential, Pack = 1)] + public unsafe struct GfxStaticModel + { + /// + /// X Origin + /// + public float X { get; set; } + + /// + /// Y Origin + /// + public float Y { get; set; } + + /// + /// Z Origin + /// + public float Z { get; set; } + + /// + /// 3x3 Rotation Matrix + /// + public fixed float Matrix[9]; + + /// + /// Model Scale + /// + public float ModelScale { get; set; } + + /// + /// Unknown Bytes + /// + public fixed byte UnknownBytes2[0x14]; + + /// + /// Pointer to the XModel Asset + /// + public long ModelPointer { get; set; } + + /// + /// Unknown Bytes + /// + public fixed byte UnknownBytes3[0x30]; + } + + /// + /// Reads BSP Data + /// + public static void ExportBSPData(ProcessReader reader, long assetPoolsAddress, long assetSizesAddress, string gameType, Action printCallback = null) + { + // Found her + printCallback?.Invoke("Found supported game: Call of Duty: World War II"); + + // Validate by XModel Name + if (reader.ReadNullTerminatedString(reader.ReadInt64(reader.ReadInt64(reader.GetBaseAddress() + assetPoolsAddress + 0x50) + 8)) == "empty_model") + { + // Load BSP Pools (they only have a size of 1 so we don't care about reading more than 1) + var gfxMapAsset = reader.ReadStruct(reader.ReadInt64(reader.GetBaseAddress() + assetPoolsAddress + 0x138)); + var gfxMapTrZoneAsset = reader.ReadStruct(reader.ReadInt64(reader.GetBaseAddress() + assetPoolsAddress + 0x140) + 8); + + // Name + string gfxMapName = reader.ReadNullTerminatedString(gfxMapAsset.NamePointer); + string mapName = reader.ReadNullTerminatedString(gfxMapAsset.MapNamePointer); + + // Verify a BSP is actually loaded (if in base menu, etc, no map is loaded) + if (String.IsNullOrWhiteSpace(gfxMapName)) + { + printCallback?.Invoke("No BSP loaded. Enter Main Menu or a Map to load in the required assets."); + } + else + { + // New IW Map + var mapFile = new IWMap(); + // Print Info + printCallback?.Invoke(String.Format("Loaded Gfx Map - {0}", gfxMapName)); + printCallback?.Invoke(String.Format("Loaded Map - {0}", mapName)); + printCallback?.Invoke(String.Format("Vertex Count - {0}", gfxMapTrZoneAsset.GfxVertexCount)); + printCallback?.Invoke(String.Format("Indices Count - {0}", gfxMapAsset.GfxIndicesCount)); + printCallback?.Invoke(String.Format("Surface Count - {0}", gfxMapAsset.SurfaceCount)); + printCallback?.Invoke(String.Format("Model Count - {0}", gfxMapAsset.GfxStaticModelsCount)); + + // Build output Folder + string outputName = Path.Combine("exported_maps", "world_war_2", gameType, mapName, mapName); + Directory.CreateDirectory(Path.GetDirectoryName(outputName)); + + // Stop watch + var stopWatch = Stopwatch.StartNew(); + + // Read Vertices + printCallback?.Invoke("Parsing vertex data...."); + var vertices = ReadGfxVertices( + reader, + gfxMapTrZoneAsset.GfxVertexPositionsPointer, + gfxMapTrZoneAsset.GfxVertexUVsPointer, + gfxMapTrZoneAsset.GfxVertexNormalsPointer, + gfxMapTrZoneAsset.GfxVertexCount); + printCallback?.Invoke(String.Format("Parsed vertex data in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + + // Reset timer + stopWatch.Restart(); + + // Read Indices + printCallback?.Invoke("Parsing surface indices...."); + var indices = ReadGfxIndices(reader, gfxMapAsset.GfxIndicesPointer, (int)gfxMapAsset.GfxIndicesCount); + printCallback?.Invoke(String.Format("Parsed indices in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + + // Reset timer + stopWatch.Restart(); + // Read Indices + printCallback?.Invoke("Parsing surfaces...."); + var surfaces = ReadGfxSufaces(reader, gfxMapAsset.GfxSurfacesPointer, gfxMapAsset.SurfaceCount); + printCallback?.Invoke(String.Format("Parsed surfaces in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + + // Reset timer + stopWatch.Restart(); + + // Write OBJ + printCallback?.Invoke("Converting to OBJ...."); + + // Create new OBJ + var obj = new WavefrontOBJ(); + + // Append Vertex Data + foreach (var vertex in vertices) + { + obj.Vertices.Add(vertex.Position); + obj.Normals.Add(vertex.Normal); + obj.UVs.Add(vertex.UV); + } + + // Image Names (for Search String) + HashSet imageNames = new HashSet(); + int x = 0; + // Append Faces + foreach (var surface in surfaces) + { + // Create new Material + var material = ReadMaterial(reader, surface.MaterialPointer); + // Add to images + imageNames.Add(material.DiffuseMap); + // Add it + obj.AddMaterial(material); + // Add points + for (ushort i = 0; i < surface.FaceCount; i++) + { + // Face Indices + var faceIndex1 = indices[i * 3 + surface.FaceIndex] + surface.VertexIndex; + var faceIndex2 = indices[i * 3 + surface.FaceIndex + 1] + surface.VertexIndex; + var faceIndex3 = indices[i * 3 + surface.FaceIndex + 2] + surface.VertexIndex; + + // Validate unique points, and write to OBJ + if (faceIndex1 != faceIndex2 && faceIndex1 != faceIndex3 && faceIndex2 != faceIndex3) + { + // new Obj Face + var objFace = new WavefrontOBJ.Face(material.Name); + + // Add points + objFace.Vertices[0] = new WavefrontOBJ.Face.Vertex(faceIndex1, faceIndex1, faceIndex1); + objFace.Vertices[2] = new WavefrontOBJ.Face.Vertex(faceIndex2, faceIndex2, faceIndex2); + objFace.Vertices[1] = new WavefrontOBJ.Face.Vertex(faceIndex3, faceIndex3, faceIndex3); + + // Add to OBJ + obj.Faces.Add(objFace); + } + } + + x++; + } + + // Save it + obj.Save(outputName + ".obj"); + + // Build search strinmg + string searchString = ""; + + // Loop through images, and append each to the search string (for Wraith/Greyhound) + foreach (string imageName in imageNames) + searchString += String.Format("{0},", Path.GetFileNameWithoutExtension(imageName)); + + // Dump it + File.WriteAllText(outputName + "_search_string.txt", searchString); + + // Read entities and dump to map + mapFile.Entities.AddRange(ReadStaticModels(reader, gfxMapAsset.GfxStaticModelsPointer, (int)gfxMapAsset.GfxStaticModelsCount)); + mapFile.DumpToMap(outputName + ".map"); + + // Done + printCallback?.Invoke(String.Format("Converted to OBJ in {0:0.00} seconds.", stopWatch.ElapsedMilliseconds / 1000.0)); + } + + } + else + { + printCallback?.Invoke("Call of Duty: World War II is supported, but this EXE is not."); + } + } + + /// + /// Reads Gfx Surfaces + /// + public static GfxSurface[] ReadGfxSufaces(ProcessReader reader, long address, int count) + { + // Preallocate array + GfxSurface[] surfaces = new GfxSurface[count]; + + // Loop number of indices we have + for (int i = 0; i < count; i++) + // Add it + surfaces[i] = reader.ReadStruct(address + i * 56); + + // Done + return surfaces; + } + + + /// + /// Reads Gfx Vertex Indices + /// + public static ushort[] ReadGfxIndices(ProcessReader reader, long address, int count) + { + // Preallocate short array + ushort[] indices = new ushort[count]; + // Read buffer + var byteBuffer = reader.ReadBytes(address, count * 2); + // Copy buffer + Buffer.BlockCopy(byteBuffer, 0, indices, 0, byteBuffer.Length); + // Done + return indices; + } + + /// + /// Reads Gfx Vertices + /// + public static Vertex[] ReadGfxVertices(ProcessReader reader, long positionsAddress, long uvsAddress, long normalsAddress, int count) + { + // Preallocate vertex array + Vertex[] vertices = new Vertex[count]; + // Read buffer + var positionsBuffer = reader.ReadBytes(positionsAddress, count * 12); + var uvsBuffer = reader.ReadBytes(uvsAddress, count * 8); + var normalsBuffer = reader.ReadBytes(normalsAddress, count * 4); + // Loop number of vertices we have + for (int i = 0; i < count; i++) + { + // Read Struct + var gfxVertexPosition = ByteUtil.BytesToStruct(positionsBuffer, i * 12); + var gfxVertexUV = ByteUtil.BytesToStruct(uvsBuffer, i * 8); + var gfxVertexNormal = ByteUtil.BytesToStruct(normalsBuffer, i * 4); + + // Create new SEModel Vertex + vertices[i] = new Vertex() + { + // Set offset + Position = new Vector3( + gfxVertexPosition.X * 2.54, + gfxVertexPosition.Y * 2.54, + gfxVertexPosition.Z * 2.54), + // Decode and set normal (from DTZxPorter - Wraith, same as XModels) + Normal = VertexNormalUnpacking.MethodB(gfxVertexNormal), + // Set UV + UV = new Vector2(gfxVertexUV.U, 1 - gfxVertexUV.V) + }; + } + + // Done + return vertices; + } + + /// + /// Reads a material for the given surface and its associated images + /// + public static WavefrontOBJ.Material ReadMaterial(ProcessReader reader, long address) + { + // Read Material + var material = reader.ReadStruct(address); + // Create new OBJ Image + var objMaterial = new WavefrontOBJ.Material(Path.GetFileNameWithoutExtension(reader.ReadNullTerminatedString(reader.ReadInt64(address)).Replace("*", ""))); + // Loop over images + for (byte i = 0; i < material.ImageCount; i++) + { + // Read Material Image + var materialImage = reader.ReadStruct(material.ImageTablePointer + i * Marshal.SizeOf()); + // Check for color map for now + if (materialImage.SemanticHash == 0xA0AB1041) + objMaterial.DiffuseMap = "_images\\\\" + reader.ReadNullTerminatedString(reader.ReadInt64(materialImage.ImagePointer)) + ".png"; + } + // Done + return objMaterial; + } + + /// + /// Reads Static Models + /// + public unsafe static List ReadStaticModels(ProcessReader reader, long address, int count) + { + // Resulting Entities + List entities = new List(count); + // Read buffer + var byteBuffer = reader.ReadBytes(address, count * Marshal.SizeOf()); + // Loop number of models we have + for (int i = 0; i < count; i++) + { + // Read Struct + var staticModel = ByteUtil.BytesToStruct(byteBuffer, i * Marshal.SizeOf()); + // Model Name + var modelName = reader.ReadNullTerminatedString(reader.ReadInt64(staticModel.ModelPointer)); + // New Matrix + var matrix = new Rotation.Matrix(); + // Copy X Values + matrix.Values[0] = staticModel.Matrix[0]; + matrix.Values[1] = staticModel.Matrix[1]; + matrix.Values[2] = staticModel.Matrix[2]; + // Copy Y Values + matrix.Values[4] = staticModel.Matrix[3]; + matrix.Values[5] = staticModel.Matrix[4]; + matrix.Values[6] = staticModel.Matrix[5]; + // Copy Z Values + matrix.Values[8] = staticModel.Matrix[6]; + matrix.Values[9] = staticModel.Matrix[7]; + matrix.Values[10] = staticModel.Matrix[8]; + // Convert to Euler + var euler = matrix.ToEuler(); + // Add it + entities.Add(IWMap.Entity.CreateMiscModel(modelName, new Vector3(staticModel.X, staticModel.Y, staticModel.Z), Rotation.ToDegrees(euler), staticModel.ModelScale)); + } + // Done + return entities; + } + } +} diff --git a/src/Husky/Husky/Husky.csproj b/src/Husky/Husky/Husky.csproj index b25c860..0f543ba 100644 --- a/src/Husky/Husky/Husky.csproj +++ b/src/Husky/Husky/Husky.csproj @@ -6,9 +6,9 @@ Debug AnyCPU {BBEB0978-D656-4432-80E9-CF2083F53349} - Exe + Library Husky - Husky + HuskyLib v4.6.1 512 true @@ -37,7 +37,11 @@ true - icon.ico + + + + + @@ -45,6 +49,8 @@ + + @@ -58,17 +64,19 @@ + + - + - + @@ -84,16 +92,13 @@ PhilLibX - - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - + + \ No newline at end of file diff --git a/src/Husky/Husky/Program.cs b/src/Husky/Husky/HuskyUtil.cs similarity index 70% rename from src/Husky/Husky/Program.cs rename to src/Husky/Husky/HuskyUtil.cs index f83cc37..662ca5e 100644 --- a/src/Husky/Husky/Program.cs +++ b/src/Husky/Husky/HuskyUtil.cs @@ -29,12 +29,12 @@ namespace Husky /// /// Game Definition (AssetDB Address, Sizes Address, Game Type ID (MP, SP, ZM, etc.), Export Method /// - using GameDefinition = Tuple>; + using GameDefinition = Tuple>>; /// /// Main Program Class /// - class Program + public class HuskyUtil { /// /// Game Addresses & Methods (Asset DB and Asset Pool Sizes) (Some are relative due to ASLR) @@ -53,64 +53,33 @@ class Program // Call of Duty: Modern Warfare 3 { "iw5mp", new GameDefinition(0x8AB258, 0x8AAF78, "mp", ModernWarfare3.ExportBSPData) }, { "iw5sp", new GameDefinition(0x92AD20, 0x92AA40, "sp", ModernWarfare3.ExportBSPData) }, + // Call of Duty: Black Ops + { "BlackOps", new GameDefinition(0xB741B8, 0xB73EF8, "sp", BlackOps.ExportBSPData) }, + { "BlackOpsMP", new GameDefinition(0xBF2C30, 0xBF2970, "mp", BlackOps.ExportBSPData) }, // Call of Duty: Black Ops 2 { "t6zm", new GameDefinition(0xD41240, 0xD40E80, "zm", BlackOps2.ExportBSPData) }, { "t6mp", new GameDefinition(0xD4B340, 0xD4AF80, "mp", BlackOps2.ExportBSPData) }, { "t6sp", new GameDefinition(0xBD46B8, 0xBD42F8, "sp", BlackOps2.ExportBSPData) }, - // Call of Duty: Black Ops - { "BlackOps", new GameDefinition(0xB741B8, 0xB73EF8, "sp", BlackOps.ExportBSPData) }, - { "BlackOpsMP", new GameDefinition(0xBF2C30, 0xBF2970, "mp", BlackOps.ExportBSPData) }, // Call of Duty: Ghosts { "iw6mp64_ship", new GameDefinition(0x1409E4F20, 0x1409E4E20, "mp", Ghosts.ExportBSPData) }, { "iw6sp64_ship", new GameDefinition(0x14086DCB0, 0x14086DBB0, "sp", Ghosts.ExportBSPData) }, + // Call of Duty: Infinite Warfare + { "iw7_ship", new GameDefinition(0x1414663D0, 0x141466290, "core", InfiniteWarfare.ExportBSPData) }, // Call of Duty: Advanced Warfare { "s1_mp64_ship", new GameDefinition(0x1409B40D0, 0x1409B4B90, "mp", AdvancedWarfare.ExportBSPData) }, { "s1_sp64_ship", new GameDefinition(0x140804690, 0x140804140, "sp", AdvancedWarfare.ExportBSPData) }, + // Call of Duty: World War II + { "s2_mp64_ship", new GameDefinition(0xC05370, 0xC05370, "mp", WorldWarII.ExportBSPData) }, + { "s2_sp64_ship", new GameDefinition(0x9483F0, 0xBCC5E0, "sp", WorldWarII.ExportBSPData) }, // Call of Duty: Modern Warfare Remastered { "h1_mp64_ship", new GameDefinition(0x10B4460, 0x10B3C80, "mp", ModernWarfareRM.ExportBSPData) }, { "h1_sp64_ship", new GameDefinition(0xEC9FB0, 0xEC97D0, "sp", ModernWarfareRM.ExportBSPData) }, }; - /// - /// Main Method - /// - /// Command Line Args. - static void Main(string[] args) - { - // Set stuffs - Directory.SetCurrentDirectory(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)); - Console.Title = "Husky"; - // Initial Info - Printer.WriteLine("INIT", "────────────────────────────────────────────────────"); - Printer.WriteLine("INIT", "Husky - Call of Duty BSP Exporter"); - Printer.WriteLine("INIT", "By Scobalula"); - Printer.WriteLine("INIT", String.Format("Version: {0}", Assembly.GetExecutingAssembly().GetName().Version)); - Printer.WriteLine("INIT", "────────────────────────────────────────────────────"); - // Information - Printer.WriteLine("INFO", "Currently supports:"); - Printer.WriteLine("INFO", " CoD: WAW"); - Printer.WriteLine("INFO", " CoD: BO1"); - Printer.WriteLine("INFO", " CoD: MW"); - Printer.WriteLine("INFO", " CoD: MW2"); - Printer.WriteLine("INFO", " CoD: MW3"); - Printer.WriteLine("INFO", " CoD: AW"); - Printer.WriteLine("INFO", " CoD: Ghosts"); - Printer.WriteLine("INFO", " CoD: MWR"); - Printer.WriteLine("INFO", "Usage:"); - Printer.WriteLine("INFO", " Run a supported game, then run Husky"); - // Scanning - Printer.WriteLine("INFO", "Looking for a supported game...."); - // Run exporter - LoadGame(); - // Done - Printer.WriteLine("DONE", "Execution complete, press Enter to exit..."); - Console.ReadLine(); - } - /// /// Looks for matching game and loads BSP from it /// - static void LoadGame() + public static void LoadGame(Action printCallback = null) { try { @@ -124,7 +93,7 @@ static void LoadGame() if (Games.TryGetValue(process.ProcessName, out var game)) { // Export it - game.Item4(new ProcessReader(process), game.Item1, game.Item2, game.Item3); + game.Item4(new ProcessReader(process), game.Item1, game.Item2, game.Item3, printCallback); // Done return; @@ -132,12 +101,12 @@ static void LoadGame() } // Failed - Printer.WriteLine("ERROR", "Failed to find a supported game, please ensure one of them is running.", ConsoleColor.DarkRed); + printCallback?.Invoke("Failed to find a supported game, please ensure one of them is running."); } catch(Exception e) { - Printer.WriteException(e, "ERROR", "An unhandled exception has occured:"); - Console.WriteLine(e); + printCallback?.Invoke("An unhandled exception has occured:"); + printCallback?.Invoke(e); } } } diff --git a/src/Husky/Husky/Utility/Color.cs b/src/Husky/Husky/Utility/Color.cs deleted file mode 100644 index 32f3f10..0000000 --- a/src/Husky/Husky/Utility/Color.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Husky -{ - public class Color - { - } -} diff --git a/src/Husky/Husky/Utility/FloatToInt.cs b/src/Husky/Husky/Utility/FloatToInt.cs index c82e0e1..729537e 100644 --- a/src/Husky/Husky/Utility/FloatToInt.cs +++ b/src/Husky/Husky/Utility/FloatToInt.cs @@ -1,12 +1,27 @@ -using System; -using System.Collections.Generic; -using System.Linq; +// ------------------------------------------------------------------------ +// Husky - Call of Duty BSP Extractor +// Copyright (C) 2018 Philip/Scobalula +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ------------------------------------------------------------------------ using System.Runtime.InteropServices; -using System.Text; -using System.Threading.Tasks; namespace Husky { + /// + /// A class that contains both a float and an int in the same bytes, to mimick C++ Unions + /// [StructLayout(LayoutKind.Explicit)] class FloatToInt { diff --git a/src/Husky/Husky/Utility/HalfFloats.cs b/src/Husky/Husky/Utility/HalfFloats.cs new file mode 100644 index 0000000..2e00758 --- /dev/null +++ b/src/Husky/Husky/Utility/HalfFloats.cs @@ -0,0 +1,150 @@ +// ------------------------------------------------------------------------ +// Husky - Call of Duty BSP Extractor +// Copyright (C) 2018 Philip/Scobalula +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ------------------------------------------------------------------------ +using System.Runtime.InteropServices; + +namespace Husky +{ + class HalfFloats + { + /// + /// Shift value + /// + const int Shift = 13; + + /// + /// Sign used to shift + /// + const int ShiftSign = 16; + + /// + /// Infinity of a float + /// + const int FloatInfinity = 0x7F800000; + + /// + /// Maximum value of a half float + /// + const int MaxValue = 0x477FE000; + + /// + /// Minimum value of a half float + /// + const int MinValue = 0x38800000; + + /// + /// Sign bit of a float + /// + const uint SignBit = 0x80000000; + + /// + /// Precalculated properties of a half float + /// + const int InfC = FloatInfinity >> Shift; + const int NaNN = (InfC + 1) << Shift; + const int MaxC = MaxValue >> Shift; + const int MinC = MinValue >> Shift; + const int SignC = (int)(SignBit >> ShiftSign); + + /// + /// Precalculated (1 << 23) / minN + /// + const int MulN = 0x52000000; + + /// + /// Precalculated minN / (1 << (23 - shift)) + /// + const int MulC = 0x33800000; + + /// + /// Max float subnormal down shifted + /// + const int SubC = 0x003FF; + + /// + /// Min float normal down shifted + /// + const int NorC = 0x00400; + + /// + /// Precalculated min and max decimals + /// + const int MaxD = InfC - MaxC - 1; + const int MinD = MinC - SubC - 1; + + /// + /// A struct to hold different data types in 1 set of bytes + /// + [StructLayout(LayoutKind.Explicit)] + public struct FloatBits + { + /// + /// Floating Point Value + /// + [FieldOffset(0)] + public float Float; + + /// + /// Unsigned Integer Value + /// + [FieldOffset(0)] + public uint UnsignedInteger; + + /// + /// Signed Integer Value + /// + [FieldOffset(0)] + public int SignedInteger; + } + + /// + /// Converts a half precision floating point number to a single precision floating point number + /// + /// 16bit Int representation of the float + /// Resulting Float + public static float ToFloat(ushort value) + { + // Define bit values + FloatBits v, s = new FloatBits(); + // Set the initial values (if we don't do this, .NET considers them unassigned) + v.SignedInteger = 0; + v.UnsignedInteger = 0; + v.Float = 0; + s.SignedInteger = 0; + s.UnsignedInteger = 0; + s.Float = 0; + // Assign the initial value given + v.UnsignedInteger = value; + // Calculate sign + int sign = v.SignedInteger & SignC; + v.SignedInteger ^= sign; + sign <<= ShiftSign; + v.SignedInteger ^= ((v.SignedInteger + MinD) ^ v.SignedInteger) & -(v.SignedInteger > SubC ? 1 : 0); + v.SignedInteger ^= ((v.SignedInteger + MaxD) ^ v.SignedInteger) & -(v.SignedInteger > MaxC ? 1 : 0); + // Inverse Subnormals + s.SignedInteger = MulC; + s.Float *= v.SignedInteger; + int mask = -(NorC > v.SignedInteger ? 1 : 0); + v.SignedInteger <<= Shift; + v.SignedInteger ^= (s.SignedInteger ^ v.SignedInteger) & mask; + v.SignedInteger |= sign; + // Return the expanded result + return v.Float; + + } + } +} diff --git a/src/Husky/Husky/Utility/Vectors.cs b/src/Husky/Husky/Utility/Vectors.cs index bf6d584..5226fe0 100644 --- a/src/Husky/Husky/Utility/Vectors.cs +++ b/src/Husky/Husky/Utility/Vectors.cs @@ -1,4 +1,22 @@ -namespace Husky +// ------------------------------------------------------------------------ +// Husky - Call of Duty BSP Extractor +// Copyright (C) 2018 Philip/Scobalula +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ------------------------------------------------------------------------ + +namespace Husky { /// /// A class to hold a 2-D Vector diff --git a/src/Husky/Husky/Utility/Vertex.cs b/src/Husky/Husky/Utility/Vertex.cs index bcd41f5..c40ac33 100644 --- a/src/Husky/Husky/Utility/Vertex.cs +++ b/src/Husky/Husky/Utility/Vertex.cs @@ -1,4 +1,22 @@ -namespace Husky +// ------------------------------------------------------------------------ +// Husky - Call of Duty BSP Extractor +// Copyright (C) 2018 Philip/Scobalula +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ------------------------------------------------------------------------ + +namespace Husky { /// /// Vertex Class (Position, Offset, etc.) @@ -19,10 +37,5 @@ public class Vertex /// Vertex UV/Texture Coordinates /// public Vector2 UV { get; set; } - - /// - /// Vertex Color - /// - public Color Color { get; set; } } } diff --git a/src/Husky/Husky/Utility/VertexNormal.cs b/src/Husky/Husky/Utility/VertexNormal.cs index 29cf5d9..c29518f 100644 --- a/src/Husky/Husky/Utility/VertexNormal.cs +++ b/src/Husky/Husky/Utility/VertexNormal.cs @@ -1,16 +1,34 @@ -namespace Husky +// ------------------------------------------------------------------------ +// Husky - Call of Duty BSP Extractor +// Copyright (C) 2018 Philip/Scobalula +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ------------------------------------------------------------------------ + +namespace Husky { /// /// Vertex Normal Unpacking Methods /// - class VertexNormal + class VertexNormalUnpacking { /// /// Unpacks a Vertex Normal from: WaW, MW2, MW3, Bo1 /// /// Packed 4 byte Vertex Normal /// Resulting Vertex Normal - public static Vector3 UnpackA(PackedUnitVector packedNormal) + public static Vector3 MethodA(PackedUnitVector packedNormal) { // Decode the scale of the vector float decodeScale = ((float)(packedNormal.Byte4 - -192.0) / 32385.0f); @@ -27,7 +45,7 @@ public static Vector3 UnpackA(PackedUnitVector packedNormal) /// /// Packed 4 byte Vertex Normal /// Resulting Vertex Normal - public static Vector3 UnpackB(PackedUnitVector packedNormal) + public static Vector3 MethodB(PackedUnitVector packedNormal) { // Return decoded vector return new Vector3( @@ -41,7 +59,7 @@ public static Vector3 UnpackB(PackedUnitVector packedNormal) /// /// Packed 4 byte Vertex Normal /// Resulting Vertex Normal - public static Vector3 UnpackC(PackedUnitVector packedNormal) + public static Vector3 MethodC(PackedUnitVector packedNormal) { // Resulting values var builtX = new FloatToInt { Integer = (uint)((packedNormal.Value & 0x3FF) - 2 * (packedNormal.Value & 0x200) + 0x40400000) }; diff --git a/src/Husky/Husky/packages.config b/src/Husky/Husky/packages.config index a0a4636..3fe324a 100644 --- a/src/Husky/Husky/packages.config +++ b/src/Husky/Husky/packages.config @@ -1,5 +1,5 @@  - + \ No newline at end of file diff --git a/src/Husky/HuskyUI/AboutWindow.xaml b/src/Husky/HuskyUI/AboutWindow.xaml new file mode 100644 index 0000000..ffe1889 --- /dev/null +++ b/src/Husky/HuskyUI/AboutWindow.xaml @@ -0,0 +1,18 @@ + + + + diff --git a/src/Husky/HuskyUI/MainWindow.xaml.cs b/src/Husky/HuskyUI/MainWindow.xaml.cs new file mode 100644 index 0000000..d4f0472 --- /dev/null +++ b/src/Husky/HuskyUI/MainWindow.xaml.cs @@ -0,0 +1,100 @@ +// ------------------------------------------------------------------------ +// Husky - Call of Duty BSP Extractor +// Copyright (C) 2018 Philip/Scobalula +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// ------------------------------------------------------------------------ +using System; +using System.Windows; +using System.Threading; +using System.Windows.Shell; +using Husky; +using System.Reflection; + +namespace HuskyUI +{ + /// + /// Interaction logic for MainWindow.xaml + /// + public partial class MainWindow : Window + { + /// + /// Whether or not a thread is active + /// + public bool ThreadActive = false; + + /// + /// Initializes MainWindow + /// + public MainWindow() + { + InitializeComponent(); + // Set title + Title = String.Format("Husky - Version {0}", Assembly.GetExecutingAssembly().GetName().Version); + // Initial Print + PrintLine("Load a supported CoD Game, then click the paper plane to export loaded BSP data."); + PrintLine(""); + } + + private void ExportClick(object sender, RoutedEventArgs e) + { + // Check is a thread already active + if (ThreadActive) + return; + // Create new thread and export + new Thread(delegate () + { + UpdateProgressState(TaskbarItemProgressState.Indeterminate); + ThreadActive = true; + HuskyUtil.LoadGame(PrintLine); + PrintLine(""); + ThreadActive = false; + UpdateProgressState(TaskbarItemProgressState.Normal); + }).Start(); + } + + /// + /// Prints to the ConsoleBox + /// + /// Value to print + private void PrintLine(object value) + { + Dispatcher.BeginInvoke(new Action(() => ConsoleBox.AppendText(value.ToString() + Environment.NewLine))); + } + + /// + /// Updates Progress State + /// + /// State to set + private void UpdateProgressState(TaskbarItemProgressState state) + { + Dispatcher.BeginInvoke(new Action(() => TaskBarProgress.ProgressState = state)); + } + + /// + /// Shows the About Window and Dims the Main Window + /// + private void AboutClick(object sender, RoutedEventArgs e) + { + AboutWindow aboutWindow = new AboutWindow() + { + Owner = this + }; + aboutWindow.VersionLabel.Content = String.Format("Version: {0}", Assembly.GetExecutingAssembly().GetName().Version); + DimBox.Visibility = Visibility.Visible; + aboutWindow.ShowDialog(); + DimBox.Visibility = Visibility.Hidden; + } + } +} diff --git a/src/Husky/HuskyUI/Properties/AssemblyInfo.cs b/src/Husky/HuskyUI/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..d685957 --- /dev/null +++ b/src/Husky/HuskyUI/Properties/AssemblyInfo.cs @@ -0,0 +1,53 @@ +using System.Reflection; +using System.Runtime.InteropServices; +using System.Windows; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Husky - Call of Duty BSP Extractor")] +[assembly: AssemblyDescription("Call of Duty BSP Extractor")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("Philip/Scobalula")] +[assembly: AssemblyProduct("HuskyUI")] +[assembly: AssemblyCopyright("Copyright © Scobalula 2018")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +//In order to begin building localizable applications, set +//CultureYouAreCodingWith in your .csproj file +//inside a . For example, if you are using US english +//in your source files, set the to en-US. Then uncomment +//the NeutralResourceLanguage attribute below. Update the "en-US" in +//the line below to match the UICulture setting in the project file. + +//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] + + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] + + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("0.6.0.0")] +[assembly: AssemblyFileVersion("0.6.0.0")] diff --git a/src/Husky/HuskyUI/Properties/Resources.Designer.cs b/src/Husky/HuskyUI/Properties/Resources.Designer.cs new file mode 100644 index 0000000..5ac76eb --- /dev/null +++ b/src/Husky/HuskyUI/Properties/Resources.Designer.cs @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace HuskyUI.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("HuskyUI.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/src/Husky/HuskyUI/Properties/Resources.resx b/src/Husky/HuskyUI/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/src/Husky/HuskyUI/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/src/Husky/HuskyUI/Properties/Settings.Designer.cs b/src/Husky/HuskyUI/Properties/Settings.Designer.cs new file mode 100644 index 0000000..b8cc233 --- /dev/null +++ b/src/Husky/HuskyUI/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace HuskyUI.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.7.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/src/Husky/HuskyUI/Properties/Settings.settings b/src/Husky/HuskyUI/Properties/Settings.settings new file mode 100644 index 0000000..033d7a5 --- /dev/null +++ b/src/Husky/HuskyUI/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Husky/HuskyUI/icon.ico b/src/Husky/HuskyUI/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8ea76fb4c2848bea8c2a7f64ada92e886eb5e73b GIT binary patch literal 108241 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 literal 0 HcmV?d00001 diff --git a/src/Husky/HuskyUI/icon.png b/src/Husky/HuskyUI/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a1d44a438316503f23ee4e10b6a1064aa2b4ca6e GIT binary patch literal 12053 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 + + + + \ No newline at end of file