├── CP77Tools ├── lib │ ├── kraken.dll │ ├── kraken.so │ └── liboodle.dylib ├── Commands │ ├── HashCommand.cs │ ├── OodleCommand.cs │ ├── PackCommand.cs │ ├── ExportCommand.cs │ ├── CR2WCommand.cs │ ├── UnbundleCommand.cs │ ├── DumpCommand.cs │ ├── UncookCommand.cs │ └── RebuildCommand.cs ├── Tasks │ ├── OodleTask.cs │ ├── ExportTask.cs │ ├── HashTask.cs │ ├── RebuildTask.cs │ ├── PackTask.cs │ ├── Cr2wTask.cs │ ├── UncookTask.cs │ ├── UnbundleTask.cs │ └── DumpTask.cs ├── Extensions │ └── CommandLineExtensions.cs ├── CP77Tools.csproj └── Program.cs ├── CP77.MSTests ├── appsettings.json ├── CP77.MSTests.csproj ├── ArchiveTests.cs ├── GameUnitTest.cs ├── FNV1A64Tests.cs └── Cr2wUnitTest.cs ├── CHANGELOG.txt ├── BUILD.md ├── .gitattributes ├── .gitignore ├── LICENSE ├── CP77Tools.sln ├── .github └── workflows │ ├── dotnet-core.yml │ └── nightly.yml └── README.md /CP77Tools/lib/kraken.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WolvenKit/CP77Tools/HEAD/CP77Tools/lib/kraken.dll -------------------------------------------------------------------------------- /CP77Tools/lib/kraken.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WolvenKit/CP77Tools/HEAD/CP77Tools/lib/kraken.so -------------------------------------------------------------------------------- /CP77Tools/lib/liboodle.dylib: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WolvenKit/CP77Tools/HEAD/CP77Tools/lib/liboodle.dylib -------------------------------------------------------------------------------- /CP77.MSTests/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "GameDirectory": "D:\\Program Files\\Steam\\steamapps\\common\\Cyberpunk 2077", 3 | "WriteToFile": true 4 | } -------------------------------------------------------------------------------- /CHANGELOG.txt: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | 5 | v1.1.0.2 6 | Implemented 7 | - automatically copy oodle lib from game folder 8 | 9 | Fixed 10 | - fixed a bug where uncooking would not extract properly 11 | 12 | v1.1 13 | Implemented 14 | - Uncook support for more extensions 15 | - extracting from missing hash list 16 | - deprecate archive command: split to unbundle, uncook 17 | - add export command 18 | 19 | Fixed 20 | - update archivehashes.zip 21 | - fixed vertical flip 22 | - fixed uppercase uncook extensions 23 | - code refactoring 24 | - bugfixes 25 | -------------------------------------------------------------------------------- /BUILD.md: -------------------------------------------------------------------------------- 1 | # Building CP77Tools 2 | 3 | ## Windows 4 | 5 | ### Requirements 6 | 7 | * Ensure that the .NET 5.0 SDK is installed. 8 | 9 | Use `dotnet --list-sdks` to check if it is already installed, if not, you can get it from here: https://dotnet.microsoft.com/download/dotnet/5.0 10 | 11 | * Clone or Download the contents of the repo 12 | 13 | * Open PowerShell within the root folder (where `CP77Tools.sln` is) and run `dotnet build --configuration Release` 14 | 15 | * The compiled project can be found at `CP77Tools\bin\Release\net5.0\` 16 | 17 | ## Linux 18 | 19 | ??? 20 | -------------------------------------------------------------------------------- /CP77Tools/Commands/HashCommand.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using System.CommandLine.Invocation; 3 | using CP77Tools.Tasks; 4 | 5 | namespace CP77Tools.Commands 6 | { 7 | public class HashCommand : Command 8 | { 9 | private static string Name = "hash"; 10 | private static string Description = "Some helper functions related to hashes."; 11 | 12 | public HashCommand() : base(Name, Description) 13 | { 14 | AddOption(new Option(new[] {"--input", "-i"}, "Create FNV1A hash of given string")); 15 | AddOption(new Option(new[] {"--missing", "-m"}, "")); 16 | 17 | Handler = CommandHandler.Create(ConsoleFunctions.HashTask); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /CP77Tools/Commands/OodleCommand.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using System.CommandLine.Invocation; 3 | 4 | namespace CP77Tools.Commands 5 | { 6 | public class OodleCommand : Command 7 | { 8 | private static string Name = "oodle"; 9 | private static string Description = "Some helper functions related to oodle compression."; 10 | 11 | public OodleCommand() : base(Name, Description) 12 | { 13 | AddOption(new Option(new[] {"--path", "-p"}, "")); 14 | AddOption(new Option(new []{"--outpath", "-o"}, "")); 15 | AddOption(new Option(new[] {"--decompress", "-d"}, "")); 16 | 17 | Handler = CommandHandler.Create(Tasks.ConsoleFunctions.OodleTask); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /CP77Tools/Commands/PackCommand.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using System.CommandLine.Invocation; 3 | 4 | namespace CP77Tools.Commands 5 | { 6 | public class PackCommand : Command 7 | { 8 | private static string Name = "pack"; 9 | private static string Description = "Pack a folder of files into an .archive file."; 10 | 11 | public PackCommand() : base(Name, Description) 12 | { 13 | AddOption(new Option(new[] {"--path", "-p"}, "Input path. Must be a folder or a list of folders.")); 14 | AddOption(new Option(new[] {"--outpath", "-o"}, "Output directory to create archive.\nIf not specified, will output twill be in place.")); 15 | 16 | Handler = CommandHandler.Create(Tasks.ConsoleFunctions.PackTask); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /CP77Tools/Commands/ExportCommand.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using System.CommandLine.Invocation; 3 | using WolvenKit.Common.DDS; 4 | 5 | namespace CP77Tools.Commands 6 | { 7 | public class ExportCommand : Command 8 | { 9 | private static string Name = "export"; 10 | private static string Description = "Export a file or a list of files into raw files."; 11 | 12 | public ExportCommand() : base(Name, Description) 13 | { 14 | AddOption(new Option(new[] {"--path", "-p"}, "Input path. Must be a file or a list of files.")); 15 | AddOption(new Option(new[] { "--uext" }, "Uncook extension (tga, bmp, jpg, png, dds). Default is tga.")); 16 | 17 | Handler = CommandHandler.Create(Tasks.ConsoleFunctions.ExportTask); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Declare files to normalize crlf, in case people don't have core.autocrlf set. 2 | *.conf text eol=crlf 3 | *.config text eol=crlf 4 | *.cs text eol=crlf 5 | *.csproj text eol=crlf 6 | *.csv text eol=crlf 7 | *.editorconfig text eol=crlf 8 | *.gitattributes text eol=crlf 9 | *.gitignore text eol=crlf 10 | *.gitmodules text eol=crlf 11 | *.manifest text eol=crlf 12 | *.md text eol=crlf 13 | *.resx text eol=crlf 14 | *.settings text eol=crlf 15 | *.sln text eol=crlf 16 | *.txt text eol=crlf 17 | *.xml text eol=crlf 18 | *.yml text eol=crlf 19 | # Explicitly declare files you do not want to be treated as text 20 | *.bmp binary 21 | *.dll binary 22 | *.exe binary 23 | *.ico binary 24 | *.jpg binary 25 | *.md2 binary 26 | *.png binary 27 | *.rtf binary 28 | *.svg -text binary 29 | *.tga binary 30 | *.xbm binary 31 | *.zip binary 32 | *.lib filter=lfs diff=lfs merge=lfs -text 33 | -------------------------------------------------------------------------------- /CP77Tools/Commands/CR2WCommand.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using System.CommandLine.Invocation; 3 | 4 | namespace CP77Tools.Commands 5 | { 6 | public class CR2WCommand : Command 7 | { 8 | private static string Name = "cr2w"; 9 | private static string Description = "Target a specific cr2w (extracted) file and dumps file information."; 10 | 11 | public CR2WCommand() : base(Name, Description) 12 | { 13 | AddOption(new Option(new[] {"--path", "-p"}, "Input path to a cr2w file.")); 14 | AddOption(new Option(new []{"--outpath", "-o"}, "Output path.")); 15 | AddOption(new Option(new[] {"--all", "-a"}, "Dump all information.")); 16 | AddOption(new Option(new[] {"--chunks", "-c"}, "Dump all class information of file.")); 17 | 18 | Handler = CommandHandler.Create(Tasks.ConsoleFunctions.Cr2wTask); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | 54 | #rider 55 | .idea 56 | CP77Tools.sln.DotSettings.user 57 | 58 | #macOS 59 | .DS_Store 60 | 61 | #obj 62 | *obj/ 63 | *bin/ 64 | *.vs/ 65 | *.suo 66 | *.user 67 | *.pubxml 68 | CP77Tools/Properties/launchSettings.json 69 | CP77Tools/Resources/archivehashes.csv 70 | 71 | #include 72 | !texconv.exe 73 | !liboodle.dylib 74 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 rfuzzo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CP77.MSTests/CP77.MSTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0-windows 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | PreserveNewest 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /CP77Tools/Commands/UnbundleCommand.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using System.CommandLine.Invocation; 3 | using CP77Tools.Tasks; 4 | 5 | namespace CP77Tools.Commands 6 | { 7 | public class UnbundleCommand : Command 8 | { 9 | private static string Name = "unbundle"; 10 | private static string Description = "Target an archive to extract files."; 11 | 12 | public UnbundleCommand() : base(Name, Description) 13 | { 14 | AddOption(new Option(new[] {"--path", "-p"}, "Input path to .archive.")); 15 | AddOption(new Option(new[] {"--outpath", "-o"}, "Output directory to extract files to.")); 16 | AddOption(new Option(new[] {"--pattern", "-w"}, "Use optional search pattern, e.g. *.ink. If both regex and pattern is defined, pattern will be used first")); 17 | AddOption(new Option(new[] {"--regex", "-r"}, "Use optional regex pattern.")); 18 | AddOption(new Option(new[] {"--hash"}, "Extract single file with given hash. If a path is supplied it extracts all hashes from that.")); 19 | 20 | Handler = CommandHandler.Create(ConsoleFunctions.UnbundleTask); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /CP77.MSTests/ArchiveTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Configuration; 4 | using System.IO; 5 | using System.IO.MemoryMappedFiles; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Catel.IoC; 10 | using WolvenKit.Common.Services; 11 | using CP77.CR2W; 12 | using CP77.CR2W.Archive; 13 | using Microsoft.Extensions.Configuration; 14 | using Microsoft.VisualStudio.TestTools.UnitTesting; 15 | using Newtonsoft.Json; 16 | using Newtonsoft.Json.Converters; 17 | using WolvenKit.Common; 18 | 19 | namespace CP77.MSTests 20 | { 21 | [TestClass] 22 | public class ArchiveTests : GameUnitTest 23 | { 24 | [TestMethod] 25 | public void Test_Unbundle() 26 | { 27 | 28 | } 29 | 30 | [TestMethod] 31 | public void Test_Uncook() 32 | { 33 | 34 | } 35 | 36 | 37 | private void test_archive(string extension = null) 38 | { 39 | var resultDir = Path.Combine(Environment.CurrentDirectory, TestResultsDirectory); 40 | Directory.CreateDirectory(resultDir); 41 | 42 | var success = true; 43 | 44 | List archives; 45 | 46 | 47 | 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /CP77Tools/Commands/DumpCommand.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using System.CommandLine.Invocation; 3 | using CP77Tools.Tasks; 4 | 5 | namespace CP77Tools.Commands 6 | { 7 | public class DumpCommand : Command 8 | { 9 | private static string Name = "dump"; 10 | private static string Description = "Target an archive or a directory to dump archive information."; 11 | 12 | public DumpCommand() : base(Name, Description) 13 | { 14 | AddOption(new Option(new[] {"--path", "-p"}, "Input path to .archive or to a directory (runs over all archives in directory).")); 15 | AddOption(new Option(new[] {"--imports", "-i"}, "Dump all imports (all filenames that are referenced by all files in the archive).")); 16 | AddOption(new Option(new[] {"--missinghashes", "-m"}, "List all missing hashes of all input archives.")); 17 | AddOption(new Option(new[] {"--texinfo"}, "Dump all xbm info.")); 18 | AddOption(new Option(new[] {"--classinfo"}, "Dump all class info.")); 19 | AddOption(new Option(new[] {"--dump", "-d"}, "Dump archive information.")); 20 | AddOption(new Option(new[] {"--list", "-l"}, "List contents of archive.")); 21 | 22 | 23 | Handler = CommandHandler.Create(ConsoleFunctions.DumpTask); 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /CP77Tools/Commands/UncookCommand.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using System.CommandLine.Invocation; 3 | using CP77Tools.Tasks; 4 | using WolvenKit.Common.DDS; 5 | 6 | namespace CP77Tools.Commands 7 | { 8 | public class UncookCommand : Command 9 | { 10 | private static string Name = "uncook"; 11 | private static string Description = "Target an archive to uncook files."; 12 | 13 | public UncookCommand() : base(Name, Description) 14 | { 15 | AddOption(new Option(new[] {"--path", "-p"}, "Input path to .archive.")); 16 | AddOption(new Option(new[] {"--outpath", "-o"}, "Output directory to extract files to.")); 17 | AddOption(new Option(new[] {"--pattern", "-w"}, "Use optional search pattern, e.g. *.ink. If both regex and pattern is defined, pattern will be used first")); 18 | AddOption(new Option(new[] {"--regex", "-r"}, "Use optional regex pattern.")); 19 | AddOption(new Option(new[] {"--uext"}, "Uncook extension (tga, bmp, jpg, png, dds). Default is tga.")); 20 | AddOption(new Option(new[] { "--flip", "-f" }, "Flips textures vertically")); 21 | AddOption(new Option(new[] {"--hash"}, "Extract single file with given hash.")); 22 | 23 | Handler = CommandHandler.Create(ConsoleFunctions.UncookTask); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /CP77Tools/Tasks/OodleTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using WolvenKit.Common.Oodle; 5 | 6 | namespace CP77Tools.Tasks 7 | { 8 | 9 | public static partial class ConsoleFunctions 10 | { 11 | 12 | public static int OodleTask(string path, string outpath, bool decompress) 13 | { 14 | if (string.IsNullOrEmpty(path)) 15 | return 0; 16 | 17 | if (string.IsNullOrEmpty(outpath)) { outpath = path; } 18 | 19 | if (decompress) 20 | { 21 | var file = File.ReadAllBytes(path); 22 | using var ms = new MemoryStream(file); 23 | using var br = new BinaryReader(ms); 24 | 25 | var oodleCompression = br.ReadBytes(4); 26 | if (!(oodleCompression.SequenceEqual(new byte[] { 0x4b, 0x41, 0x52, 0x4b }))) 27 | throw new NotImplementedException(); 28 | var size = br.ReadUInt32(); 29 | 30 | var buffer = br.ReadBytes(file.Length - 8); 31 | 32 | byte[] unpacked = new byte[size]; 33 | long unpackedSize = OodleHelper.Decompress(buffer, unpacked); 34 | 35 | using var msout = new MemoryStream(); 36 | using var bw = new BinaryWriter(msout); 37 | bw.Write(unpacked); 38 | 39 | File.WriteAllBytes($"{outpath}.kark", msout.ToArray()); 40 | } 41 | 42 | 43 | return 1; 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /CP77Tools/Commands/RebuildCommand.cs: -------------------------------------------------------------------------------- 1 | using System.CommandLine; 2 | using System.CommandLine.Invocation; 3 | 4 | namespace CP77Tools.Commands 5 | { 6 | public class RebuildCommand : Command 7 | { 8 | private static string Name = "rebuild"; 9 | private static string Description = "Recombine split buffers and textures in a folder."; 10 | 11 | public RebuildCommand() : base(Name, Description) 12 | { 13 | AddOption(new Option(new[] {"--path", "-p"}, 14 | "Input path. Must be a folder or a list of folders.")); 15 | AddOption(new Option(new[] {"--buffers", "-b"}, 16 | "Recombine buffers")); 17 | AddOption(new Option(new[] {"--textures", "-t"}, 18 | "Recombine textures")); 19 | AddOption(new Option(new[] {"--import", "-i"}, 20 | "Optionally import missing raw files into their redengine formats.")); 21 | AddOption(new Option(new[] {"--keep"}, 22 | "Optionally keep existing cr2w files intact and only append the buffer")); 23 | AddOption(new Option(new[] {"--clean"}, 24 | "Optionally remove buffers and textures after successful recombination.")); 25 | AddOption(new Option(new[] {"--unsaferaw"}, 26 | "Optionally add raw assets (dds textures, fbx) as buffers without check.")); 27 | 28 | Handler = CommandHandler.Create(Tasks.ConsoleFunctions.RebuildTask); 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /CP77Tools.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30709.64 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CP77Tools", "CP77Tools\CP77Tools.csproj", "{37C3288C-A6D7-4C71-8174-0F2F6D47938E}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CP77.MSTests", "CP77.MSTests\CP77.MSTests.csproj", "{78302151-8D4B-407B-A2FE-5774C471F8AA}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {37C3288C-A6D7-4C71-8174-0F2F6D47938E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {37C3288C-A6D7-4C71-8174-0F2F6D47938E}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {37C3288C-A6D7-4C71-8174-0F2F6D47938E}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {37C3288C-A6D7-4C71-8174-0F2F6D47938E}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {78302151-8D4B-407B-A2FE-5774C471F8AA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {78302151-8D4B-407B-A2FE-5774C471F8AA}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {78302151-8D4B-407B-A2FE-5774C471F8AA}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {78302151-8D4B-407B-A2FE-5774C471F8AA}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {D17FF89E-56D7-4FBF-9C1D-90FA8B7813DD} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /CP77Tools/Tasks/ExportTask.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using CP77.CR2W; 4 | using WolvenKit.Common.DDS; 5 | using WolvenKit.Common.Services; 6 | 7 | namespace CP77Tools.Tasks 8 | { 9 | public static partial class ConsoleFunctions 10 | { 11 | public static void ExportTask(string[] path, EUncookExtension uncookext) 12 | { 13 | if (path == null || path.Length < 1) 14 | { 15 | logger.LogString("Please fill in an input path", Logtype.Error); 16 | return; 17 | } 18 | 19 | Parallel.ForEach(path, file => 20 | { 21 | ExportTaskInner(file, uncookext); 22 | }); 23 | 24 | } 25 | 26 | private static int ExportTaskInner(string path, EUncookExtension uncookext) 27 | { 28 | 29 | #region checks 30 | 31 | if (string.IsNullOrEmpty(path)) 32 | { 33 | logger.LogString("Please fill in an input path.", Logtype.Error); 34 | return 0; 35 | } 36 | var inputFileInfo = new FileInfo(path); 37 | if (!inputFileInfo.Exists) 38 | { 39 | logger.LogString("Input file does not exist.", Logtype.Error); 40 | return 0; 41 | } 42 | 43 | #endregion 44 | 45 | if (ModTools.Export(new FileInfo(path), uncookext) == 1) 46 | { 47 | logger.LogString($"Successfully exported {path}.", Logtype.Success); 48 | } 49 | else 50 | { 51 | logger.LogString($"Failed to export {path}.", Logtype.Error); 52 | } 53 | 54 | return 1; 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /CP77Tools/Tasks/HashTask.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Catel.IoC; 6 | using WolvenKit.Common.FNV1A; 7 | using WolvenKit.Common.Services; 8 | 9 | namespace CP77Tools.Tasks 10 | { 11 | 12 | public static partial class ConsoleFunctions 13 | { 14 | 15 | public static int HashTask(string[] input, bool missing) 16 | { 17 | #region checks 18 | 19 | foreach (var s in input) 20 | { 21 | if (!string.IsNullOrEmpty(s)) 22 | logger.LogString(FNV1A64HashAlgorithm.HashString(s).ToString(), Logtype.Normal); 23 | } 24 | 25 | #endregion 26 | 27 | if (missing) 28 | { 29 | var missingh = File.ReadAllLines(@"C:\Gog\Cyberpunk 2077\archive\pc\content\missinghashes.txt"); 30 | var lines = File.ReadAllLines(@"X:\cp77\langs-work.txt"); 31 | var Hashdict = new Dictionary(); 32 | var bad = new Dictionary(); 33 | 34 | 35 | foreach (var line in lines) 36 | { 37 | var hash = FNV1A64HashAlgorithm.HashString(line); 38 | 39 | if (missingh.Contains(hash.ToString())) 40 | { 41 | if (!Hashdict.ContainsKey(hash)) 42 | Hashdict.Add(hash, line); 43 | } 44 | else 45 | { 46 | if (!bad.ContainsKey(hash)) 47 | bad.Add(hash, line); 48 | } 49 | 50 | } 51 | 52 | 53 | 54 | } 55 | 56 | 57 | return 1; 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /.github/workflows/dotnet-core.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: windows-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup .NET Core 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: 5.0.100 20 | - name: Clean 21 | run: dotnet clean ./CP77Tools.sln --configuration Release && dotnet nuget locals all --clear 22 | - name: Install dependencies 23 | run: dotnet restore 24 | - name: Build 25 | run: dotnet build --configuration Release --no-restore 26 | - name: Upload a Build Artifact 27 | uses: actions/upload-artifact@v2.2.1 28 | with: 29 | name: artifact 30 | path: ./CP77Tools/bin/Release/net5.0-windows 31 | - uses: papeloto/action-zip@v1 32 | with: 33 | files: ./CP77Tools/bin/Release/net5.0-windows 34 | dest: release.zip 35 | - name: Create Release 36 | id: create_release 37 | uses: actions/create-release@v1 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | with: 41 | tag_name: commit-${{ github.sha }} 42 | draft: true 43 | - name: Upload Release Asset 44 | id: upload-release-asset 45 | uses: actions/upload-release-asset@v1 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | with: 49 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 50 | asset_path: ./release.zip 51 | asset_name: release.zip 52 | asset_content_type: application/zip 53 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | name: Nightly 2 | on: 3 | push: 4 | branches: [ nightly ] 5 | pull_request: 6 | branches: [ nightly ] 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: windows-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup .NET Core 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: 5.0.100 20 | - name: Clean 21 | run: dotnet clean ./CP77Tools.sln --configuration Release && dotnet nuget locals all --clear 22 | - name: Install dependencies 23 | run: dotnet restore 24 | - name: Build 25 | run: dotnet build --configuration Release --no-restore 26 | - name: Upload a Build Artifact 27 | uses: actions/upload-artifact@v2.2.1 28 | with: 29 | name: artifact 30 | path: ./CP77Tools/bin/Release/net5.0 31 | - uses: papeloto/action-zip@v1 32 | with: 33 | files: ./CP77Tools/bin/Release/net5.0 34 | dest: nightly.zip 35 | - name: Create Release 36 | id: create_release 37 | uses: actions/create-release@v1 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | with: 41 | tag_name: commit-${{ github.sha }} 42 | draft: true 43 | prerelease: true 44 | - name: Upload Nightly Asset 45 | id: upload-release-asset 46 | uses: actions/upload-release-asset@v1 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | with: 50 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 51 | asset_path: ./nightly.zip 52 | asset_name: nightly.zip 53 | asset_content_type: application/zip 54 | -------------------------------------------------------------------------------- /CP77Tools/Extensions/CommandLineExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace CP77Tools.Extensions 6 | { 7 | public class CommandLineExtensions 8 | { 9 | public static IEnumerable ParseText(String line, Char delimiter, Char textQualifier) 10 | { 11 | 12 | if (line == null) 13 | yield break; 14 | 15 | else 16 | { 17 | Char prevChar = '\0'; 18 | Char nextChar = '\0'; 19 | Char currentChar = '\0'; 20 | 21 | Boolean inString = false; 22 | 23 | StringBuilder token = new StringBuilder(); 24 | 25 | for (int i = 0; i < line.Length; i++) 26 | { 27 | currentChar = line[i]; 28 | 29 | if (i > 0) 30 | prevChar = line[i - 1]; 31 | else 32 | prevChar = '\0'; 33 | 34 | if (i + 1 < line.Length) 35 | nextChar = line[i + 1]; 36 | else 37 | nextChar = '\0'; 38 | 39 | if (currentChar == textQualifier && prevChar != 0x5c && !inString) 40 | { 41 | inString = true; 42 | continue; 43 | } 44 | 45 | if (currentChar == textQualifier && inString) 46 | { 47 | inString = false; 48 | continue; 49 | } 50 | 51 | if (currentChar == delimiter && !inString) 52 | { 53 | yield return token.ToString(); 54 | token = token.Remove(0, token.Length); 55 | continue; 56 | } 57 | 58 | token = token.Append(currentChar); 59 | 60 | } 61 | 62 | yield return token.ToString(); 63 | 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /CP77Tools/CP77Tools.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net5.0-windows 6 | 1.1.0.2 7 | 1.1.0.2 8 | true 9 | false 10 | win-x64 11 | true 12 | true 13 | embedded 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | Always 24 | 25 | 26 | 27 | Always 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | Always 46 | 47 | 48 | PreserveNewest 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /CP77Tools/Tasks/RebuildTask.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using CP77.CR2W; 4 | using WolvenKit.Common.Services; 5 | 6 | namespace CP77Tools.Tasks 7 | { 8 | 9 | public static partial class ConsoleFunctions 10 | { 11 | /// 12 | /// Recombine split buffers and textures in a folder. 13 | /// 14 | /// 15 | /// 16 | public static void RebuildTask(string[] path, 17 | bool buffers, 18 | bool textures, 19 | bool import, 20 | bool keep, 21 | bool clean, 22 | bool unsaferaw 23 | ) 24 | { 25 | if (path == null || path.Length < 1) 26 | { 27 | logger.LogString("Please fill in an input path", Logtype.Error); 28 | return; 29 | } 30 | 31 | Parallel.ForEach(path, p => 32 | { 33 | RebuildTaskInner(p, buffers, textures, import, keep, clean, unsaferaw); 34 | }); 35 | 36 | } 37 | 38 | private static void RebuildTaskInner(string path, 39 | bool buffers, 40 | bool textures, 41 | bool import, 42 | bool keep, 43 | bool clean, 44 | bool unsaferaw 45 | ) 46 | { 47 | #region checks 48 | 49 | if (string.IsNullOrEmpty(path)) 50 | { 51 | logger.LogString("Please fill in an input path", Logtype.Error); 52 | return; 53 | } 54 | 55 | var inputDirInfo = new DirectoryInfo(path); 56 | if (!Directory.Exists(path) || !inputDirInfo.Exists) 57 | { 58 | logger.LogString("Input path does not exist", Logtype.Error); 59 | return; 60 | } 61 | 62 | var basedir = inputDirInfo; 63 | if (basedir?.Parent == null) return; 64 | 65 | #endregion 66 | 67 | 68 | ModTools.Recombine(basedir, buffers, textures, import, keep, clean, unsaferaw); 69 | 70 | return; 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /CP77Tools/Tasks/PackTask.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using CP77.CR2W; 4 | using WolvenKit.Common.Services; 5 | 6 | namespace CP77Tools.Tasks 7 | { 8 | 9 | public static partial class ConsoleFunctions 10 | { 11 | /// 12 | /// Packs a folder or list of folders to .archive files. 13 | /// 14 | /// 15 | /// 16 | public static void PackTask(string[] path, string outpath) 17 | { 18 | if (path == null || path.Length < 1) 19 | { 20 | logger.LogString("Please fill in an input path", Logtype.Error); 21 | return; 22 | } 23 | 24 | Parallel.ForEach(path, file => 25 | { 26 | PackTaskInner(file, outpath); 27 | }); 28 | 29 | } 30 | 31 | private static void PackTaskInner(string path, string outpath, int cp = 0) 32 | { 33 | #region checks 34 | 35 | if (string.IsNullOrEmpty(path)) 36 | { 37 | logger.LogString("Please fill in an input path", Logtype.Error); 38 | return; 39 | } 40 | 41 | var inputDirInfo = new DirectoryInfo(path); 42 | if (!Directory.Exists(path) || !inputDirInfo.Exists) 43 | { 44 | logger.LogString("Input path does not exist", Logtype.Error); 45 | return; 46 | } 47 | 48 | var basedir = inputDirInfo; 49 | if (basedir?.Parent == null) return; 50 | 51 | DirectoryInfo outDir; 52 | if (string.IsNullOrEmpty(outpath)) 53 | { 54 | outDir = basedir.Parent; 55 | } 56 | else 57 | { 58 | outDir = new DirectoryInfo(outpath); 59 | if (!outDir.Exists) 60 | outDir = Directory.CreateDirectory(outpath); 61 | } 62 | 63 | #endregion 64 | 65 | var ar = ModTools.Pack(basedir, outDir); 66 | if (ar != null) 67 | logger.LogString($"Finished packing {ar.ArchiveAbsolutePath}.", Logtype.Success); 68 | else 69 | logger.LogString($"Packing failed.", Logtype.Error); 70 | 71 | return; 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /CP77.MSTests/GameUnitTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Configuration; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | using Catel.IoC; 9 | using CP77.CR2W.Archive; 10 | using Microsoft.Extensions.Configuration; 11 | using Microsoft.VisualStudio.TestTools.UnitTesting; 12 | using WolvenKit.Common.Services; 13 | 14 | namespace CP77.MSTests 15 | { 16 | [TestClass] 17 | public class GameUnitTest 18 | { 19 | private const string GameDirectorySetting = "GameDirectory"; 20 | private const string WriteToFileSetting = "WriteToFile"; 21 | 22 | private static ArchiveManager bm; 23 | internal static Dictionary> GroupedFiles; 24 | 25 | private static IConfigurationRoot _config; 26 | 27 | internal const string TestResultsDirectory = "_CR2WTestResults"; 28 | private static string _gameDirectoryPath; 29 | internal static bool WriteToFile; 30 | 31 | protected static void Setup(TestContext context) 32 | { 33 | Console.WriteLine("BaseTestClass.BaseTestInitialize()"); 34 | 35 | _config = new ConfigurationBuilder() 36 | .AddJsonFile("appsettings.json") 37 | .Build(); 38 | 39 | // check for CP77_DIR environment variable first 40 | // overrides hardcoded appsettings.json 41 | var CP77_DIR = System.Environment.GetEnvironmentVariable("CP77_DIR", EnvironmentVariableTarget.User); 42 | if (!string.IsNullOrEmpty(CP77_DIR) && new DirectoryInfo(CP77_DIR).Exists) 43 | _gameDirectoryPath = CP77_DIR; 44 | else 45 | _gameDirectoryPath = _config.GetSection(GameDirectorySetting).Value; 46 | if (string.IsNullOrEmpty(_gameDirectoryPath)) 47 | throw new ConfigurationErrorsException($"'{GameDirectorySetting}' is not configured"); 48 | var gameDirectory = new DirectoryInfo(_gameDirectoryPath); 49 | if (!gameDirectory.Exists) 50 | throw new ConfigurationErrorsException($"'{GameDirectorySetting}' is not a valid directory"); 51 | 52 | WriteToFile = Boolean.Parse(_config.GetSection(WriteToFileSetting).Value); 53 | 54 | ServiceLocator.Default.RegisterType(); 55 | ServiceLocator.Default.RegisterType(); 56 | 57 | var hashService = ServiceLocator.Default.ResolveType(); 58 | hashService.ReloadLocally(); 59 | 60 | DirectoryInfo gameArchiveDir; 61 | try 62 | { 63 | gameArchiveDir = new DirectoryInfo(Path.Combine(gameDirectory.FullName, "archive", "pc", "content")); 64 | } 65 | catch (Exception e) 66 | { 67 | Console.WriteLine(e); 68 | throw; 69 | } 70 | 71 | bm = new ArchiveManager(gameArchiveDir); 72 | GroupedFiles = bm.GroupedFiles; 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CP77Tools 2 | 3 | [![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](https://lbesson.mit-license.org/) 4 | [![Discord](https://img.shields.io/discord/717692382849663036.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/Epkq79kd96) 5 | [![CodeFactor](https://www.codefactor.io/repository/github/wolvenkit/cp77tools/badge)](https://www.codefactor.io/repository/github/wolvenkit/cp77tools) 6 | ![GitHub all releases](https://img.shields.io/github/downloads/rfuzzo/cp77tools/total) 7 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/rfuzzo/cp77tools)](https://github.com/WolvenKit/CP77Tools/releases) 8 | [![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/rfuzzo/cp77tools?include_prereleases)](https://github.com/WolvenKit/CP77Tools/releases) 9 | 10 | 11 | ------------- 12 | 13 | # ❗ This repository is archived. 14 | 15 | # ❗ It will be maintained here: https://github.com/WolvenKit/Wolvenkit 16 | 17 | 18 | ------------- 19 | 20 | Modding tools for the CDPR Cyberpunk 2077 video game. 21 | 22 | ## Latest Stable Release 23 | [![GitHub release (latest by date)](https://img.shields.io/github/v/release/rfuzzo/cp77tools)](https://github.com/WolvenKit/CP77Tools/releases) https://github.com/WolvenKit/CP77Tools/releases 24 | 25 | 26 | ## Latest Beta Release 27 | [![GitHub release (latest by date including pre-releases)](https://img.shields.io/github/v/release/rfuzzo/cp77tools?include_prereleases)](https://github.com/WolvenKit/CP77Tools/releases) https://github.com/WolvenKit/CP77Tools/releases 28 | 29 | ------------- 30 | 31 | ❗ **requires NET5.0.** (if you don't have it: https://dotnet.microsoft.com/download/dotnet/5.0) 32 | 33 | ❗ **The cp77 tools require oo2ext_7_win64.dll to work.** Copy and paste the dll found here `Cyberpunk 2077\bin\x64\oo2ext_7_win64.dll` into the cp77Tools folder. 34 | 35 | If you are building from source, the dll needs to be in the same folder as the build .exe, e.g. 36 | C:\cpmod\CP77Tools\CP77Tools\bin\Debug\net5.0\oo2ext_7_win64.dll 37 | 38 | ------------- 39 | 40 | ❓ **Check the wiki for guides and how to use this tool:** https://github.com/WolvenKit/CP77Tools/wiki 41 | 42 | 43 | ## Usage: 44 | 45 | ❗ **Surround paths with quotation marks** (if the path contains spaces: e.g. `archive -e -p "C:\Cyberpunk 2077\modding"`) 46 | 47 | * displays the general help: list all commands 48 | `--help` 49 | 50 | * displays help for a specific command, e.g. 51 | `archive -h` 52 | 53 | ### Main functions 54 | * extract all files from archive 55 | `unbundle -p "PATH TO ARCHIVE"` 56 | 57 | * extract all textures from archive (supports conversion to tga, bmp, jpg, png, dds) 58 | `uncook --uext png -p "PATH TO ARCHIVE"` 59 | 60 | * rebuild textures and buffers (this step is needed when packing textures!, see the wiki for how to use this command) 61 | `rebuild -p "PATH TO FOLDER" -b -t --keep --unsaferaw` 62 | 63 | * pack a folder into an .archive (see the wiki for how to use this command) 64 | `pack -p "PATH TO FOLDER"` 65 | 66 | ### Debug Options 67 | * dumps property info from extracted cr2w file 68 | `cr2w -c -p ""` 69 | -------------------------------------------------------------------------------- /CP77Tools/Tasks/Cr2wTask.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Threading.Tasks; 3 | using Newtonsoft.Json; 4 | using CP77.CR2W; 5 | using WolvenKit.Common.Services; 6 | 7 | namespace CP77Tools.Tasks 8 | { 9 | public static partial class ConsoleFunctions 10 | { 11 | public static void Cr2wTask(string[] path, string outpath, bool all, bool chunks) 12 | { 13 | if (path == null || path.Length < 1) 14 | { 15 | logger.LogString("Please fill in an input path", Logtype.Error); 16 | return; 17 | } 18 | 19 | Parallel.ForEach(path, file => 20 | { 21 | Cr2wTaskInner(file, outpath, all, chunks); 22 | }); 23 | 24 | } 25 | 26 | private static int Cr2wTaskInner(string path, string outpath, bool all, bool chunks) 27 | { 28 | // initial checks 29 | if (string.IsNullOrEmpty(path)) 30 | { 31 | logger.LogString("Please fill in an input path.", Logtype.Error); 32 | return 0; 33 | } 34 | var inputFileInfo = new FileInfo(path); 35 | if (!inputFileInfo.Exists) 36 | { 37 | logger.LogString("Input file does not exist.", Logtype.Error); 38 | return 0; 39 | } 40 | 41 | var outputDirInfo = string.IsNullOrEmpty(outpath) 42 | ? inputFileInfo.Directory 43 | : new DirectoryInfo(outpath); 44 | if (outputDirInfo == null || !outputDirInfo.Exists) 45 | { 46 | logger.LogString("Output directory is not valid.", Logtype.Error); 47 | return 0; 48 | } 49 | 50 | var f = File.ReadAllBytes(inputFileInfo.FullName); 51 | using var ms = new MemoryStream(f); 52 | using var br = new BinaryReader(ms); 53 | 54 | var cr2w = new CR2WFile(); 55 | 56 | if (all) 57 | { 58 | cr2w.ReadImportsAndBuffers(br); 59 | 60 | var obj = new Cr2wChunkInfo { Filename = inputFileInfo.FullName }; 61 | 62 | obj.Stringdict = cr2w.StringDictionary; 63 | obj.Imports = cr2w.Imports; 64 | obj.Buffers = cr2w.Buffers; 65 | obj.Chunks = cr2w.Chunks; 66 | foreach (var chunk in cr2w.Chunks) 67 | { 68 | obj.ChunkData.Add(chunk.GetDumpObject(br)); 69 | } 70 | 71 | //write 72 | File.WriteAllText(Path.Combine(outputDirInfo.FullName, $"{inputFileInfo.Name}.info.json"), 73 | JsonConvert.SerializeObject(obj, Formatting.Indented, new JsonSerializerSettings() 74 | { 75 | ReferenceLoopHandling = ReferenceLoopHandling.Ignore, 76 | PreserveReferencesHandling = PreserveReferencesHandling.None, 77 | TypeNameHandling = TypeNameHandling.Auto 78 | })); 79 | logger.LogString($"Finished. Dump file written to {Path.Combine(outputDirInfo.FullName, $"{inputFileInfo.Name}.info.json")}", Logtype.Success); 80 | } 81 | 82 | if (chunks) 83 | { 84 | br.BaseStream.Seek(0, SeekOrigin.Begin); 85 | cr2w.Read(br); 86 | 87 | //write 88 | File.WriteAllText(Path.Combine(outputDirInfo.FullName, $"{inputFileInfo.Name}.json"), 89 | JsonConvert.SerializeObject(cr2w, Formatting.Indented, new JsonSerializerSettings() 90 | { 91 | ReferenceLoopHandling = ReferenceLoopHandling.Ignore, 92 | PreserveReferencesHandling = PreserveReferencesHandling.None, 93 | TypeNameHandling = TypeNameHandling.None 94 | })); 95 | logger.LogString($"Finished. Dump file written to {Path.Combine(outputDirInfo.FullName, $"{inputFileInfo.Name}.json")}", Logtype.Success); 96 | } 97 | 98 | 99 | 100 | 101 | return 1; 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /CP77Tools/Tasks/UncookTask.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using CP77.CR2W.Archive; 6 | using CP77.CR2W; 7 | using WolvenKit.Common.DDS; 8 | using WolvenKit.Common.Services; 9 | 10 | namespace CP77Tools.Tasks 11 | { 12 | public static partial class ConsoleFunctions 13 | { 14 | 15 | public static void UncookTask(string[] path, string outpath, 16 | EUncookExtension uext, bool flip, ulong hash, string pattern, string regex) 17 | { 18 | if (path == null || path.Length < 1) 19 | { 20 | logger.LogString("Please fill in an input path", Logtype.Error); 21 | return; 22 | } 23 | 24 | Parallel.ForEach(path, file => 25 | { 26 | UncookTaskInner(file, outpath, uext, flip, hash, pattern, regex); 27 | }); 28 | 29 | } 30 | 31 | 32 | private static void UncookTaskInner(string path, string outpath, 33 | EUncookExtension uext, bool flip, ulong hash, string pattern, string regex) 34 | { 35 | #region checks 36 | 37 | if (string.IsNullOrEmpty(path)) 38 | { 39 | logger.LogString("Please fill in an input path", Logtype.Error); 40 | return; 41 | } 42 | 43 | var inputFileInfo = new FileInfo(path); 44 | var inputDirInfo = new DirectoryInfo(path); 45 | 46 | 47 | if (!inputFileInfo.Exists && !inputDirInfo.Exists) 48 | { 49 | logger.LogString("Input path does not exist", Logtype.Error); 50 | return; 51 | } 52 | 53 | if (inputFileInfo.Exists && inputFileInfo.Extension != ".archive") 54 | { 55 | logger.LogString("Input file is not an .archive", Logtype.Error); 56 | return; 57 | } 58 | else if (inputDirInfo.Exists && inputDirInfo.GetFiles().All(_ => _.Extension != ".archive")) 59 | { 60 | logger.LogString("No .archive file to process in the input directory", Logtype.Error); 61 | return; 62 | } 63 | 64 | var isDirectory = !inputFileInfo.Exists; 65 | var basedir = inputFileInfo.Exists ? new FileInfo(path).Directory : inputDirInfo; 66 | 67 | #endregion 68 | 69 | List archiveFileInfos; 70 | if (isDirectory) 71 | { 72 | var archiveManager = new ArchiveManager(basedir); 73 | // TODO: use the manager here? 74 | archiveFileInfos = archiveManager.Archives.Select(_ => new FileInfo(_.Value.ArchiveAbsolutePath)).ToList(); 75 | } 76 | else 77 | { 78 | archiveFileInfos = new List {inputFileInfo}; 79 | } 80 | 81 | 82 | foreach (var processedarchive in archiveFileInfos) 83 | { 84 | // get outdirectory 85 | DirectoryInfo outDir; 86 | if (string.IsNullOrEmpty(outpath)) 87 | { 88 | outDir = Directory.CreateDirectory(Path.Combine( 89 | basedir.FullName, 90 | processedarchive.Name.Replace(".archive", ""))); 91 | } 92 | else 93 | { 94 | outDir = new DirectoryInfo(outpath); 95 | if (!outDir.Exists) 96 | { 97 | outDir = Directory.CreateDirectory(outpath); 98 | } 99 | 100 | if (inputDirInfo.Exists) 101 | { 102 | outDir = Directory.CreateDirectory(Path.Combine( 103 | outDir.FullName, 104 | processedarchive.Name.Replace(".archive", ""))); 105 | } 106 | } 107 | 108 | // read archive 109 | var ar = new Archive(processedarchive.FullName); 110 | 111 | // run 112 | if (hash != 0) 113 | { 114 | ar.UncookSingle(hash, outDir, uext, flip); 115 | logger.LogString($" {ar.ArchiveAbsolutePath}: Uncooked one file: {hash}", Logtype.Success); 116 | } 117 | else 118 | { 119 | var r = ar.UncookAll(outDir, pattern, regex, uext, flip); 120 | logger.LogString($" {ar.ArchiveAbsolutePath}: Uncooked {r.Item1.Count}/{r.Item2} files.", 121 | Logtype.Success); 122 | } 123 | } 124 | 125 | return; 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /CP77.MSTests/FNV1A64Tests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using WolvenKit.Common.FNV1A; 5 | 6 | namespace CP77.MSTests 7 | { 8 | [TestClass] 9 | public class FNV1A64Tests 10 | { 11 | [TestMethod] 12 | [DataRow("", 0xcbf29ce484222325UL)] 13 | [DataRow("a", 0xaf63dc4c8601ec8cUL)] 14 | [DataRow("foobar", 0x85944171f73967e8UL)] 15 | [DataRow("\0", 0xaf63bd4c8601b7dfUL)] 16 | [DataRow("a\0", 0x089be207b544f1e4UL)] 17 | [DataRow("foobar\0", 0x34531ca7168b8f38UL)] 18 | [DataRow("hi", 0x08ba5f07b55ec3daUL)] 19 | [DataRow("hi\0", 0x337354193006cb6eUL)] 20 | [DataRow("hello", 0xa430d84680aabd0bUL)] 21 | [DataRow("hello\0", 0xa9bc8acca21f39b1UL)] 22 | [DataRow("127.0.0.1", 0xaabafe7104d914beUL)] 23 | [DataRow("127.0.0.1\0", 0xf4d3180b3cde3edaUL)] 24 | [DataRow("127.0.0.2", 0xaabafd7104d9130bUL)] 25 | [DataRow("127.0.0.2\0", 0xf4cfb20b3cdb5bb1UL)] 26 | [DataRow("127.0.0.3", 0xaabafc7104d91158UL)] 27 | [DataRow("127.0.0.3\0", 0xf4cc4c0b3cd87888UL)] 28 | [DataRow("feedfacedeadbeef", 0xcac54572bb1a6fc8UL)] 29 | [DataRow("feedfacedeadbeef\0", 0xa7a4c9f3edebf0d8UL)] 30 | [DataRow("line 1\nline 2\nline 3", 0x7829851fac17b143UL)] 31 | public void TestFNV1a64(string test, ulong result) 32 | { 33 | // Assert.AreEqual(FNV1A64HashAlgorithm.HashString(test), result); 34 | Assert.AreEqual(FNV1A64HashAlgorithm.HashString(test), result); 35 | } 36 | 37 | [TestMethod] 38 | [DataRow("", 0xaf63bd4c8601b7dfUL)] 39 | [DataRow("a", 0x089be207b544f1e4UL)] 40 | [DataRow("hi", 0x337354193006cb6eUL)] 41 | [DataRow("hello", 0xa9bc8acca21f39b1UL)] 42 | [DataRow("foobar", 0x34531ca7168b8f38UL)] 43 | [DataRow("feedfacedeadbeef", 0xa7a4c9f3edebf0d8UL)] 44 | public void TestFNV1a64_NullEnded(string test, ulong result) 45 | { 46 | Assert.AreEqual(FNV1A64HashAlgorithm.HashString(test, Encoding.ASCII, true), result); 47 | } 48 | 49 | [TestMethod] 50 | [DataRow(new byte[] {0xff, 0x00, 0x00, 0x01}, 0x6961196491cc682dUL)] 51 | [DataRow(new byte[] {0x01, 0x00, 0x00, 0xff}, 0xad2bb1774799dfe9UL)] 52 | [DataRow(new byte[] {0xff, 0x00, 0x00, 0x02}, 0x6961166491cc6314UL)] 53 | [DataRow(new byte[] {0x02, 0x00, 0x00, 0xff}, 0x8d1bb3904a3b1236UL)] 54 | [DataRow(new byte[] {0xff, 0x00, 0x00, 0x03}, 0x6961176491cc64c7UL)] 55 | [DataRow(new byte[] {0x03, 0x00, 0x00, 0xff}, 0xed205d87f40434c7UL)] 56 | [DataRow(new byte[] {0xff, 0x00, 0x00, 0x04}, 0x6961146491cc5faeUL)] 57 | [DataRow(new byte[] {0x04, 0x00, 0x00, 0xff}, 0xcd3baf5e44f8ad9cUL)] 58 | [DataRow(new byte[] {0x40, 0x51, 0x4e, 0x44}, 0xe3b36596127cd6d8UL)] 59 | [DataRow(new byte[] {0x44, 0x4e, 0x51, 0x40}, 0xf77f1072c8e8a646UL)] 60 | [DataRow(new byte[] {0x40, 0x51, 0x4e, 0x4a}, 0xe3b36396127cd372UL)] 61 | [DataRow(new byte[] {0x4a, 0x4e, 0x51, 0x40}, 0x6067dce9932ad458UL)] 62 | [DataRow(new byte[] {0x40, 0x51, 0x4e, 0x54}, 0xe3b37596127cf208UL)] 63 | [DataRow(new byte[] {0x54, 0x4e, 0x51, 0x40}, 0x4b7b10fa9fe83936UL)] 64 | public void TestFNV1a64_ByteSpan(byte[] test, ulong result) 65 | { 66 | Assert.AreEqual(FNV1A64HashAlgorithm.HashReadOnlySpan(test), result); 67 | } 68 | 69 | [TestMethod] 70 | [DataRow("\xff\x00\x00\x01", 0x6961196491cc682dUL)] 71 | [DataRow("\x01\x00\x00\xff", 0xad2bb1774799dfe9UL)] 72 | [DataRow("\xff\x00\x00\x02", 0x6961166491cc6314UL)] 73 | [DataRow("\x02\x00\x00\xff", 0x8d1bb3904a3b1236UL)] 74 | [DataRow("\xff\x00\x00\x03", 0x6961176491cc64c7UL)] 75 | [DataRow("\x03\x00\x00\xff", 0xed205d87f40434c7UL)] 76 | [DataRow("\xff\x00\x00\x04", 0x6961146491cc5faeUL)] 77 | [DataRow("\x04\x00\x00\xff", 0xcd3baf5e44f8ad9cUL)] 78 | [DataRow("\x40\x51\x4e\x44", 0xe3b36596127cd6d8UL)] 79 | [DataRow("\x44\x4e\x51\x40", 0xf77f1072c8e8a646UL)] 80 | [DataRow("\x40\x51\x4e\x4a", 0xe3b36396127cd372UL)] 81 | [DataRow("\x4a\x4e\x51\x40", 0x6067dce9932ad458UL)] 82 | [DataRow("\x40\x51\x4e\x54", 0xe3b37596127cf208UL)] 83 | [DataRow("\x54\x4e\x51\x40", 0x4b7b10fa9fe83936UL)] 84 | [DataRow("", 0xcbf29ce484222325UL)] 85 | [DataRow("a", 0xaf63dc4c8601ec8cUL)] 86 | [DataRow("foobar", 0x85944171f73967e8UL)] 87 | [DataRow("\0", 0xaf63bd4c8601b7dfUL)] 88 | [DataRow("a\0", 0x089be207b544f1e4UL)] 89 | [DataRow("foobar\0", 0x34531ca7168b8f38UL)] 90 | [DataRow("hi", 0x08ba5f07b55ec3daUL)] 91 | [DataRow("hi\0", 0x337354193006cb6eUL)] 92 | [DataRow("hello", 0xa430d84680aabd0bUL)] 93 | [DataRow("hello\0", 0xa9bc8acca21f39b1UL)] 94 | [DataRow("127.0.0.1", 0xaabafe7104d914beUL)] 95 | [DataRow("127.0.0.1\0", 0xf4d3180b3cde3edaUL)] 96 | [DataRow("127.0.0.2", 0xaabafd7104d9130bUL)] 97 | [DataRow("127.0.0.2\0", 0xf4cfb20b3cdb5bb1UL)] 98 | [DataRow("127.0.0.3", 0xaabafc7104d91158UL)] 99 | [DataRow("127.0.0.3\0", 0xf4cc4c0b3cd87888UL)] 100 | [DataRow("feedfacedeadbeef", 0xcac54572bb1a6fc8UL)] 101 | [DataRow("feedfacedeadbeef\0", 0xa7a4c9f3edebf0d8UL)] 102 | [DataRow("line 1\nline 2\nline 3", 0x7829851fac17b143UL)] 103 | public void TestFNV1a64_CharSpan(string test, ulong result) 104 | { 105 | Assert.AreEqual(FNV1A64HashAlgorithm.HashReadOnlySpan(test.AsSpan()), result); 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /CP77Tools/Tasks/UnbundleTask.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using CP77.CR2W.Archive; 6 | using Catel.IoC; 7 | using CP77.CR2W; 8 | using WolvenKit.Common.Services; 9 | 10 | namespace CP77Tools.Tasks 11 | { 12 | public static partial class ConsoleFunctions 13 | { 14 | private static readonly ILoggerService logger = ServiceLocator.Default.ResolveType(); 15 | 16 | public static void UnbundleTask(string[] path, string outpath, 17 | string hash, string pattern, string regex) 18 | { 19 | if (path == null || path.Length < 1) 20 | { 21 | logger.LogString("Please fill in an input path", Logtype.Error); 22 | return; 23 | } 24 | 25 | Parallel.ForEach(path, file => 26 | { 27 | UnbundleTaskInner(file, outpath, hash, pattern, regex); 28 | }); 29 | 30 | } 31 | 32 | 33 | private static void UnbundleTaskInner(string path, string outpath, 34 | string hash, string pattern, string regex) 35 | { 36 | #region checks 37 | 38 | if (string.IsNullOrEmpty(path)) 39 | { 40 | logger.LogString("Please fill in an input path", Logtype.Error); 41 | return; 42 | } 43 | 44 | var inputFileInfo = new FileInfo(path); 45 | var inputDirInfo = new DirectoryInfo(path); 46 | 47 | 48 | if (!inputFileInfo.Exists && !inputDirInfo.Exists) 49 | { 50 | logger.LogString("Input path does not exist", Logtype.Error); 51 | return; 52 | } 53 | 54 | if (inputFileInfo.Exists && inputFileInfo.Extension != ".archive") 55 | { 56 | logger.LogString("Input file is not an .archive", Logtype.Error); 57 | return; 58 | } 59 | else if (inputDirInfo.Exists && inputDirInfo.GetFiles().All(_ => _.Extension != ".archive")) 60 | { 61 | logger.LogString("No .archive file to process in the input directory", Logtype.Error); 62 | return; 63 | } 64 | 65 | var isDirectory = !inputFileInfo.Exists; 66 | var basedir = inputFileInfo.Exists ? new FileInfo(path).Directory : inputDirInfo; 67 | 68 | #endregion 69 | 70 | List archiveFileInfos; 71 | if (isDirectory) 72 | { 73 | var archiveManager = new ArchiveManager(basedir); 74 | // TODO: use the manager here? 75 | archiveFileInfos = archiveManager.Archives.Select(_ => new FileInfo(_.Value.ArchiveAbsolutePath)).ToList(); 76 | } 77 | else 78 | { 79 | archiveFileInfos = new List {inputFileInfo}; 80 | } 81 | 82 | 83 | foreach (var processedarchive in archiveFileInfos) 84 | { 85 | // get outdirectory 86 | DirectoryInfo outDir; 87 | if (string.IsNullOrEmpty(outpath)) 88 | { 89 | outDir = Directory.CreateDirectory(Path.Combine( 90 | basedir.FullName, 91 | processedarchive.Name.Replace(".archive", ""))); 92 | } 93 | else 94 | { 95 | outDir = new DirectoryInfo(outpath); 96 | if (!outDir.Exists) 97 | { 98 | outDir = Directory.CreateDirectory(outpath); 99 | } 100 | 101 | if (inputDirInfo.Exists) 102 | { 103 | outDir = Directory.CreateDirectory(Path.Combine( 104 | outDir.FullName, 105 | processedarchive.Name.Replace(".archive", ""))); 106 | } 107 | } 108 | 109 | // read archive 110 | var ar = new Archive(processedarchive.FullName); 111 | 112 | var isHash = ulong.TryParse(hash, out ulong hashNumber); 113 | 114 | // run 115 | { 116 | if (!isHash && File.Exists(hash)) 117 | { 118 | var hashlist = File.ReadAllLines(hash) 119 | .ToList().Select(_ => ulong.TryParse(_, out ulong res) ? res : 0); 120 | logger.LogString($"Extracing all files from the hashlist ({hashlist.Count()}hashes) ...", 121 | Logtype.Normal); 122 | foreach (var hash_num in hashlist) 123 | { 124 | ar.ExtractSingle(hash_num, outDir); 125 | logger.LogString($" {ar.ArchiveAbsolutePath}: Extracted one file: {hash_num}", Logtype.Success); 126 | } 127 | 128 | logger.LogString($"Bulk extraction from hashlist file completed!", Logtype.Success); 129 | } 130 | else if (isHash && hashNumber != 0) 131 | { 132 | ar.ExtractSingle(hashNumber, outDir); 133 | logger.LogString($" {ar.ArchiveAbsolutePath}: Extracted one file: {hashNumber}", Logtype.Success); 134 | } 135 | else 136 | { 137 | var r = ar.ExtractAll(outDir, pattern, regex); 138 | logger.LogString($"{ar.ArchiveAbsolutePath}: Extracted {r.Item1.Count}/{r.Item2} files.", 139 | Logtype.Success); 140 | } 141 | 142 | } 143 | 144 | 145 | 146 | 147 | } 148 | 149 | return; 150 | } 151 | } 152 | } -------------------------------------------------------------------------------- /CP77Tools/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Catel.IoC; 5 | using System.CommandLine; 6 | using System.ComponentModel; 7 | using System.Configuration; 8 | using System.IO; 9 | using System.Reflection; 10 | using CP77Tools.Commands; 11 | using CP77Tools.Extensions; 12 | using Luna.ConsoleProgressBar; 13 | using Microsoft.Win32; 14 | using WolvenKit.Common.Oodle; 15 | using WolvenKit.Common.Services; 16 | 17 | namespace CP77Tools 18 | { 19 | class Program 20 | { 21 | [STAThread] 22 | public static async Task Main(string[] args) 23 | { 24 | ServiceLocator.Default.RegisterType(); 25 | ServiceLocator.Default.RegisterType(); 26 | 27 | var logger = ServiceLocator.Default.ResolveType(); 28 | var hashService = ServiceLocator.Default.ResolveType(); 29 | logger.OnStringLogged += delegate (object? sender, LogStringEventArgs args) 30 | { 31 | switch (args.Logtype) 32 | { 33 | 34 | case Logtype.Error: 35 | Console.ForegroundColor = ConsoleColor.Red; 36 | break; 37 | case Logtype.Important: 38 | Console.ForegroundColor = ConsoleColor.Yellow; 39 | break; 40 | case Logtype.Success: 41 | Console.ForegroundColor = ConsoleColor.Green; 42 | break; 43 | case Logtype.Normal: 44 | case Logtype.Wcc: 45 | break; 46 | default: 47 | throw new ArgumentOutOfRangeException(); 48 | } 49 | 50 | Console.WriteLine("[" + args.Logtype + "]" + args.Message); 51 | Console.ResetColor(); 52 | }; 53 | 54 | var rootCommand = new RootCommand 55 | { 56 | new UnbundleCommand(), 57 | new UncookCommand(), 58 | new RebuildCommand(), 59 | new PackCommand(), 60 | new ExportCommand(), 61 | 62 | new DumpCommand(), 63 | new CR2WCommand(), 64 | 65 | new HashCommand(), 66 | new OodleCommand(), 67 | }; 68 | 69 | //await ConsoleFunctions.UpdateHashesAsync(); 70 | hashService.ReloadLocally(); 71 | 72 | // try get oodle dll from game 73 | if (!TryCopyOodleLib()) 74 | { 75 | logger.LogString("Could not automatically find oo2ext_7_win64.dll. " + 76 | "Please manually copy and paste the dll found here Cyberpunk 2077\\bin\\x64\\oo2ext_7_win64.dll into this folder: " + 77 | $"{AppDomain.CurrentDomain.BaseDirectory}."); 78 | } 79 | 80 | // Run 81 | if (args == null || args.Length == 0) 82 | { 83 | // write welcome message 84 | rootCommand.InvokeAsync("-h").Wait(); 85 | 86 | while (true) 87 | { 88 | string line = System.Console.ReadLine(); 89 | 90 | 91 | if (line == "q()") 92 | return; 93 | 94 | var pb = new ConsoleProgressBar() 95 | { 96 | DisplayBars = false, 97 | DisplayPercentComplete = false, 98 | DisplayAnimation = false 99 | }; 100 | var parsed = CommandLineExtensions.ParseText(line, ' ', '"'); 101 | 102 | logger.PropertyChanged += OnLoggerOnPropertyChanged; 103 | void OnLoggerOnPropertyChanged(object? sender, PropertyChangedEventArgs args) 104 | { 105 | switch (args.PropertyName) 106 | { 107 | case "Progress": 108 | { 109 | if (logger.Progress.Item1 == 0) 110 | { 111 | pb = new ConsoleProgressBar() { DisplayBars = true, DisplayAnimation = false }; 112 | } 113 | 114 | pb?.Report(logger.Progress.Item1); 115 | if (logger.Progress.Item1 == 1) 116 | { 117 | System.Threading.Thread.Sleep(1000); 118 | 119 | Console.WriteLine(); 120 | pb?.Dispose(); 121 | pb = null; 122 | } 123 | 124 | break; 125 | } 126 | default: 127 | break; 128 | } 129 | } 130 | 131 | rootCommand.InvokeAsync(parsed.ToArray()).Wait(); 132 | 133 | await WriteLog(); 134 | logger.PropertyChanged -= OnLoggerOnPropertyChanged; 135 | } 136 | 137 | } 138 | else 139 | { 140 | rootCommand.InvokeAsync(args).Wait(); 141 | 142 | await WriteLog(); 143 | } 144 | 145 | 146 | async Task WriteLog() 147 | { 148 | if (string.IsNullOrEmpty(logger.ErrorLogStr)) 149 | return; 150 | 151 | var t = DateTime.Now.ToString("yyyyMMddHHmmss"); 152 | var fi = new FileInfo(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), 153 | $"errorlogs/log_{t}.txt")); 154 | if (fi.Directory != null) 155 | { 156 | Directory.CreateDirectory(fi.Directory.FullName); 157 | var log = logger.ErrorLogStr; 158 | await File.WriteAllTextAsync(fi.FullName, log); 159 | } 160 | else 161 | { 162 | 163 | } 164 | } 165 | 166 | } 167 | 168 | private delegate void StrDelegate(string value); 169 | 170 | private static string TryGetGameInstallDir() 171 | { 172 | var cp77BinDir = ""; 173 | var cp77exe = ""; 174 | // check for CP77_DIR environment variable first 175 | var CP77_DIR = System.Environment.GetEnvironmentVariable("CP77_DIR", EnvironmentVariableTarget.User); 176 | if (!string.IsNullOrEmpty(CP77_DIR) && new DirectoryInfo(CP77_DIR).Exists) 177 | cp77BinDir = Path.Combine(CP77_DIR, "bin", "x64"); 178 | if (File.Exists(Path.Combine(cp77BinDir, "Cyberpunk2077.exe"))) 179 | return cp77BinDir; 180 | 181 | // else: look for install location 182 | const string uninstallkey = "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\"; 183 | const string uninstallkey2 = "SOFTWARE\\Wow6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\"; 184 | const string gameName = "Cyberpunk 2077"; 185 | const string exeName = "Cyberpunk2077.exe"; 186 | var exePath = ""; 187 | StrDelegate strDelegate = msg => cp77exe = msg; 188 | 189 | try 190 | { 191 | Parallel.ForEach(Registry.LocalMachine.OpenSubKey(uninstallkey)?.GetSubKeyNames(), item => 192 | { 193 | var programName = Registry.LocalMachine.OpenSubKey(uninstallkey + item) 194 | ?.GetValue("DisplayName"); 195 | var installLocation = Registry.LocalMachine.OpenSubKey(uninstallkey + item) 196 | ?.GetValue("InstallLocation"); 197 | if (programName != null && installLocation != null) 198 | { 199 | if (programName.ToString().Contains(gameName) || 200 | programName.ToString().Contains(gameName)) 201 | { 202 | exePath = Directory.GetFiles(installLocation.ToString(), exeName, 203 | SearchOption.AllDirectories).First(); 204 | } 205 | } 206 | 207 | strDelegate.Invoke(exePath); 208 | }); 209 | Parallel.ForEach(Registry.LocalMachine.OpenSubKey(uninstallkey2)?.GetSubKeyNames(), item => 210 | { 211 | var programName = Registry.LocalMachine.OpenSubKey(uninstallkey2 + item) 212 | ?.GetValue("DisplayName"); 213 | var installLocation = Registry.LocalMachine.OpenSubKey(uninstallkey2 + item) 214 | ?.GetValue("InstallLocation"); 215 | if (programName != null && installLocation != null) 216 | { 217 | if (programName.ToString().Contains(gameName) || 218 | programName.ToString().Contains(gameName)) 219 | { 220 | if (Directory.Exists(installLocation.ToString())) 221 | exePath = Directory.GetFiles(installLocation.ToString(), exeName, 222 | SearchOption.AllDirectories).First(); 223 | } 224 | } 225 | 226 | strDelegate.Invoke(exePath); 227 | }); 228 | 229 | if (File.Exists(cp77exe)) 230 | cp77BinDir = new FileInfo(cp77exe).Directory.FullName; 231 | } 232 | catch (Exception e) 233 | { 234 | 235 | } 236 | 237 | if (string.IsNullOrEmpty(cp77BinDir)) 238 | return null; 239 | if (!File.Exists(Path.Combine(cp77BinDir, "Cyberpunk2077.exe"))) 240 | return null; 241 | 242 | return cp77BinDir; 243 | } 244 | 245 | private static bool TryCopyOodleLib() 246 | { 247 | var ass = AppDomain.CurrentDomain.BaseDirectory; 248 | var destFileName = Path.Combine(ass, "oo2ext_7_win64.dll"); 249 | if (File.Exists(destFileName)) 250 | return true; 251 | 252 | var cp77BinDir = TryGetGameInstallDir(); 253 | if (string.IsNullOrEmpty(cp77BinDir)) 254 | return false; 255 | 256 | // copy oodle dll 257 | var oodleInfo = new FileInfo(Path.Combine(cp77BinDir, "oo2ext_7_win64.dll")); 258 | if (!oodleInfo.Exists) 259 | return false; 260 | 261 | if (!File.Exists(destFileName)) 262 | oodleInfo.CopyTo(destFileName); 263 | 264 | return true; 265 | } 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /CP77Tools/Tasks/DumpTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | //using System.IO.MemoryMappedFiles; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Catel.IoC; 11 | using CP77.CR2W.Archive; 12 | using Newtonsoft.Json; 13 | using WolvenKit.Common; 14 | using WolvenKit.Common.Extensions; 15 | using CP77.CR2W; 16 | using CP77.CR2W.Reflection; 17 | using CP77.CR2W.Types; 18 | using WolvenKit.Common.FNV1A; 19 | using WolvenKit.Common.Services; 20 | 21 | namespace CP77Tools.Tasks 22 | { 23 | public class ArchiveDumpObject 24 | { 25 | public ConcurrentDictionary FileDictionary { get; set; } 26 | public ConcurrentDictionary TextureDictionary { get; set; } 27 | public string Filename { get; set; } 28 | } 29 | 30 | public class Cr2wChunkInfo 31 | { 32 | public Dictionary Stringdict { get; set; } 33 | public List Imports { get; set; } 34 | public List Buffers { get; set; } 35 | 36 | public List Chunks { get; set; } 37 | 38 | public List ChunkData { get; } = 39 | new List(); 40 | 41 | public string Filename { get; set; } 42 | } 43 | 44 | public class Cr2wTextureInfo 45 | { 46 | public string Filename { get; set; } 47 | 48 | public ushort width { get; set; } 49 | public ushort height { get; set; } 50 | public byte mips { get; set; } 51 | public ushort slicecount { get; set; } 52 | public uint alignment { get; set; } 53 | public CEnum compression { get; set; } 54 | public CEnum Group { get; set; } 55 | public CEnum rawFormat { get; set; } 56 | } 57 | 58 | 59 | public static partial class ConsoleFunctions 60 | { 61 | private static byte[] MAGIC = {0x43, 0x52, 0x32, 0x57}; 62 | 63 | public static void DumpTask(string[] path, bool imports, bool missinghashes, 64 | bool texinfo, bool classinfo, bool dump, bool list) 65 | { 66 | if (path == null || path.Length < 1) 67 | { 68 | logger.LogString("Please fill in an input path", Logtype.Error); 69 | return; 70 | } 71 | 72 | Parallel.ForEach(path, file => 73 | { 74 | DumpTaskInner(file, imports, missinghashes, texinfo, classinfo, dump, list); 75 | }); 76 | 77 | } 78 | 79 | public static int DumpTaskInner(string path, bool imports, bool missinghashes, 80 | bool texinfo, bool classinfo, bool dump, bool list) 81 | { 82 | #region checks 83 | 84 | if (string.IsNullOrEmpty(path)) 85 | { 86 | ConsoleFunctions.logger.LogString("Please fill in an input path",Logtype.Error); 87 | return 0; 88 | } 89 | 90 | var isDirectory = false; 91 | var inputFileInfo = new FileInfo(path); 92 | var inputDirInfo = new DirectoryInfo(path); 93 | if (!inputFileInfo.Exists) 94 | { 95 | if (!inputDirInfo.Exists) 96 | return 0; 97 | else 98 | isDirectory = true; 99 | } 100 | #endregion 101 | 102 | var archives = new List(); 103 | 104 | if (isDirectory) 105 | { 106 | archives.AddRange(inputDirInfo 107 | .GetFiles("*.archive", SearchOption.AllDirectories) 108 | .Select(_ => new Archive(_.FullName))); 109 | } 110 | else 111 | { 112 | archives.Add(new Archive(inputFileInfo.FullName)); 113 | } 114 | 115 | 116 | 117 | var mainController = ServiceLocator.Default.ResolveType(); 118 | var logger = ServiceLocator.Default.ResolveType(); 119 | var typedict = new ConcurrentDictionary>(); 120 | 121 | // Parallel 122 | foreach (var ar in archives) 123 | { 124 | if (classinfo) 125 | { 126 | // using var mmf = MemoryMappedFile.CreateFromFile(ar.Filepath, FileMode.Open, 127 | // ar.Filepath.GetHashMD5(), 0, 128 | // MemoryMappedFileAccess.Read); 129 | 130 | var fileinfo = ar.Files.Values; 131 | var query = fileinfo.GroupBy( 132 | ext => Path.GetExtension(ext.FileName), 133 | file => file, 134 | (ext, finfo) => new 135 | { 136 | Key = ext, 137 | File = fileinfo.Where(_ => Path.GetExtension(_.FileName) == ext) 138 | }).ToList(); 139 | 140 | 141 | 142 | var total = query.Count; 143 | logger.LogString($"Exporting {total} bundle entries "); 144 | 145 | Thread.Sleep(1000); 146 | int progress = 0; 147 | logger.LogProgress(0); 148 | 149 | // foreach extension 150 | Parallel.ForEach(query, result => 151 | { 152 | if (!string.IsNullOrEmpty(result.Key)) 153 | { 154 | Parallel.ForEach(result.File, fi => 155 | { 156 | var (f, b) = ar.GetFileData(fi.NameHash64, false); 157 | using var ms = new MemoryStream(f); 158 | using var br = new BinaryReader(ms); 159 | 160 | var cr2w = new CR2WFile(); 161 | try 162 | { 163 | cr2w.ReadImportsAndBuffers(br); 164 | } 165 | catch (Exception e) 166 | { 167 | return; 168 | } 169 | 170 | foreach (var chunk in cr2w.Chunks) 171 | { 172 | var o = chunk.GetDumpObject(br); 173 | if (o != null) Register(o); 174 | } 175 | }); 176 | } 177 | 178 | Interlocked.Increment(ref progress); 179 | logger.LogProgress(progress / (float)total); 180 | 181 | logger.LogString($"Dumped extension {result.Key}", Logtype.Normal); 182 | }); 183 | 184 | 185 | } 186 | if (imports || texinfo) 187 | { 188 | // using var mmf = MemoryMappedFile.CreateFromFile(ar.Filepath, FileMode.Open, 189 | // ar.Filepath.GetHashMD5(), 0, 190 | // MemoryMappedFileAccess.Read); 191 | 192 | var fileDictionary = new ConcurrentDictionary(); 193 | var texDictionary = new ConcurrentDictionary(); 194 | 195 | // get info 196 | var count = ar.FileCount; 197 | logger.LogString($"Exporting {count} bundle entries "); 198 | 199 | Thread.Sleep(1000); 200 | int progress = 0; 201 | logger.LogProgress(0); 202 | 203 | Parallel.For(0, count, i => 204 | { 205 | var entry = ar.Files.ToList()[i]; 206 | var hash = entry.Key; 207 | var filename = string.IsNullOrEmpty(entry.Value.FileName) ? hash.ToString() : entry.Value.FileName; 208 | 209 | if (imports) 210 | { 211 | var (f, buffers) = ar.GetFileData(hash, false); 212 | 213 | // check if cr2w file 214 | if (f.Length < 4) 215 | return; 216 | var id = f.Take(4); 217 | if (!id.SequenceEqual(MAGIC)) 218 | return; 219 | 220 | var cr2w = new CR2WFile(); 221 | 222 | using var ms = new MemoryStream(f); 223 | using var br = new BinaryReader(ms); 224 | cr2w.ReadImportsAndBuffers(br); 225 | 226 | var obj = new Cr2wChunkInfo 227 | { 228 | Filename = filename, 229 | Imports = cr2w.Imports 230 | }; 231 | 232 | 233 | 234 | fileDictionary.AddOrUpdate(hash, obj, (arg1, o) => obj); 235 | } 236 | 237 | if (texinfo) 238 | { 239 | if (!string.IsNullOrEmpty(entry.Value.FileName) && entry.Value.FileName.Contains(".xbm")) 240 | { 241 | var (f, buffers) = ar.GetFileData(hash, false); 242 | 243 | // check if cr2w file 244 | if (f.Length < 4) 245 | return; 246 | var id = f.Take(4); 247 | if (!id.SequenceEqual(MAGIC)) 248 | return; 249 | 250 | var cr2w = new CR2WFile(); 251 | 252 | using var ms = new MemoryStream(f); 253 | using var br = new BinaryReader(ms); 254 | var result = cr2w.Read(br); 255 | 256 | if (result != EFileReadErrorCodes.NoError) 257 | return; 258 | if (!(cr2w.Chunks.FirstOrDefault()?.data is CBitmapTexture xbm) || 259 | !(cr2w.Chunks[1]?.data is rendRenderTextureBlobPC blob)) 260 | return; 261 | 262 | // create dds header 263 | var texinfo = new Cr2wTextureInfo() 264 | { 265 | Filename = filename, 266 | width = blob.Header.SizeInfo.Width.val, 267 | height = blob.Header.SizeInfo.Height.val, 268 | mips = blob.Header.TextureInfo.MipCount.val, 269 | slicecount = blob.Header.TextureInfo.SliceCount.val, 270 | alignment = blob.Header.TextureInfo.DataAlignment.val, 271 | compression = xbm.Setup.Compression, 272 | Group = xbm.Setup.Group, 273 | rawFormat = xbm.Setup.RawFormat, 274 | }; 275 | 276 | texDictionary.AddOrUpdate(hash, texinfo, (arg1, o) => texinfo); 277 | } 278 | } 279 | 280 | Interlocked.Increment(ref progress); 281 | logger.LogProgress(progress / (float)count); 282 | }); 283 | 284 | // write 285 | var arobj = new ArchiveDumpObject() 286 | { 287 | Filename = ar.ArchiveAbsolutePath, 288 | FileDictionary = fileDictionary, 289 | TextureDictionary = texDictionary 290 | }; 291 | 292 | if (imports) 293 | { 294 | using var hwriter = File.CreateText($"{ar.ArchiveAbsolutePath}.hashes.csv"); 295 | hwriter.WriteLine("String,Hash"); 296 | List allimports = new List(); 297 | 298 | foreach (var (key, value) in arobj.FileDictionary) 299 | { 300 | if (value.Imports == null) 301 | continue; 302 | allimports.AddRange(value.Imports.Select(import => import.DepotPathStr)); 303 | } 304 | foreach (var str in allimports.Distinct()) 305 | { 306 | var hash = FNV1A64HashAlgorithm.HashString(str); 307 | if (!mainController.Hashdict.ContainsKey(hash)) 308 | hwriter.WriteLine($"{str},{hash}"); 309 | } 310 | 311 | logger.LogString($"Finished. Dump file written to {ar.ArchiveAbsolutePath}.", Logtype.Success); 312 | 313 | //write 314 | File.WriteAllText($"{ar.ArchiveAbsolutePath}.json", 315 | JsonConvert.SerializeObject(arobj, Formatting.Indented, new JsonSerializerSettings() 316 | { 317 | ReferenceLoopHandling = ReferenceLoopHandling.Ignore, 318 | PreserveReferencesHandling = PreserveReferencesHandling.None, 319 | TypeNameHandling = TypeNameHandling.None 320 | })); 321 | logger.LogString($"Finished. Dump file written to {inputFileInfo.FullName}.json", Logtype.Success); 322 | 323 | } 324 | 325 | if (texinfo) 326 | { 327 | //write 328 | File.WriteAllText($"{ar.ArchiveAbsolutePath}.textures.json", 329 | JsonConvert.SerializeObject(arobj, Formatting.Indented, new JsonSerializerSettings() 330 | { 331 | ReferenceLoopHandling = ReferenceLoopHandling.Ignore, 332 | PreserveReferencesHandling = PreserveReferencesHandling.None, 333 | TypeNameHandling = TypeNameHandling.None 334 | })); 335 | logger.LogString($"Finished. Dump file written to {inputFileInfo.FullName}.json", Logtype.Success); 336 | } 337 | } 338 | 339 | // TODO: add this here 340 | if (dump) 341 | { 342 | File.WriteAllText($"{ar.ArchiveAbsolutePath}.json", 343 | JsonConvert.SerializeObject(ar, Formatting.Indented, new JsonSerializerSettings() 344 | { 345 | ReferenceLoopHandling = ReferenceLoopHandling.Ignore, 346 | PreserveReferencesHandling = PreserveReferencesHandling.None, 347 | TypeNameHandling = TypeNameHandling.None 348 | })); 349 | 350 | logger.LogString($"Finished dumping {ar.ArchiveAbsolutePath}.", Logtype.Success); 351 | } 352 | 353 | if (list) 354 | { 355 | foreach (var entry in ar.Files) 356 | { 357 | logger.LogString(entry.Value.FileName, Logtype.Normal); 358 | } 359 | } 360 | } 361 | 362 | if (classinfo) 363 | { 364 | //write class definitions 365 | var outdir = isDirectory 366 | ? Path.Combine(inputDirInfo.FullName, "ClassDefinitions") 367 | : Path.Combine(inputFileInfo.Directory.FullName, "ClassDefinitions"); 368 | Directory.CreateDirectory(outdir); 369 | var outfile = Path.Combine(outdir, "classdefinitions.txt"); 370 | var outfileS = Path.Combine(outdir, "classdefinitions_simple.json"); 371 | var text = ""; 372 | foreach (var (typename, variables) in typedict) 373 | { 374 | //write 375 | var sb = new StringBuilder($"[REDMeta] public class {typename} : CVariable {{\r\n"); 376 | 377 | var variableslist = variables.ToList(); 378 | for (int i = 0; i < variableslist.Count; i++) 379 | { 380 | var typ = variableslist[i].Split(' ').First(); 381 | var nam = variableslist[i].Split(' ').Last(); 382 | var wktype = REDReflection.GetWKitBaseTypeFromREDBaseType(typ); 383 | 384 | if (string.IsNullOrEmpty(nam)) nam = "Missing"; 385 | if (string.IsNullOrEmpty(typ)) typ = "Missing"; 386 | 387 | 388 | sb.Append($"\t[Ordinal({i})] [RED(\"{nam}\")] public {wktype} {nam.FirstCharToUpper()} {{ get; set; }}\r\n"); 389 | } 390 | 391 | sb.Append( 392 | $"public {typename}(CR2WFile cr2w, CVariable parent, string name) : base(cr2w, parent, name) {{ }}\r\n"); 393 | 394 | sb.Append("}\r\n"); 395 | text += sb.ToString(); 396 | } 397 | File.WriteAllText(outfile, text); 398 | 399 | //write 400 | File.WriteAllText(outfileS, 401 | JsonConvert.SerializeObject(typedict, Formatting.Indented, new JsonSerializerSettings() 402 | { 403 | ReferenceLoopHandling = ReferenceLoopHandling.Ignore, 404 | PreserveReferencesHandling = PreserveReferencesHandling.None, 405 | TypeNameHandling = TypeNameHandling.None 406 | })); 407 | 408 | logger.LogString("Done.", Logtype.Success); 409 | } 410 | if (missinghashes) 411 | { 412 | var missinghashtxt = isDirectory 413 | ? Path.Combine(inputDirInfo.FullName, "missinghashes.txt") 414 | : $"{inputFileInfo.FullName}.missinghashes.txt"; 415 | using var mwriter = File.CreateText(missinghashtxt); 416 | foreach (var ar in archives) 417 | { 418 | var ctr = 0; 419 | foreach (var (hash, fileInfoEntry) in ar.Files) 420 | { 421 | if (fileInfoEntry.NameOrHash == hash.ToString()) 422 | { 423 | mwriter.WriteLine(hash); 424 | ctr++; 425 | } 426 | } 427 | logger.LogString($"{ar.ArchiveAbsolutePath} - missing: {ctr}", Logtype.Normal); 428 | } 429 | } 430 | 431 | 432 | return 1; 433 | 434 | void Register(CR2WExportWrapper.Cr2wVariableDumpObject o) 435 | { 436 | if (o?.Type == null) return; 437 | 438 | o.Variables ??= new List(); 439 | 440 | IEnumerable vars = o.Variables.Select(_ => _.ToSimpleString()); 441 | if (typedict.ContainsKey(o.Type)) 442 | { 443 | var existing = typedict[o.Type]; 444 | var newlist = o.Variables.Select(_ => _.ToSimpleString()); 445 | if (existing != null) vars = existing.Union(newlist); 446 | } 447 | typedict.AddOrUpdate(o.Type, vars, (arg1, ol) => ol); 448 | 449 | foreach (var oVariable in o.Variables) 450 | { 451 | // generic types (arrays, handles, refs) 452 | if (oVariable.Type != null && oVariable.Type.Contains(":")) 453 | { 454 | var gentyp = oVariable.Type.Split(":").First(); 455 | var innertype = oVariable.Type.Substring(gentyp.Length + 1); 456 | var innertype2 = oVariable.Type[(gentyp.Length + 1)]; 457 | if (gentyp == "array") 458 | { 459 | oVariable.Type = innertype; 460 | Register(oVariable); 461 | } 462 | else 463 | { 464 | continue; 465 | } 466 | 467 | 468 | } 469 | 470 | Register(oVariable); 471 | } 472 | } 473 | 474 | } 475 | } 476 | } 477 | 478 | -------------------------------------------------------------------------------- /CP77.MSTests/Cr2wUnitTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Configuration; 4 | using System.IO; 5 | using System.IO.MemoryMappedFiles; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Catel.IoC; 10 | using WolvenKit.Common.Services; 11 | using CP77.CR2W; 12 | using CP77.CR2W.Archive; 13 | using Microsoft.Extensions.Configuration; 14 | using Microsoft.VisualStudio.TestTools.UnitTesting; 15 | using Newtonsoft.Json; 16 | using Newtonsoft.Json.Converters; 17 | using WolvenKit.Common; 18 | 19 | namespace CP77.MSTests 20 | { 21 | [TestClass] 22 | public class Cr2WUnitTest : GameUnitTest 23 | { 24 | [ClassInitialize] 25 | public static void SetupClass(TestContext context) 26 | { 27 | Setup(context); 28 | } 29 | 30 | 31 | #region test methods 32 | 33 | [TestMethod] 34 | public void Test_All_Extensions() 35 | { 36 | Test_Extension(); 37 | } 38 | 39 | [TestMethod] 40 | public void Test_Can_Read_acousticdata() 41 | { 42 | Test_Extension(".acousticdata"); 43 | } 44 | 45 | [TestMethod] 46 | public void Test_Can_Read_actionanimdb() 47 | { 48 | Test_Extension(".actionanimdb"); 49 | } 50 | 51 | [TestMethod] 52 | public void Test_Can_Read_aiarch() 53 | { 54 | Test_Extension(".aiarch"); 55 | } 56 | 57 | [TestMethod] 58 | public void Test_Can_Read_animgraph() 59 | { 60 | Test_Extension(".animgraph"); 61 | } 62 | 63 | [TestMethod] 64 | public void Test_Can_Read_anims() 65 | { 66 | Test_Extension(".anims"); 67 | } 68 | 69 | [TestMethod] 70 | public void Test_Can_Read_app() 71 | { 72 | Test_Extension(".app"); 73 | } 74 | 75 | [TestMethod] 76 | public void Test_Can_Read_archetypes() 77 | { 78 | Test_Extension(".archetypes"); 79 | } 80 | 81 | [TestMethod] 82 | public void Test_Can_Read_areas() 83 | { 84 | Test_Extension(".areas"); 85 | } 86 | 87 | [TestMethod] 88 | public void Test_Can_Read_audio_metadata() 89 | { 90 | Test_Extension(".audio_metadata"); 91 | } 92 | 93 | [TestMethod] 94 | public void Test_Can_Read_audiovehcurveset() 95 | { 96 | Test_Extension(".audiovehcurveset"); 97 | } 98 | 99 | [TestMethod] 100 | public void Test_Can_Read_behavior() 101 | { 102 | Test_Extension(".behavior"); 103 | } 104 | 105 | [TestMethod] 106 | public void Test_Can_Read_bikecurveset() 107 | { 108 | Test_Extension(".bikecurveset"); 109 | } 110 | 111 | [TestMethod] 112 | public void Test_Can_Read_bin() 113 | { 114 | Test_Extension(".bin"); 115 | } 116 | 117 | [TestMethod] 118 | public void Test_Can_Read_bk2() 119 | { 120 | Test_Extension(".bk2"); 121 | } 122 | 123 | [TestMethod] 124 | public void Test_Can_Read_bnk() 125 | { 126 | Test_Extension(".bnk"); 127 | } 128 | 129 | [TestMethod] 130 | public void Test_Can_Read_camcurveset() 131 | { 132 | Test_Extension(".camcurveset"); 133 | } 134 | 135 | [TestMethod] 136 | public void Test_Can_Read_cfoliage() 137 | { 138 | Test_Extension(".cfoliage"); 139 | } 140 | 141 | [TestMethod] 142 | public void Test_Can_Read_charcustpreset() 143 | { 144 | Test_Extension(".charcustpreset"); 145 | } 146 | 147 | [TestMethod] 148 | public void Test_Can_Read_cminimap() 149 | { 150 | Test_Extension(".cminimap"); 151 | } 152 | 153 | [TestMethod] 154 | public void Test_Can_Read_community() 155 | { 156 | Test_Extension(".community"); 157 | } 158 | 159 | [TestMethod] 160 | public void Test_Can_Read_conversations() 161 | { 162 | Test_Extension(".conversations"); 163 | } 164 | 165 | [TestMethod] 166 | public void Test_Can_Read_cooked_mlsetup() 167 | { 168 | Test_Extension(".cooked_mlsetup"); 169 | } 170 | 171 | [TestMethod] 172 | public void Test_Can_Read_cookedanims() 173 | { 174 | Test_Extension(".cookedanims"); 175 | } 176 | 177 | [TestMethod] 178 | public void Test_Can_Read_cookedapp() 179 | { 180 | Test_Extension(".cookedapp"); 181 | } 182 | 183 | [TestMethod] 184 | public void Test_Can_Read_credits() 185 | { 186 | Test_Extension(".credits"); 187 | } 188 | 189 | [TestMethod] 190 | public void Test_Can_Read_csv() 191 | { 192 | Test_Extension(".csv"); 193 | } 194 | 195 | [TestMethod] 196 | public void Test_Can_Read_cubemap() 197 | { 198 | Test_Extension(".cubemap"); 199 | } 200 | 201 | [TestMethod] 202 | public void Test_Can_Read_curveset() 203 | { 204 | Test_Extension(".curveset"); 205 | } 206 | 207 | [TestMethod] 208 | public void Test_Can_Read_dat() 209 | { 210 | Test_Extension(".dat"); 211 | } 212 | 213 | [TestMethod] 214 | public void Test_Can_Read_devices() 215 | { 216 | Test_Extension(".devices"); 217 | } 218 | 219 | [TestMethod] 220 | public void Test_Can_Read_dtex() 221 | { 222 | Test_Extension(".dtex"); 223 | } 224 | 225 | [TestMethod] 226 | public void Test_Can_Read_effect() 227 | { 228 | Test_Extension(".effect"); 229 | } 230 | 231 | [TestMethod] 232 | public void Test_Can_Read_ent() 233 | { 234 | Test_Extension(".ent"); 235 | } 236 | 237 | [TestMethod] 238 | public void Test_Can_Read_env() 239 | { 240 | Test_Extension(".env"); 241 | } 242 | 243 | [TestMethod] 244 | public void Test_Can_Read_envparam() 245 | { 246 | Test_Extension(".envparam"); 247 | } 248 | 249 | [TestMethod] 250 | public void Test_Can_Read_envprobe() 251 | { 252 | Test_Extension(".envprobe"); 253 | } 254 | 255 | [TestMethod] 256 | public void Test_Can_Read_es() 257 | { 258 | Test_Extension(".es"); 259 | } 260 | 261 | [TestMethod] 262 | public void Test_Can_Read_facialcustom() 263 | { 264 | Test_Extension(".facialcustom"); 265 | } 266 | 267 | [TestMethod] 268 | public void Test_Can_Read_facialsetup() 269 | { 270 | Test_Extension(".facialsetup"); 271 | } 272 | 273 | [TestMethod] 274 | public void Test_Can_Read_fb2tl() 275 | { 276 | Test_Extension(".fb2tl"); 277 | } 278 | 279 | [TestMethod] 280 | public void Test_Can_Read_fnt() 281 | { 282 | Test_Extension(".fnt"); 283 | } 284 | 285 | [TestMethod] 286 | public void Test_Can_Read_folbrush() 287 | { 288 | Test_Extension(".folbrush"); 289 | } 290 | 291 | [TestMethod] 292 | public void Test_Can_Read_foldest() 293 | { 294 | Test_Extension(".foldest"); 295 | } 296 | 297 | [TestMethod] 298 | public void Test_Can_Read_fp() 299 | { 300 | Test_Extension(".fp"); 301 | } 302 | 303 | [TestMethod] 304 | public void Test_Can_Read_gamedef() 305 | { 306 | Test_Extension(".gamedef"); 307 | } 308 | 309 | [TestMethod] 310 | public void Test_Can_Read_garmentlayerparams() 311 | { 312 | Test_Extension(".garmentlayerparams"); 313 | } 314 | 315 | [TestMethod] 316 | public void Test_Can_Read_genericanimdb() 317 | { 318 | Test_Extension(".genericanimdb"); 319 | } 320 | 321 | [TestMethod] 322 | public void Test_Can_Read_geometry_cache() 323 | { 324 | Test_Extension(".geometry_cache"); 325 | } 326 | 327 | [TestMethod] 328 | public void Test_Can_Read_gidata() 329 | { 330 | Test_Extension(".gidata"); 331 | } 332 | 333 | [TestMethod] 334 | public void Test_Can_Read_gradient() 335 | { 336 | Test_Extension(".gradient"); 337 | } 338 | 339 | [TestMethod] 340 | public void Test_Can_Read_hitrepresentation() 341 | { 342 | Test_Extension(".hitrepresentation"); 343 | } 344 | 345 | [TestMethod] 346 | public void Test_Can_Read_hp() 347 | { 348 | Test_Extension(".hp"); 349 | } 350 | 351 | [TestMethod] 352 | public void Test_Can_Read_ies() 353 | { 354 | Test_Extension(".ies"); 355 | } 356 | 357 | [TestMethod] 358 | public void Test_Can_Read_inkanim() 359 | { 360 | Test_Extension(".inkanim"); 361 | } 362 | 363 | [TestMethod] 364 | public void Test_Can_Read_inkatlas() 365 | { 366 | Test_Extension(".inkatlas"); 367 | } 368 | 369 | [TestMethod] 370 | public void Test_Can_Read_inkcharcustomization() 371 | { 372 | Test_Extension(".inkcharcustomization"); 373 | } 374 | 375 | [TestMethod] 376 | public void Test_Can_Read_inkfontfamily() 377 | { 378 | Test_Extension(".inkfontfamily"); 379 | } 380 | 381 | [TestMethod] 382 | public void Test_Can_Read_inkfullscreencomposition() 383 | { 384 | Test_Extension(".inkfullscreencomposition"); 385 | } 386 | 387 | [TestMethod] 388 | public void Test_Can_Read_inkgamesettings() 389 | { 390 | Test_Extension(".inkgamesettings"); 391 | } 392 | 393 | [TestMethod] 394 | public void Test_Can_Read_inkhud() 395 | { 396 | Test_Extension(".inkhud"); 397 | } 398 | 399 | [TestMethod] 400 | public void Test_Can_Read_inklayers() 401 | { 402 | Test_Extension(".inklayers"); 403 | } 404 | 405 | [TestMethod] 406 | public void Test_Can_Read_inkmenu() 407 | { 408 | Test_Extension(".inkmenu"); 409 | } 410 | 411 | [TestMethod] 412 | public void Test_Can_Read_inkshapecollection() 413 | { 414 | Test_Extension(".inkshapecollection"); 415 | } 416 | 417 | [TestMethod] 418 | public void Test_Can_Read_inkstyle() 419 | { 420 | Test_Extension(".inkstyle"); 421 | } 422 | 423 | [TestMethod] 424 | public void Test_Can_Read_inktypography() 425 | { 426 | Test_Extension(".inktypography"); 427 | } 428 | 429 | [TestMethod] 430 | public void Test_Can_Read_inkwidget() 431 | { 432 | Test_Extension(".inkwidget"); 433 | } 434 | 435 | [TestMethod] 436 | public void Test_Can_Read_interaction() 437 | { 438 | Test_Extension(".interaction"); 439 | } 440 | 441 | [TestMethod] 442 | public void Test_Can_Read_journal() 443 | { 444 | Test_Extension(".journal"); 445 | } 446 | 447 | [TestMethod] 448 | public void Test_Can_Read_journaldesc() 449 | { 450 | Test_Extension(".journaldesc"); 451 | } 452 | 453 | [TestMethod] 454 | public void Test_Can_Read_json() 455 | { 456 | Test_Extension(".json"); 457 | } 458 | 459 | [TestMethod] 460 | public void Test_Can_Read_lane_connections() 461 | { 462 | Test_Extension(".lane_connections"); 463 | } 464 | 465 | [TestMethod] 466 | public void Test_Can_Read_lane_polygons() 467 | { 468 | Test_Extension(".lane_polygons"); 469 | } 470 | 471 | [TestMethod] 472 | public void Test_Can_Read_lane_spots() 473 | { 474 | Test_Extension(".lane_spots"); 475 | } 476 | 477 | [TestMethod] 478 | public void Test_Can_Read_lights() 479 | { 480 | Test_Extension(".lights"); 481 | } 482 | 483 | [TestMethod] 484 | public void Test_Can_Read_lipmap() 485 | { 486 | Test_Extension(".lipmap"); 487 | } 488 | 489 | [TestMethod] 490 | public void Test_Can_Read_location() 491 | { 492 | Test_Extension(".location"); 493 | } 494 | 495 | [TestMethod] 496 | public void Test_Can_Read_locopaths() 497 | { 498 | Test_Extension(".locopaths"); 499 | } 500 | 501 | [TestMethod] 502 | public void Test_Can_Read_loot() 503 | { 504 | Test_Extension(".loot"); 505 | } 506 | 507 | [TestMethod] 508 | public void Test_Can_Read_mappins() 509 | { 510 | Test_Extension(".mappins"); 511 | } 512 | 513 | [TestMethod] 514 | public void Test_Can_Read_mesh() 515 | { 516 | Test_Extension(".mesh"); 517 | } 518 | 519 | [TestMethod] 520 | public void Test_Can_Read_mi() 521 | { 522 | Test_Extension(".mi"); 523 | } 524 | 525 | [TestMethod] 526 | public void Test_Can_Read_mlmask() 527 | { 528 | Test_Extension(".mlmask"); 529 | } 530 | 531 | [TestMethod] 532 | public void Test_Can_Read_mlsetup() 533 | { 534 | Test_Extension(".mlsetup"); 535 | } 536 | 537 | [TestMethod] 538 | public void Test_Can_Read_mltemplate() 539 | { 540 | Test_Extension(".mltemplate"); 541 | } 542 | 543 | [TestMethod] 544 | public void Test_Can_Read_morphtarget() 545 | { 546 | Test_Extension(".morphtarget"); 547 | } 548 | 549 | [TestMethod] 550 | public void Test_Can_Read_mt() 551 | { 552 | Test_Extension(".mt"); 553 | } 554 | 555 | [TestMethod] 556 | public void Test_Can_Read_navmesh() 557 | { 558 | Test_Extension(".navmesh"); 559 | } 560 | 561 | [TestMethod] 562 | public void Test_Can_Read_null_areas() 563 | { 564 | Test_Extension(".null_areas"); 565 | } 566 | 567 | [TestMethod] 568 | public void Test_Can_Read_opusinfo() 569 | { 570 | Test_Extension(".opusinfo"); 571 | } 572 | 573 | [TestMethod] 574 | public void Test_Can_Read_opuspak() 575 | { 576 | Test_Extension(".opuspak"); 577 | } 578 | 579 | [TestMethod] 580 | public void Test_Can_Read_particle() 581 | { 582 | Test_Extension(".particle"); 583 | } 584 | 585 | [TestMethod] 586 | public void Test_Can_Read_phys() 587 | { 588 | Test_Extension(".phys"); 589 | } 590 | 591 | [TestMethod] 592 | public void Test_Can_Read_physicalscene() 593 | { 594 | Test_Extension(".physicalscene"); 595 | } 596 | 597 | [TestMethod] 598 | public void Test_Can_Read_physmatlib() 599 | { 600 | Test_Extension(".physmatlib"); 601 | } 602 | 603 | [TestMethod] 604 | public void Test_Can_Read_poimappins() 605 | { 606 | Test_Extension(".poimappins"); 607 | } 608 | 609 | [TestMethod] 610 | public void Test_Can_Read_psrep() 611 | { 612 | Test_Extension(".psrep"); 613 | } 614 | 615 | [TestMethod] 616 | public void Test_Can_Read_quest() 617 | { 618 | Test_Extension(".quest"); 619 | } 620 | 621 | [TestMethod] 622 | public void Test_Can_Read_questphase() 623 | { 624 | Test_Extension(".questphase"); 625 | } 626 | 627 | [TestMethod] 628 | public void Test_Can_Read_regionset() 629 | { 630 | Test_Extension(".regionset"); 631 | } 632 | 633 | [TestMethod] 634 | public void Test_Can_Read_remt() 635 | { 636 | Test_Extension(".remt"); 637 | } 638 | 639 | [TestMethod] 640 | public void Test_Can_Read_reslist() 641 | { 642 | Test_Extension(".reslist"); 643 | } 644 | 645 | [TestMethod] 646 | public void Test_Can_Read_rig() 647 | { 648 | Test_Extension(".rig"); 649 | } 650 | 651 | [TestMethod] 652 | public void Test_Can_Read_scene() 653 | { 654 | Test_Extension(".scene"); 655 | } 656 | 657 | [TestMethod] 658 | public void Test_Can_Read_scenerid() 659 | { 660 | Test_Extension(".scenerid"); 661 | } 662 | 663 | [TestMethod] 664 | public void Test_Can_Read_scenesversions() 665 | { 666 | Test_Extension(".scenesversions"); 667 | } 668 | 669 | [TestMethod] 670 | public void Test_Can_Read_smartobject() 671 | { 672 | Test_Extension(".smartobject"); 673 | } 674 | 675 | [TestMethod] 676 | public void Test_Can_Read_smartobjects() 677 | { 678 | Test_Extension(".smartobjects"); 679 | } 680 | 681 | [TestMethod] 682 | public void Test_Can_Read_sp() 683 | { 684 | Test_Extension(".sp"); 685 | } 686 | 687 | [TestMethod] 688 | public void Test_Can_Read_spatial_representation() 689 | { 690 | Test_Extension(".spatial_representation"); 691 | } 692 | 693 | [TestMethod] 694 | public void Test_Can_Read_streamingquerydata() 695 | { 696 | Test_Extension(".streamingquerydata"); 697 | } 698 | 699 | [TestMethod] 700 | public void Test_Can_Read_streamingsector() 701 | { 702 | Test_Extension(".streamingsector"); 703 | } 704 | 705 | [TestMethod] 706 | public void Test_Can_Read_streamingsector_inplace() 707 | { 708 | Test_Extension(".streamingsector_inplace"); 709 | } 710 | 711 | [TestMethod] 712 | public void Test_Can_Read_streamingworld() 713 | { 714 | Test_Extension(".streamingworld"); 715 | } 716 | 717 | [TestMethod] 718 | public void Test_Can_Read_terrainsetup() 719 | { 720 | Test_Extension(".terrainsetup"); 721 | } 722 | 723 | [TestMethod] 724 | public void Test_Can_Read_texarray() 725 | { 726 | Test_Extension(".texarray"); 727 | } 728 | 729 | [TestMethod] 730 | public void Test_Can_Read_traffic_collisions() 731 | { 732 | Test_Extension(".traffic_collisions"); 733 | } 734 | 735 | [TestMethod] 736 | public void Test_Can_Read_traffic_persistent() 737 | { 738 | Test_Extension(".traffic_persistent"); 739 | } 740 | 741 | [TestMethod] 742 | public void Test_Can_Read_voicetags() 743 | { 744 | Test_Extension(".voicetags"); 745 | } 746 | 747 | [TestMethod] 748 | public void Test_Can_Read_w2mesh() 749 | { 750 | Test_Extension(".w2mesh"); 751 | } 752 | 753 | [TestMethod] 754 | public void Test_Can_Read_w2mi() 755 | { 756 | Test_Extension(".w2mi"); 757 | } 758 | 759 | [TestMethod] 760 | public void Test_Can_Read_wem() 761 | { 762 | Test_Extension(".wem"); 763 | } 764 | 765 | [TestMethod] 766 | public void Test_Can_Read_workspot() 767 | { 768 | Test_Extension(".workspot"); 769 | } 770 | 771 | [TestMethod] 772 | public void Test_Can_Read_xbm() 773 | { 774 | Test_Extension(".xbm"); 775 | } 776 | 777 | [TestMethod] 778 | public void Test_Can_Read_xcube() 779 | { 780 | Test_Extension(".xcube"); 781 | } 782 | 783 | #endregion 784 | 785 | private void Test_Extension(string extension = null) 786 | { 787 | var resultDir = Path.Combine(Environment.CurrentDirectory, TestResultsDirectory); 788 | Directory.CreateDirectory(resultDir); 789 | 790 | var success = true; 791 | 792 | List files; 793 | 794 | if (string.IsNullOrEmpty(extension)) 795 | files = GroupedFiles.Keys.ToList(); 796 | else 797 | files = GroupedFiles.Keys.Where(k => k.Equals(extension)).ToList(); 798 | 799 | var sb = new StringBuilder(); 800 | 801 | Parallel.ForEach(files, ext => 802 | { 803 | var results = Read_Archive_Items(GroupedFiles[ext]); 804 | 805 | var successCount = results.Count(r => r.Success); 806 | var totalCount = GroupedFiles[ext].Count; 807 | 808 | sb.AppendLine($"{ext} -> Successful Reads: {successCount} / {totalCount} ({(int)(((double)successCount / (double)totalCount) * 100)}%)"); 809 | 810 | if (success && results.Any(r => !r.Success)) success = false; 811 | 812 | if (WriteToFile) 813 | { 814 | if (results.Any(r => !r.Success)) 815 | { 816 | var resultPath = Path.Combine(resultDir, $"{ext[1..]}.csv"); 817 | var csv = TestResultAsCsv(results.Where(r => !r.Success)); 818 | File.WriteAllText(resultPath, csv); 819 | } 820 | } 821 | }); 822 | 823 | var logPath = Path.Combine(resultDir, $"logfile_{(string.IsNullOrEmpty(extension) ? string.Empty : $"{extension[1..]}_")}{DateTime.Now:yyyyMMddHHmmss}.log"); 824 | File.WriteAllText(logPath, sb.ToString()); 825 | Console.WriteLine(sb.ToString()); 826 | 827 | if (!success) Assert.Fail(); 828 | } 829 | 830 | private IEnumerable Read_Archive_Items(List files) 831 | { 832 | var results = new List(); 833 | 834 | foreach (var file in files) 835 | try 836 | { 837 | var (fileBytes, bufferBytes) = (file.Archive as Archive).GetFileData(file.NameHash64, false); 838 | 839 | using var ms = new MemoryStream(fileBytes); 840 | using var br = new BinaryReader(ms); 841 | 842 | var c = new CR2WFile(); 843 | var readResult = c.Read(br); 844 | 845 | switch (readResult) 846 | { 847 | case EFileReadErrorCodes.NoCr2w: 848 | results.Add(new TestResult 849 | { 850 | ArchiveItem = file, 851 | Success = true, 852 | Result = TestResult.ResultType.NoCr2W 853 | }); 854 | break; 855 | case EFileReadErrorCodes.UnsupportedVersion: 856 | results.Add(new TestResult 857 | { 858 | ArchiveItem = file, 859 | Success = false, 860 | Result = TestResult.ResultType.UnsupportedVersion, 861 | Message = $"Unsupported Version ({c.GetFileHeader().version})" 862 | }); 863 | break; 864 | default: 865 | var hasAdditionalBytes = c.additionalCr2WFileBytes != null && c.additionalCr2WFileBytes.Any(); 866 | results.Add(new TestResult 867 | { 868 | ArchiveItem = file, 869 | Success = !hasAdditionalBytes, 870 | Result = hasAdditionalBytes ? TestResult.ResultType.HasAdditionalBytes : TestResult.ResultType.NoError, 871 | Message = hasAdditionalBytes ? $"Additional Bytes: {c.additionalCr2WFileBytes.Length}" : null, 872 | AdditionalBytes = hasAdditionalBytes ? c.additionalCr2WFileBytes.Length : 0 873 | }); 874 | break; 875 | } 876 | } 877 | catch (Exception e) 878 | { 879 | results.Add(new TestResult 880 | { 881 | ArchiveItem = file, 882 | Success = false, 883 | Result = TestResult.ResultType.RuntimeException, 884 | ExceptionType = e.GetType(), 885 | Message = e.Message 886 | }); 887 | } 888 | 889 | return results; 890 | } 891 | 892 | private string TestResultAsCsv(IEnumerable results) 893 | { 894 | var sb = new StringBuilder(); 895 | 896 | sb.AppendLine( 897 | $@"{nameof(ArchiveItem.NameHash64)},{nameof(ArchiveItem.FileName)},{nameof(TestResult.Result)},{nameof(TestResult.Success)},{nameof(TestResult.AdditionalBytes)},{nameof(TestResult.ExceptionType)},{nameof(TestResult.Message)}"); 898 | 899 | foreach (var r in results) 900 | { 901 | sb.AppendLine( 902 | $"{r.ArchiveItem.NameHash64},{r.ArchiveItem.FileName},{r.Result},{r.Success},{r.AdditionalBytes},{r.ExceptionType?.FullName},{r.Message}"); 903 | } 904 | 905 | return sb.ToString(); 906 | } 907 | 908 | private class TestResult 909 | { 910 | public enum ResultType 911 | { 912 | NoCr2W, 913 | UnsupportedVersion, 914 | RuntimeException, 915 | HasAdditionalBytes, 916 | NoError 917 | } 918 | 919 | public ArchiveItem ArchiveItem { get; set; } 920 | 921 | [JsonConverter(typeof(StringEnumConverter))] 922 | public ResultType Result { get; set; } 923 | public int AdditionalBytes { get; set; } 924 | public bool Success { get; set; } 925 | public Type ExceptionType { get; set; } 926 | public string Message { get; set; } 927 | } 928 | } 929 | } --------------------------------------------------------------------------------