diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f3a6862f7..afdb1360b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -71,6 +71,7 @@ jobs: - GBX.NET.NewtonsoftJson - GBX.NET.Tool - GBX.NET.Tool.CLI + - GBX.NET.Crypto name: Publish ${{ matrix.lib }} runs-on: ubuntu-latest diff --git a/Benchmarks/GBX.NET.Benchmarks/GBX.NET.Benchmarks.csproj b/Benchmarks/GBX.NET.Benchmarks/GBX.NET.Benchmarks.csproj index 668fc8e5c..21b591d01 100644 --- a/Benchmarks/GBX.NET.Benchmarks/GBX.NET.Benchmarks.csproj +++ b/Benchmarks/GBX.NET.Benchmarks/GBX.NET.Benchmarks.csproj @@ -14,7 +14,7 @@ - + diff --git a/GBX.NET.sln b/GBX.NET.sln index 82fca23fc..c585e343b 100644 --- a/GBX.NET.sln +++ b/GBX.NET.sln @@ -162,7 +162,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InputExtract", "Samples\Too EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InputExtractCLI", "Samples\Tool\InputExtractCLI\InputExtractCLI.csproj", "{5B124FAF-2658-49E0-9ABD-0333E829A5A5}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GBX.NET.NewtonsoftJson", "Src\GBX.NET.NewtonsoftJson\GBX.NET.NewtonsoftJson.csproj", "{B4C2F12F-54EA-4844-8620-917BE0303687}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GBX.NET.NewtonsoftJson", "Src\GBX.NET.NewtonsoftJson\GBX.NET.NewtonsoftJson.csproj", "{B4C2F12F-54EA-4844-8620-917BE0303687}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GBX.NET.Tool.Razor", "Src\GBX.NET.Tool.Razor\GBX.NET.Tool.Razor.csproj", "{76A1C1C4-8012-42C8-BB72-B139C9B2963F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CryCryptSwitch", "Tools\CryCryptSwitch\CryCryptSwitch.csproj", "{4327AB87-DF45-4364-9D27-1C72BA0E78FF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GBX.NET.BlockInfo", "Src\GBX.NET.BlockInfo\GBX.NET.BlockInfo.csproj", "{89F6FFE5-E32A-4713-8D63-98D070A17DC1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MuxCryptSwitch", "Tools\MuxCryptSwitch\MuxCryptSwitch.csproj", "{0B5609C6-7B10-4E8D-B41C-35CC1A2532A4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -382,6 +390,22 @@ Global {B4C2F12F-54EA-4844-8620-917BE0303687}.Debug|Any CPU.Build.0 = Debug|Any CPU {B4C2F12F-54EA-4844-8620-917BE0303687}.Release|Any CPU.ActiveCfg = Release|Any CPU {B4C2F12F-54EA-4844-8620-917BE0303687}.Release|Any CPU.Build.0 = Release|Any CPU + {76A1C1C4-8012-42C8-BB72-B139C9B2963F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {76A1C1C4-8012-42C8-BB72-B139C9B2963F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {76A1C1C4-8012-42C8-BB72-B139C9B2963F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {76A1C1C4-8012-42C8-BB72-B139C9B2963F}.Release|Any CPU.Build.0 = Release|Any CPU + {4327AB87-DF45-4364-9D27-1C72BA0E78FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4327AB87-DF45-4364-9D27-1C72BA0E78FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4327AB87-DF45-4364-9D27-1C72BA0E78FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4327AB87-DF45-4364-9D27-1C72BA0E78FF}.Release|Any CPU.Build.0 = Release|Any CPU + {89F6FFE5-E32A-4713-8D63-98D070A17DC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {89F6FFE5-E32A-4713-8D63-98D070A17DC1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {89F6FFE5-E32A-4713-8D63-98D070A17DC1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {89F6FFE5-E32A-4713-8D63-98D070A17DC1}.Release|Any CPU.Build.0 = Release|Any CPU + {0B5609C6-7B10-4E8D-B41C-35CC1A2532A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0B5609C6-7B10-4E8D-B41C-35CC1A2532A4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0B5609C6-7B10-4E8D-B41C-35CC1A2532A4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0B5609C6-7B10-4E8D-B41C-35CC1A2532A4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -446,6 +470,10 @@ Global {72B2091A-1162-4993-9D3D-278E229F6A43} = {0269B7F3-C346-49B3-A9B3-B33ABC95AB5E} {5B124FAF-2658-49E0-9ABD-0333E829A5A5} = {0269B7F3-C346-49B3-A9B3-B33ABC95AB5E} {B4C2F12F-54EA-4844-8620-917BE0303687} = {80DCE6B7-4BD9-415C-B053-92B059D7C938} + {76A1C1C4-8012-42C8-BB72-B139C9B2963F} = {80DCE6B7-4BD9-415C-B053-92B059D7C938} + {4327AB87-DF45-4364-9D27-1C72BA0E78FF} = {F3336145-FDA9-4517-AEDC-7F4C4D526ECB} + {89F6FFE5-E32A-4713-8D63-98D070A17DC1} = {80DCE6B7-4BD9-415C-B053-92B059D7C938} + {0B5609C6-7B10-4E8D-B41C-35CC1A2532A4} = {F3336145-FDA9-4517-AEDC-7F4C4D526ECB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8EA2F0DE-BA72-486D-AB3A-9320C0CE5CFD} diff --git a/Generators/GBX.NET.Generators/GBX.NET.Generators.csproj b/Generators/GBX.NET.Generators/GBX.NET.Generators.csproj index a84df57a8..76f5fecaf 100644 --- a/Generators/GBX.NET.Generators/GBX.NET.Generators.csproj +++ b/Generators/GBX.NET.Generators/GBX.NET.Generators.csproj @@ -26,7 +26,7 @@ - + diff --git a/Generators/GBX.NET.Lua.Generators/GBX.NET.Lua.Generators.csproj b/Generators/GBX.NET.Lua.Generators/GBX.NET.Lua.Generators.csproj index 9c138ce03..cfaf2f7d9 100644 --- a/Generators/GBX.NET.Lua.Generators/GBX.NET.Lua.Generators.csproj +++ b/Generators/GBX.NET.Lua.Generators/GBX.NET.Lua.Generators.csproj @@ -11,7 +11,7 @@ - + diff --git a/Generators/GBX.NET.Tests.Generators/GBX.NET.Tests.Generators.csproj b/Generators/GBX.NET.Tests.Generators/GBX.NET.Tests.Generators.csproj index 364ac4a8c..1bda878eb 100644 --- a/Generators/GBX.NET.Tests.Generators/GBX.NET.Tests.Generators.csproj +++ b/Generators/GBX.NET.Tests.Generators/GBX.NET.Tests.Generators.csproj @@ -11,7 +11,7 @@ - + diff --git a/Resources/Wrap.txt b/Resources/Wrap.txt index 5f05c5e87..97864eb6c 100644 --- a/Resources/Wrap.txt +++ b/Resources/Wrap.txt @@ -3,7 +3,6 @@ 0301A000 2E001000 0301C000 2E002000 03031000 090F4000 -0303C000 0915D000 03074000 2E005000 0307B000 03078000 030FD000 2E004000 diff --git a/SUPPORTED_GBX_FILE_TYPES.md b/SUPPORTED_GBX_FILE_TYPES.md index ac3b89ff3..fcc973d4b 100644 --- a/SUPPORTED_GBX_FILE_TYPES.md +++ b/SUPPORTED_GBX_FILE_TYPES.md @@ -21,7 +21,9 @@ Older extensions | Latest extension | Class | Read (whole) | Write | Read (heade | ConstructionCampaign.Gbx | Campaign.Gbx | [CGameCtnCampaign](Src/GBX.NET/Engines/Game/CGameCtnCampaign.chunkl) | Yes | Yes | TMCollection.Gbx | Collection.Gbx | [CGameCtnCollection](Src/GBX.NET/Engines/Game/CGameCtnCollection.chunkl) | Yes | Yes | Yes | TMDecoration.Gbx | Decoration.Gbx | [CGameCtnDecoration](Src/GBX.NET/Engines/Game/CGameCtnDecoration.chunkl) | Yes | Yes | Yes -| TMDecorationSize.Gbx | DecorationSize.Gbx | [CGameCtnDecorationSize](Src/GBX.NET/Engines/Game/CGameCtnDecorationSize.chunkl) | Yes | Yes +| TMDecorationSize.Gbx | DecorationSize.Gbx | [CGameCtnDecorationSize](Src/GBX.NET/Engines/Game/CGameCtnDecorationSize.chunkl) | Yes | Yes | Yes +| TMDecorationMood.Gbx | DecorationMood.Gbx | [CGameCtnDecorationMood](Src/GBX.NET/Engines/Game/CGameCtnDecorationMood.chunkl) | Yes | Yes | Yes +| TMDecorationAudio.Gbx | DecorationAudio.Gbx | [CGameCtnDecorationAudio](Src/GBX.NET/Engines/Game/CGameCtnDecorationAudio.chunkl) | Yes | Yes | Yes | TMEDClassic.Gbx | EDClassic.Gbx | [CGameCtnBlockInfoClassic](Src/GBX.NET/Engines/Game/CGameCtnBlockInfoClassic.chunkl) | Yes | Yes | Yes | TMEDClip.Gbx | EDClip.Gbx | [CGameCtnBlockInfoClip](Src/GBX.NET/Engines/Game/CGameCtnBlockInfoClip.chunkl) | Yes | Yes | Yes | TMEDFlat.Gbx | EDFlat.Gbx | [CGameCtnBlockInfoFlat](Src/GBX.NET/Engines/Game/CGameCtnBlockInfoFlat.chunkl) | Yes | Yes | Yes @@ -30,6 +32,13 @@ Older extensions | Latest extension | Class | Read (whole) | Write | Read (heade | TMEDRectAsym.Gbx | EDRectAsym.Gbx | [CGameCtnBlockInfoRectAsym](Src/GBX.NET/Engines/Game/CGameCtnBlockInfoRectAsym.chunkl) | Yes | Yes | Yes | TMEDRoad.Gbx | EDRoad.Gbx | [CGameCtnBlockInfoRoad](Src/GBX.NET/Engines/Game/CGameCtnBlockInfoRoad.chunkl) | Yes | Yes | Yes | | EDTransition.Gbx | [CGameCtnBlockInfoTransition](Src/GBX.NET/Engines/Game/CGameCtnBlockInfoTransition.chunkl) | Yes | Yes | Yes +| TMZoneFlat.Gbx | ZoneFlat.Gbx | [CGameCtnZoneFlat](Src/GBX.NET/Engines/Game/CGameCtnZoneFlat.chunkl) | Yes | Yes +| TMZoneFrontier.Gbx | ZoneFrontier.Gbx | [CGameCtnZoneFrontier](Src/GBX.NET/Engines/Game/CGameCtnZoneFrontier.chunkl) | Yes | Yes +| | FuncShader.Gbx | [CFuncShaderLayerUV](Src/GBX.NET/Engines/Func/CFuncShaderLayerUV.chunkl) | Yes | Yes +| | TMTerrainModifier.Gbx | [CGameCtnDecorationTerrainModifier](Src/GBX.NET/Engines/Game/CGameCtnDecorationTerrainModifier.chunkl) | Yes | Yes +| | MotionManagerWeathers.Gbx | [CPlugWeatherModel](Src/GBX.NET/Engines/Plug/CPlugWeatherModel.chunkl) | Up to TMUF | Yes +| | RallyLeafManager.Gbx | [CMotionManagerLeaves](Src/GBX.NET/Engines/Motion/CMotionManagerLeaves.chunkl) | Yes | Yes +| | GameSkin.Gbx | [CPlugGameSkin](Src/GBX.NET/Engines/Plug/CPlugGameSkin.chunkl) | Yes | Yes | | VehicleTunings.Gbx | [CPlugVehiclePhyTunings](Src/GBX.NET/Engines/Plug/CPlugVehiclePhyTunings.chunkl) | Up to TM2 | Up to TM2 | Yes | | ObjectInfo.Gbx | [CGameItemModel](Src/GBX.NET/Engines/GameData/CGameItemModel.chunkl) | Yes | Yes | Yes | | Mobil.Gbx | [CSceneMobil](Src/GBX.NET/Engines/Scene/CSceneMobil.chunkl) | Yes | Yes diff --git a/Samples/Tool/GhostExtract/GhostExtractTool.cs b/Samples/Tool/GhostExtract/GhostExtractTool.cs index 5fcae439e..c0c5daddd 100644 --- a/Samples/Tool/GhostExtract/GhostExtractTool.cs +++ b/Samples/Tool/GhostExtract/GhostExtractTool.cs @@ -34,11 +34,13 @@ public IEnumerable> Produce() { logger.LogInformation("Extracting ghost {GhostIndex} from {FileName}...", i + 1, fileName); + var ghostFileName = GbxPath.GetValidFileName($"{GbxPath.GetFileNameWithoutExtension(fileName ?? "Ghost")}_{i + 1:00}.Ghost.Gbx"); + return new Gbx(ghost, gbx.Header.Basic) { FilePath = ghost.CanBeGameVersion(GameVersion.MP3) - ? Path.Combine("Replays", "GbxTools", "GhostExtract", $"{GbxPath.GetFileNameWithoutExtension(fileName ?? "Ghost")}_{i + 1:00}.Ghost.Gbx") - : Path.Combine("Tracks", "Replays", "GbxTools", "GhostExtract", $"{GbxPath.GetFileNameWithoutExtension(fileName ?? "Ghost")}_{i + 1:00}.Ghost.Gbx"), + ? Path.Combine("Replays", "GbxTools", "GhostExtract", ghostFileName) + : Path.Combine("Tracks", "Replays", "GbxTools", "GhostExtract", ghostFileName), ClassIdRemapMode = gbx.ClassIdRemapMode, PackDescVersion = gbx.PackDescVersion, }; diff --git a/Src/GBX.NET.BlockInfo/Class1.cs b/Src/GBX.NET.BlockInfo/Class1.cs new file mode 100644 index 000000000..4a750e587 --- /dev/null +++ b/Src/GBX.NET.BlockInfo/Class1.cs @@ -0,0 +1,6 @@ +namespace GBX.NET.BlockInfo; + +public class Class1 +{ + +} diff --git a/Src/GBX.NET.BlockInfo/GBX.NET.BlockInfo.csproj b/Src/GBX.NET.BlockInfo/GBX.NET.BlockInfo.csproj new file mode 100644 index 000000000..fa71b7ae6 --- /dev/null +++ b/Src/GBX.NET.BlockInfo/GBX.NET.BlockInfo.csproj @@ -0,0 +1,9 @@ + + + + net8.0 + enable + enable + + + diff --git a/Src/GBX.NET.Crypto/Class1.cs b/Src/GBX.NET.Crypto/Class1.cs deleted file mode 100644 index 741c14a4d..000000000 --- a/Src/GBX.NET.Crypto/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace GBX.NET.Crypto; - -public class Class1 -{ - -} diff --git a/Src/GBX.NET.Crypto/Cry.cs b/Src/GBX.NET.Crypto/Cry.cs new file mode 100644 index 000000000..b9f336734 --- /dev/null +++ b/Src/GBX.NET.Crypto/Cry.cs @@ -0,0 +1,97 @@ +using GBX.NET.Exceptions; +using GBX.NET.Serialization; +using System.Text; + +namespace GBX.NET.Crypto; + +public static partial class Cry +{ + private const ulong Key = 0xCF08317C90460052; + + [Zomp.SyncMethodGenerator.CreateSyncVersion] + public static async Task DecryptAsync(Stream stream, CancellationToken cancellationToken = default) + { + if (Gbx.LZO is null) + { + throw new LzoNotDefinedException(); + } + + using var r = new GbxReader(stream); + var uncompressedSize = r.ReadInt32(); + var compressedData = await r.ReadDataAsync(cancellationToken); + + var uncompressedData = new byte[uncompressedSize]; + + Gbx.LZO.Decompress(compressedData, uncompressedData); + + var shift = uncompressedSize & 0x3F; + var rotkey = (Key << shift) | (Key >> (64 - shift)); + + var rotkeyBytes = BitConverter.GetBytes(rotkey); + + for (int i = 0; i < uncompressedSize; i++) + { + uncompressedData[i] ^= rotkeyBytes[i & 0x7]; + } + + return Encoding.ASCII.GetString(uncompressedData); + } + + public static async Task DecryptAsync(string fileName, CancellationToken cancellationToken = default) + { +#if !NETSTANDARD2_0 + await +#endif + using var fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync: true); + return await DecryptAsync(fs, cancellationToken); + } + + public static string Decrypt(string fileName) + { + using var fs = File.OpenRead(fileName); + return Decrypt(fs); + } + + [Zomp.SyncMethodGenerator.CreateSyncVersion] + public static async Task EncryptAsync(Stream stream, string contents, CancellationToken cancellationToken = default) + { + if (Gbx.LZO is null) + { + throw new LzoNotDefinedException(); + } + + var uncompressedData = Encoding.ASCII.GetBytes(contents); + + var shift = uncompressedData.Length & 0x3F; + var rotkey = (Key << shift) | (Key >> (64 - shift)); + + var rotkeyBytes = BitConverter.GetBytes(rotkey); + + for (int i = 0; i < uncompressedData.Length; i++) + { + uncompressedData[i] ^= rotkeyBytes[i & 0x7]; + } + + using var w = new GbxWriter(stream); + + w.Write(uncompressedData.Length); + + var compressedData = Gbx.LZO.Compress(uncompressedData); + await w.WriteDataAsync(compressedData, cancellationToken); + } + + public static async Task EncryptAsync(string fileName, string contents, CancellationToken cancellationToken = default) + { +#if !NETSTANDARD2_0 + await +#endif + using var fs = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: true); + await EncryptAsync(fs, contents, cancellationToken); + } + + public static void Encrypt(string fileName, string contents) + { + using var fs = File.Create(fileName); + Encrypt(fs, contents); + } +} diff --git a/Src/GBX.NET.Crypto/GBX.NET.Crypto.csproj b/Src/GBX.NET.Crypto/GBX.NET.Crypto.csproj index bf7aa605c..3ada23a6d 100644 --- a/Src/GBX.NET.Crypto/GBX.NET.Crypto.csproj +++ b/Src/GBX.NET.Crypto/GBX.NET.Crypto.csproj @@ -2,9 +2,9 @@ GBX.NET.Crypto - 1.0.0-alpha1 + 1.0.0 BigBang1112 - + Cryptographic features for GBX.NET 2. Copyright (c) 2024 Petr Pivoňka https://github.com/BigBang1112/gbx-net logo_icon_outline.png @@ -39,11 +39,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -51,9 +51,14 @@ all runtime; build; native; contentfiles; analyzers + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/Src/GBX.NET.Crypto/MD5.cs b/Src/GBX.NET.Crypto/MD5.cs new file mode 100644 index 000000000..4bbe8abd4 --- /dev/null +++ b/Src/GBX.NET.Crypto/MD5.cs @@ -0,0 +1,45 @@ +using System.Text; + +namespace GBX.NET.Crypto; + +public static partial class MD5 +{ + public static byte[] Compute(byte[] data) + { +#if NET6_0_OR_GREATER + return System.Security.Cryptography.MD5.HashData(data); +#else + using var md5 = System.Security.Cryptography.MD5.Create(); + return md5.ComputeHash(data); +#endif + } + + public static byte[] Compute(string data) + { + return Compute(Encoding.ASCII.GetBytes(data)); + } + +#if NET8_0_OR_GREATER + public static async ValueTask ComputeAsync(byte[] data, CancellationToken cancellationToken = default) + { + await using var ms = new MemoryStream(data); + return await System.Security.Cryptography.MD5.HashDataAsync(ms, cancellationToken); + } +#elif NET6_0_OR_GREATER || NETSTANDARD2_0 + public static async Task ComputeAsync(byte[] data, CancellationToken cancellationToken = default) + { + using var md5 = System.Security.Cryptography.MD5.Create(); +#if NET6_0_OR_GREATER + await using var ms = new MemoryStream(data); + return await md5.ComputeHashAsync(ms, cancellationToken); +#else + return await Task.FromResult(md5.ComputeHash(data)); +#endif + } +#endif + + public static async Task ComputeAsync(string data, CancellationToken cancellationToken = default) + { + return await ComputeAsync(Encoding.ASCII.GetBytes(data), cancellationToken); + } +} diff --git a/Src/GBX.NET.Crypto/Mux.cs b/Src/GBX.NET.Crypto/Mux.cs new file mode 100644 index 000000000..71237d989 --- /dev/null +++ b/Src/GBX.NET.Crypto/Mux.cs @@ -0,0 +1,67 @@ +using GBX.NET.Serialization; +using System.Text; + +namespace GBX.NET.Crypto; + +public sealed partial class Mux +{ + [Zomp.SyncMethodGenerator.CreateSyncVersion] + public static async Task DecryptAsync(Stream stream, CancellationToken cancellationToken = default) + { + using var r = new GbxReader(stream); + + var magic = await r.ReadBytesAsync(9, cancellationToken); + + if (Encoding.ASCII.GetString(magic) != "NadeoFile") + { + throw new InvalidDataException("Invalid magic number."); + } + + var version = r.ReadByte(); + var keySalt = r.ReadInt32(); + + return new MuxStream(stream, keySalt); + } + + public static async Task DecryptAsync(string fileName, CancellationToken cancellationToken = default) + { +#if !NETSTANDARD2_0 + await +#endif + using var fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync: true); + return await DecryptAsync(fs, cancellationToken); + } + + public static MuxStream Decrypt(string fileName) + { + using var fs = File.OpenRead(fileName); + return Decrypt(fs); + } + + [Zomp.SyncMethodGenerator.CreateSyncVersion] + public static async Task EncryptAsync(Stream stream, int keySalt, byte version = 1, CancellationToken cancellationToken = default) + { + using var w = new GbxWriter(stream); + + await w.WriteAsync(Encoding.ASCII.GetBytes("NadeoFile"), cancellationToken); + w.Write(version); + w.Write(keySalt); + + return new MuxStream(stream, keySalt); + } + + public static async Task EncryptAsync(string fileName, int keySalt, byte version = 1, CancellationToken cancellationToken = default) + { +#if !NETSTANDARD2_0 + await +#endif + using var fs = new FileStream(fileName, FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: true); + return await EncryptAsync(fs, keySalt, version, cancellationToken); + } + + public static MuxStream Encrypt(string fileName, int keySalt, byte version = 1) + { + using var fs = File.Create(fileName); + return Encrypt(fs, keySalt, version); + } +} diff --git a/Src/GBX.NET.Crypto/MuxStream.cs b/Src/GBX.NET.Crypto/MuxStream.cs new file mode 100644 index 000000000..ff4c1c92b --- /dev/null +++ b/Src/GBX.NET.Crypto/MuxStream.cs @@ -0,0 +1,98 @@ +using System.Text; + +namespace GBX.NET.Crypto; + +public class MuxStream : Stream +{ + private readonly Stream stream; + private readonly byte[] key; + + private long position; + + public override bool CanRead => stream.CanRead; + public override bool CanSeek => stream.CanSeek; + public override bool CanWrite => stream.CanWrite; + public override long Length => stream.Length; + + public override long Position + { + get => position; + set + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value)); + } + + stream.Position = value; + position = value; + } + } + + public MuxStream(Stream stream, int keySalt) + { + this.stream = stream ?? throw new ArgumentNullException(nameof(stream)); + + if (!BitConverter.IsLittleEndian) + { + throw new PlatformNotSupportedException("Only little-endian systems are supported."); + } + + key = MD5.Compute(BitConverter.GetBytes(keySalt).Concat(Encoding.ASCII.GetBytes("Hello,hack3r!")).ToArray()); + } + + public override void Flush() => stream.Flush(); + + public override int Read(byte[] buffer, int offset, int count) + { + var bytesRead = stream.Read(buffer, offset, count); + Xor(key, buffer, offset, bytesRead); + position += bytesRead; + return bytesRead; + } + + public override long Seek(long offset, SeekOrigin origin) + { + var newPosition = stream.Seek(offset, origin); + position = newPosition; + return position; + } + + public override void SetLength(long value) => stream.SetLength(value); + + public override void Write(byte[] buffer, int offset, int count) + { + Xor(key, buffer, offset, count); + stream.Write(buffer, offset, count); + position += count; + } + + private void Xor(byte[] key, byte[] data, int offset, int length) + { + for (int i = 0; i < length; i++) + { + data[offset + i] ^= GetKeyStreamByte(key, (int)position + i); + } + } + + private static byte GetKeyStreamByte(byte[] key, int pos) + { + return Rol(key[pos % 16], (pos / 17) % 8); + } + + private static byte Rol(byte input, int amount) + { + return (byte)((input << amount) | (input >> (8 - amount))); + } + + // Dispose pattern to properly clean up the base stream. + protected override void Dispose(bool disposing) + { + if (disposing) + { + stream?.Dispose(); + } + + base.Dispose(disposing); + } +} diff --git a/Src/GBX.NET.Crypto/README.md b/Src/GBX.NET.Crypto/README.md index 90030fc61..94439aba3 100644 --- a/Src/GBX.NET.Crypto/README.md +++ b/Src/GBX.NET.Crypto/README.md @@ -1 +1,10 @@ -# GBX.NET.Crypto \ No newline at end of file +# GBX.NET.Crypto + +[![NuGet](https://img.shields.io/nuget/vpre/GBX.NET.Crypto?style=for-the-badge&logo=nuget)](https://www.nuget.org/packages/GBX.NET.Crypto/) +[![Discord](https://img.shields.io/discord/1012862402611642448?style=for-the-badge&logo=discord)](https://discord.gg/tECTQcAWC9) + +Cryptographic features for GBX.NET 2. Adds MUX and CRY algorithms, simplifies MD5 implementation. + +## License + +GBX.NET.Crypto library is MIT Licensed. \ No newline at end of file diff --git a/Src/GBX.NET.Extensions.Hosting/GBX.NET.Extensions.Hosting.csproj b/Src/GBX.NET.Extensions.Hosting/GBX.NET.Extensions.Hosting.csproj index ed6563b93..21053cb80 100644 --- a/Src/GBX.NET.Extensions.Hosting/GBX.NET.Extensions.Hosting.csproj +++ b/Src/GBX.NET.Extensions.Hosting/GBX.NET.Extensions.Hosting.csproj @@ -37,11 +37,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Src/GBX.NET.Hashing/GBX.NET.Hashing.csproj b/Src/GBX.NET.Hashing/GBX.NET.Hashing.csproj index 03f5d6313..b0bfbc3d6 100644 --- a/Src/GBX.NET.Hashing/GBX.NET.Hashing.csproj +++ b/Src/GBX.NET.Hashing/GBX.NET.Hashing.csproj @@ -40,11 +40,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Src/GBX.NET.Imaging.ImageSharp/GBX.NET.Imaging.ImageSharp.csproj b/Src/GBX.NET.Imaging.ImageSharp/GBX.NET.Imaging.ImageSharp.csproj index 3e1603091..7080d5d5b 100644 --- a/Src/GBX.NET.Imaging.ImageSharp/GBX.NET.Imaging.ImageSharp.csproj +++ b/Src/GBX.NET.Imaging.ImageSharp/GBX.NET.Imaging.ImageSharp.csproj @@ -38,11 +38,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Src/GBX.NET.Imaging.SkiaSharp/GBX.NET.Imaging.SkiaSharp.csproj b/Src/GBX.NET.Imaging.SkiaSharp/GBX.NET.Imaging.SkiaSharp.csproj index e78680032..b376bbeed 100644 --- a/Src/GBX.NET.Imaging.SkiaSharp/GBX.NET.Imaging.SkiaSharp.csproj +++ b/Src/GBX.NET.Imaging.SkiaSharp/GBX.NET.Imaging.SkiaSharp.csproj @@ -38,11 +38,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Src/GBX.NET.Imaging/GBX.NET.Imaging.csproj b/Src/GBX.NET.Imaging/GBX.NET.Imaging.csproj index fc4f540bf..15764dd53 100644 --- a/Src/GBX.NET.Imaging/GBX.NET.Imaging.csproj +++ b/Src/GBX.NET.Imaging/GBX.NET.Imaging.csproj @@ -39,12 +39,12 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Src/GBX.NET.Json/GBX.NET.Json.csproj b/Src/GBX.NET.Json/GBX.NET.Json.csproj index e1f180e21..0aa3194d3 100644 --- a/Src/GBX.NET.Json/GBX.NET.Json.csproj +++ b/Src/GBX.NET.Json/GBX.NET.Json.csproj @@ -39,11 +39,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Src/GBX.NET.LZO/GBX.NET.LZO.csproj b/Src/GBX.NET.LZO/GBX.NET.LZO.csproj index 0faf5332b..9f5dc459b 100644 --- a/Src/GBX.NET.LZO/GBX.NET.LZO.csproj +++ b/Src/GBX.NET.LZO/GBX.NET.LZO.csproj @@ -2,7 +2,7 @@ GBX.NET.LZO - 2.1.0 + 2.1.1 BigBang1112 An LZO compression plugin for GBX.NET to allow de/serialization of compressed Gbx bodies. This official implementation uses lzo 2.10 from NativeSharpLzo and minilzo 2.06 port by zzattack. Copyright (c) 2024 Petr Pivoňka @@ -42,11 +42,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Src/GBX.NET.LZO/Lzo.cs b/Src/GBX.NET.LZO/Lzo.cs index e59e1fcef..d25fd528e 100644 --- a/Src/GBX.NET.LZO/Lzo.cs +++ b/Src/GBX.NET.LZO/Lzo.cs @@ -4,9 +4,14 @@ namespace GBX.NET.LZO; public sealed class Lzo : ILzo { + private static readonly object PadLock = new(); + public byte[] Compress(byte[] data) { - return SharpLzo.Lzo.Compress(SharpLzo.CompressionMode.Lzo1x_999, data); + lock (PadLock) + { + return SharpLzo.Lzo.Compress(SharpLzo.CompressionMode.Lzo1x_999, data); + } } public void Decompress(in Span input, byte[] output) diff --git a/Src/GBX.NET.Lua/GBX.NET.Lua.csproj b/Src/GBX.NET.Lua/GBX.NET.Lua.csproj index 3b38d312e..3f5d6552a 100644 --- a/Src/GBX.NET.Lua/GBX.NET.Lua.csproj +++ b/Src/GBX.NET.Lua/GBX.NET.Lua.csproj @@ -40,11 +40,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Src/GBX.NET.NewtonsoftJson/GBX.NET.NewtonsoftJson.csproj b/Src/GBX.NET.NewtonsoftJson/GBX.NET.NewtonsoftJson.csproj index ec8e74d5b..887594e33 100644 --- a/Src/GBX.NET.NewtonsoftJson/GBX.NET.NewtonsoftJson.csproj +++ b/Src/GBX.NET.NewtonsoftJson/GBX.NET.NewtonsoftJson.csproj @@ -41,11 +41,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Src/GBX.NET.PAK/Class1.cs b/Src/GBX.NET.PAK/Class1.cs deleted file mode 100644 index 4913eb32a..000000000 --- a/Src/GBX.NET.PAK/Class1.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace GBX.NET.PAK; - -public class Class1 -{ - -} diff --git a/Src/GBX.NET.PAK/Compatibilty/IsExternalInit.cs b/Src/GBX.NET.PAK/Compatibilty/IsExternalInit.cs new file mode 100644 index 000000000..2f6d9a0f8 --- /dev/null +++ b/Src/GBX.NET.PAK/Compatibilty/IsExternalInit.cs @@ -0,0 +1,13 @@ +#if NETSTANDARD2_0 || NETSTANDARD2_1 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NETCOREAPP3_0 || NETCOREAPP3_1 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 + +using System.ComponentModel; + +namespace System.Runtime.CompilerServices; + +[EditorBrowsable(EditorBrowsableState.Never)] +internal static class IsExternalInit +{ + +} + +#endif \ No newline at end of file diff --git a/Src/GBX.NET.PAK/Compatibilty/MaybeNullWhenAttribute.cs b/Src/GBX.NET.PAK/Compatibilty/MaybeNullWhenAttribute.cs new file mode 100644 index 000000000..b998e7a59 --- /dev/null +++ b/Src/GBX.NET.PAK/Compatibilty/MaybeNullWhenAttribute.cs @@ -0,0 +1,18 @@ +#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 + +namespace System.Diagnostics.CodeAnalysis; + +[AttributeUsage(AttributeTargets.Parameter, Inherited = false)] +public sealed class MaybeNullWhenAttribute : Attribute +{ + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter may be null. + /// + public MaybeNullWhenAttribute(bool returnValue) => ReturnValue = returnValue; + + /// Gets the return value condition. + public bool ReturnValue { get; } +} + +#endif \ No newline at end of file diff --git a/Src/GBX.NET.PAK/Compatibilty/NotNullIfNotNullAttribute.cs b/Src/GBX.NET.PAK/Compatibilty/NotNullIfNotNullAttribute.cs new file mode 100644 index 000000000..dfe65ae71 --- /dev/null +++ b/Src/GBX.NET.PAK/Compatibilty/NotNullIfNotNullAttribute.cs @@ -0,0 +1,19 @@ +#if NETSTANDARD2_0 || NETCOREAPP2_0 || NETCOREAPP2_1 || NETCOREAPP2_2 || NET45 || NET451 || NET452 || NET6 || NET461 || NET462 || NET47 || NET471 || NET472 || NET48 + +namespace System.Diagnostics.CodeAnalysis; + +/// Specifies that the output will be non-null if the named parameter is non-null. +[AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, AllowMultiple = true, Inherited = false)] +public sealed class NotNullIfNotNullAttribute : Attribute +{ + /// Initializes the attribute with the associated parameter name. + /// + /// The associated parameter name. The output will be non-null if the argument to the parameter specified is non-null. + /// + public NotNullIfNotNullAttribute(string parameterName) => ParameterName = parameterName; + + /// Gets the associated parameter name. + public string ParameterName { get; } +} + +#endif diff --git a/Src/GBX.NET.PAK/GBX.NET.PAK.csproj b/Src/GBX.NET.PAK/GBX.NET.PAK.csproj index c1ad69d7e..77bb686fa 100644 --- a/Src/GBX.NET.PAK/GBX.NET.PAK.csproj +++ b/Src/GBX.NET.PAK/GBX.NET.PAK.csproj @@ -39,11 +39,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -51,6 +51,10 @@ all runtime; build; native; contentfiles; analyzers + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/Src/GBX.NET.PAK/Pak.cs b/Src/GBX.NET.PAK/Pak.cs new file mode 100644 index 000000000..41a39c6da --- /dev/null +++ b/Src/GBX.NET.PAK/Pak.cs @@ -0,0 +1,9 @@ +namespace GBX.NET.PAK; + +public sealed class Pak +{ + public static Task ParseAsync(Stream stream, byte[] key, CancellationToken cancellationToken = default) + { + return Task.FromResult(new Pak()); + } +} diff --git a/Src/GBX.NET.PAK/Pak6.cs b/Src/GBX.NET.PAK/Pak6.cs new file mode 100644 index 000000000..e631a859d --- /dev/null +++ b/Src/GBX.NET.PAK/Pak6.cs @@ -0,0 +1,9 @@ +namespace GBX.NET.PAK; + +public sealed class Pak6 +{ + public static Task ParseAsync(Stream stream, byte[] key, CancellationToken cancellationToken = default) + { + return Task.FromResult(new Pak6()); + } +} diff --git a/Src/GBX.NET.PAK/PakList.cs b/Src/GBX.NET.PAK/PakList.cs new file mode 100644 index 000000000..1deb97bcd --- /dev/null +++ b/Src/GBX.NET.PAK/PakList.cs @@ -0,0 +1,124 @@ +using GBX.NET.Serialization; +using System.Collections; +using System.Text; +using GBX.NET.Crypto; + +#if !NETSTANDARD2_0 +using System.Diagnostics.CodeAnalysis; +#endif + +namespace GBX.NET.PAK; + +public sealed partial class PakList : IReadOnlyDictionary +{ + private readonly IReadOnlyDictionary packs; + + private const string NameKeySalt = "6611992868945B0B59536FC3226F3FD0"; + private const string KeyStringKeySalt = "B97C1205648A66E04F86A1B5D5AF9862"; + + public byte Version { get; init; } + public uint CRC32 { get; init; } + public uint Salt { get; init; } + public byte[] Signature { get; init; } + + public IEnumerable Keys => packs.Keys; + public IEnumerable Values => packs.Values; + + public int Count => packs.Count; + + public PakListItem this[string key] => packs[key]; + + public PakList(byte version, uint crc32, uint salt, byte[] signature, IReadOnlyDictionary packs) + { + Version = version; + CRC32 = crc32; + Salt = salt; + Signature = signature; + + this.packs = packs; + } + + [Zomp.SyncMethodGenerator.CreateSyncVersion] + public static async Task ParseAsync(Stream stream, CancellationToken cancellationToken = default) + { + using var r = new GbxReader(stream); + + var version = r.ReadByte(); + var numPacks = r.ReadByte(); + var crc32 = r.ReadUInt32(); + var salt = r.ReadUInt32(); + + var nameKey = await MD5.ComputeAsync(NameKeySalt + salt, cancellationToken); + + var packs = new Dictionary(numPacks); + + for (var i = 0; i < numPacks; i++) + { + var flags = r.ReadByte(); + var nameLength = r.ReadByte(); + var encryptedName = await r.ReadBytesAsync(nameLength, cancellationToken); + var encryptedKeyString = await r.ReadBytesAsync(32, cancellationToken); + + for (var j = 0; j < encryptedName.Length; j++) + { + encryptedName[j] ^= nameKey[j % nameKey.Length]; + } + + var name = Encoding.ASCII.GetString(encryptedName); + + var keyStringKey = await MD5.ComputeAsync(name + salt + KeyStringKeySalt, cancellationToken); + + for (var j = 0; j < encryptedKeyString.Length; j++) + { + encryptedKeyString[j] ^= keyStringKey[j % keyStringKey.Length]; + } + + var key = await MD5.ComputeAsync(Encoding.ASCII.GetString(encryptedKeyString) + "NadeoPak", cancellationToken); + + packs[name] = new PakListItem(key, flags); + } + + var signature = await r.ReadBytesAsync(0x10, cancellationToken); + + return new PakList(version, crc32, salt, signature, packs); + } + + public static async Task ParseAsync(string filePath, CancellationToken cancellationToken = default) + { +#if !NETSTANDARD2_0 + await +#endif + using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, useAsync: true); + return await ParseAsync(fs, cancellationToken); + } + + public static PakList Parse(string filePath) + { + using var fs = File.OpenRead(filePath); + return Parse(fs); + } + + public bool ContainsKey(string key) + { + return packs.ContainsKey(key); + } + +#if NETSTANDARD2_0 + public bool TryGetValue(string key, out PakListItem value) +#else + public bool TryGetValue(string key, [MaybeNullWhen(false)] out PakListItem value) +#endif + { + return packs.TryGetValue(key, out value); + } + + public IEnumerator> GetEnumerator() + { + return packs.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)packs).GetEnumerator(); + } +} \ No newline at end of file diff --git a/Src/GBX.NET.PAK/PakListItem.cs b/Src/GBX.NET.PAK/PakListItem.cs new file mode 100644 index 000000000..c6b292115 --- /dev/null +++ b/Src/GBX.NET.PAK/PakListItem.cs @@ -0,0 +1,9 @@ +namespace GBX.NET.PAK; + +public sealed record PakListItem(byte[] Key, byte Flags) +{ + public override string ToString() + { + return BitConverter.ToString(Key); + } +} diff --git a/Src/GBX.NET.Tool.CLI/ConsoleSettings.cs b/Src/GBX.NET.Tool.CLI/ConsoleSettings.cs index 9fc0fde56..777b406a4 100644 --- a/Src/GBX.NET.Tool.CLI/ConsoleSettings.cs +++ b/Src/GBX.NET.Tool.CLI/ConsoleSettings.cs @@ -11,4 +11,5 @@ public sealed record ConsoleSettings public string? OutputDirPath { get; set; } public string? ConfigName { get; set; } public LogLevel LogLevel { get; set; } = LogLevel.Information; + public bool HidePath { get; set; } } diff --git a/Src/GBX.NET.Tool.CLI/GBX.NET.Tool.CLI.csproj b/Src/GBX.NET.Tool.CLI/GBX.NET.Tool.CLI.csproj index 2d6f00d8e..50f018c43 100644 --- a/Src/GBX.NET.Tool.CLI/GBX.NET.Tool.CLI.csproj +++ b/Src/GBX.NET.Tool.CLI/GBX.NET.Tool.CLI.csproj @@ -2,7 +2,7 @@ GBX.NET.Tool.CLI - 0.1.0 + 0.2.0 BigBang1112 CLI implementation for the GBX.NET tool framework using Spectre.Console. Copyright (c) 2024 Petr Pivoňka @@ -40,11 +40,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -56,6 +56,7 @@ + diff --git a/Src/GBX.NET.Tool.CLI/IntroWriter.cs b/Src/GBX.NET.Tool.CLI/IntroWriter.cs index fe877b44c..7226995fa 100644 --- a/Src/GBX.NET.Tool.CLI/IntroWriter.cs +++ b/Src/GBX.NET.Tool.CLI/IntroWriter.cs @@ -6,7 +6,7 @@ namespace GBX.NET.Tool.CLI; internal static class IntroWriter where T : ITool { - public static async Task WriteIntroAsync(string[] args) + public static async Task WriteIntroAsync(string[] args, ToolSettings toolSettings) { var assembly = typeof(T).Assembly; var assemblyName = assembly.GetName(); @@ -85,10 +85,19 @@ await AnsiConsole.Live(new FigletText(toolName.Substring(0, 1)) } AnsiConsole.MarkupInterpolated($"[grey]Current Directory:[/] "); - AnsiConsole.Write(new TextPath(Environment.CurrentDirectory) - .RootColor(Spectre.Console.Color.Red) - .StemColor(Spectre.Console.Color.Yellow) - .LeafColor(Spectre.Console.Color.Yellow)); + + if (toolSettings.ConsoleSettings.HidePath) + { + AnsiConsole.Write("(hidden)"); + } + else + { + AnsiConsole.Write(new TextPath(Environment.CurrentDirectory) + .RootColor(Spectre.Console.Color.Red) + .StemColor(Spectre.Console.Color.Yellow) + .LeafColor(Spectre.Console.Color.Yellow)); + } + AnsiConsole.WriteLine(); AnsiConsole.MarkupLineInterpolated($"[grey]Privileged:[/] [yellow]{Environment.IsPrivilegedProcess}[/]"); diff --git a/Src/GBX.NET.Tool.CLI/OutputDistributor.cs b/Src/GBX.NET.Tool.CLI/OutputDistributor.cs index 1a37ff6cb..02ad381b6 100644 --- a/Src/GBX.NET.Tool.CLI/OutputDistributor.cs +++ b/Src/GBX.NET.Tool.CLI/OutputDistributor.cs @@ -1,22 +1,31 @@ -namespace GBX.NET.Tool.CLI; +using Microsoft.Extensions.Logging; + +namespace GBX.NET.Tool.CLI; internal sealed class OutputDistributor { - private readonly string runningDir; private readonly ToolSettings toolSettings; - private readonly SpectreConsoleLogger logger; + private readonly ILogger logger; private readonly string outputDir; - public OutputDistributor(string runningDir, ToolSettings toolSettings, SpectreConsoleLogger logger) + public OutputDistributor(string runningDir, ToolSettings toolSettings, ILogger logger) { - this.runningDir = runningDir; this.toolSettings = toolSettings; this.logger = logger; outputDir = string.IsNullOrWhiteSpace(toolSettings.ConsoleSettings.OutputDirPath) ? Path.Combine(runningDir, "Output") : toolSettings.ConsoleSettings.OutputDirPath; + + if (toolSettings.ConsoleSettings.HidePath) + { + logger.LogDebug("Output directory: {OutputDir}", outputDir); + } + else + { + logger.LogInformation("Output directory: {OutputDir}", outputDir); + } } public async Task DistributeOutputsAsync(IEnumerable outputs, CancellationToken cancellationToken) @@ -44,6 +53,8 @@ public async ValueTask DistributeOutputAsync(object? output, CancellationToken c filePath = Path.GetFileName(filePath); } + logger.LogInformation("Saving Gbx ({FilePath})...", filePath); + var finalPath = Path.Combine(outputDir, filePath); var dirPath = Path.GetDirectoryName(finalPath); @@ -57,6 +68,8 @@ public async ValueTask DistributeOutputAsync(object? output, CancellationToken c gbx.Save(fs); } + logger.LogInformation("Gbx ({FilePath}) saved.", filePath); + break; default: throw new NotSupportedException($"Output type '{output.GetType().Name}' is not supported."); diff --git a/Src/GBX.NET.Tool.CLI/SettingsManager.cs b/Src/GBX.NET.Tool.CLI/SettingsManager.cs index 2c6ceb3c3..f8dfdea56 100644 --- a/Src/GBX.NET.Tool.CLI/SettingsManager.cs +++ b/Src/GBX.NET.Tool.CLI/SettingsManager.cs @@ -10,16 +10,24 @@ namespace GBX.NET.Tool.CLI; internal sealed class SettingsManager { - private static readonly JsonSerializerOptions jsonOptions = new() - { - WriteIndented = true - }; - private readonly string runningDir; - - public SettingsManager(string runningDir) + private readonly JsonSerializerContext? jsonContext; + private readonly JsonSerializerOptions jsonOptions; + private readonly YamlDotNet.Serialization.IDeserializer? ymlDeserializer; + private readonly YamlDotNet.Serialization.ISerializer? ymlSerializer; + + public SettingsManager( + string runningDir, + JsonSerializerContext? jsonContext, + JsonSerializerOptions jsonOptions, + YamlDotNet.Serialization.IDeserializer? yamlDeserializer, + YamlDotNet.Serialization.ISerializer? yamlSerializer) { this.runningDir = runningDir; + this.jsonContext = jsonContext; + this.jsonOptions = jsonOptions; + this.ymlDeserializer = yamlDeserializer; + this.ymlSerializer = yamlSerializer; } public async Task GetOrCreateFileAsync( @@ -78,7 +86,7 @@ public async Task GetOrCreateFileAsync( [RequiresDynamicCode(DynamicCodeMessages.JsonSerializeMessage)] [RequiresUnreferencedCode(DynamicCodeMessages.JsonSerializeMessage)] - public async Task PopulateConfigAsync(string configName, Config config, JsonSerializerContext? context, CancellationToken cancellationToken) + public async Task PopulateConfigAsync(string configName, Config config, CancellationToken cancellationToken) { ArgumentException.ThrowIfNullOrWhiteSpace(configName); @@ -86,15 +94,23 @@ public async Task PopulateConfigAsync(string configName, Config config, JsonSeri var existingConfig = default(Config); var configDir = Path.Combine(runningDir, "Config", configName); - var mainConfigFilePath = Path.Combine(configDir, "Config.json"); + var mainConfigFilePath = Path.Combine(configDir, ymlDeserializer is null ? "Config.json" : "Config.yml"); if (File.Exists(mainConfigFilePath)) { await using var fs = new FileStream(mainConfigFilePath, FileMode.Open, FileAccess.Read, FileShare.None, 4096, useAsync: true); + using var reader = new StreamReader(fs); - existingConfig = context is null - ? (Config?)await JsonSerializer.DeserializeAsync(fs, configType, jsonOptions, cancellationToken) - : (Config?)await JsonSerializer.DeserializeAsync(fs, configType, context, cancellationToken: cancellationToken); + if (ymlDeserializer is null) + { + existingConfig = jsonContext is null + ? (Config?)await JsonSerializer.DeserializeAsync(fs, configType, jsonOptions, cancellationToken) + : (Config?)await JsonSerializer.DeserializeAsync(fs, configType, jsonContext, cancellationToken: cancellationToken); + } + else + { + existingConfig = (Config?)ymlDeserializer.Deserialize(reader, configType); + } } else { @@ -112,15 +128,24 @@ public async Task PopulateConfigAsync(string configName, Config config, JsonSeri { var att = prop.GetCustomAttribute()!; - var filePath = Path.Combine(configDir, att.FileName + ".json"); + var filePathWithoutExtension = Path.Combine(configDir, att.FileName); - if (File.Exists(filePath)) + if (File.Exists(filePathWithoutExtension + ".json")) { - await using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None, 4096, useAsync: true); + await using var fs = new FileStream(filePathWithoutExtension + ".json", FileMode.Open, FileAccess.Read, FileShare.None, 4096, useAsync: true); - var value = context is null + var value = jsonContext is null ? await JsonSerializer.DeserializeAsync(fs, prop.PropertyType, jsonOptions, cancellationToken) - : await JsonSerializer.DeserializeAsync(fs, prop.PropertyType, context, cancellationToken: cancellationToken); + : await JsonSerializer.DeserializeAsync(fs, prop.PropertyType, jsonContext, cancellationToken: cancellationToken); + + prop.SetValue(config, value); + } + else if (ymlDeserializer is not null && File.Exists(filePathWithoutExtension + ".yml")) + { + await using var fs = new FileStream(filePathWithoutExtension + ".yml", FileMode.Open, FileAccess.Read, FileShare.None, 4096, useAsync: true); + using var reader = new StreamReader(fs); + + var value = ymlDeserializer.Deserialize(reader, prop.PropertyType); prop.SetValue(config, value); } @@ -129,13 +154,21 @@ public async Task PopulateConfigAsync(string configName, Config config, JsonSeri await using var fsCreate = new FileStream(mainConfigFilePath, FileMode.Create, FileAccess.Write, FileShare.None, 4096, useAsync: true); - if (context is null) + if (ymlSerializer is null) { - await JsonSerializer.SerializeAsync(fsCreate, config, configType, jsonOptions, cancellationToken); + if (jsonContext is null) + { + await JsonSerializer.SerializeAsync(fsCreate, config, configType, jsonOptions, cancellationToken); + } + else + { + await JsonSerializer.SerializeAsync(fsCreate, config, configType, jsonContext, cancellationToken); + } } else { - await JsonSerializer.SerializeAsync(fsCreate, config, configType, context, cancellationToken); + using var writer = new StreamWriter(fsCreate); + ymlSerializer.Serialize(writer, config); } } } diff --git a/Src/GBX.NET.Tool.CLI/ToolConsole.cs b/Src/GBX.NET.Tool.CLI/ToolConsole.cs index f1118496f..b70c0336d 100644 --- a/Src/GBX.NET.Tool.CLI/ToolConsole.cs +++ b/Src/GBX.NET.Tool.CLI/ToolConsole.cs @@ -36,7 +36,11 @@ public ToolConsole(string[] args, HttpClient http, ToolConsoleOptions options) this.options = options ?? throw new ArgumentNullException(nameof(options)); runningDir = AppDomain.CurrentDomain.BaseDirectory; - settingsManager = new SettingsManager(runningDir); + settingsManager = new SettingsManager(runningDir, + options.JsonContext, + options.JsonOptions, + options.YmlDeserializer, + options.YmlSerializer); argsResolver = new ArgsResolver(args, http); } @@ -119,7 +123,7 @@ private async Task RunAsync(CancellationToken cancellationToken) if (!toolSettings.ConsoleSettings.SkipIntro) { - introWriterTask = IntroWriter.WriteIntroAsync(args); + introWriterTask = IntroWriter.WriteIntroAsync(args, toolSettings); } logger.LogTrace("Checking for updates..."); @@ -173,6 +177,7 @@ private async Task RunAsync(CancellationToken cancellationToken) AnsiConsole.WriteLine(); logger.LogInformation("Starting tool instance creation..."); AnsiConsole.WriteLine(); + var counter = 0; await foreach (var toolInstance in toolInstanceMaker.MakeToolInstancesAsync(cancellationToken)) @@ -187,9 +192,9 @@ private async Task RunAsync(CancellationToken cancellationToken) var configName = string.IsNullOrWhiteSpace(toolSettings.ConsoleSettings.ConfigName) ? "Default" : toolSettings.ConsoleSettings.ConfigName; - logger.LogInformation("Populating tool config (name: {ConfigName}, type: {ConfigType})...", configName, typeof(Config)); + logger.LogInformation("Populating tool config (name: {ConfigName}, type: {ConfigType})...", configName, configurable.Config.GetType()); - await settingsManager.PopulateConfigAsync(configName, configurable.Config, options.JsonSerializerContext, cancellationToken); + await settingsManager.PopulateConfigAsync(configName, configurable.Config, cancellationToken); } // Run all produce methods in parallel and run mutate methods in sequence diff --git a/Src/GBX.NET.Tool.CLI/ToolConsoleOptions.cs b/Src/GBX.NET.Tool.CLI/ToolConsoleOptions.cs index 4fdd50e96..d566d8243 100644 --- a/Src/GBX.NET.Tool.CLI/ToolConsoleOptions.cs +++ b/Src/GBX.NET.Tool.CLI/ToolConsoleOptions.cs @@ -1,9 +1,17 @@ -using System.Text.Json.Serialization; +using System.Text.Json; +using System.Text.Json.Serialization; namespace GBX.NET.Tool.CLI; public sealed record ToolConsoleOptions { public string IntroText { get; init; } = string.Empty; - public JsonSerializerContext? JsonSerializerContext { get; init; } + public JsonSerializerContext? JsonContext { get; init; } + public JsonSerializerOptions JsonOptions { get; init; } = new() + { + WriteIndented = true + }; + + public YamlDotNet.Serialization.IDeserializer? YmlDeserializer { get; init; } + public YamlDotNet.Serialization.ISerializer? YmlSerializer { get; init; } } diff --git a/Src/GBX.NET.Tool.CLI/ToolInstanceMaker.cs b/Src/GBX.NET.Tool.CLI/ToolInstanceMaker.cs index 3d4c9a1ba..5ae1c24c7 100644 --- a/Src/GBX.NET.Tool.CLI/ToolInstanceMaker.cs +++ b/Src/GBX.NET.Tool.CLI/ToolInstanceMaker.cs @@ -60,6 +60,7 @@ public async IAsyncEnumerable MakeToolInstancesAsync([EnumeratorCancellation] throw new ConsoleProblemException("Invalid files passed to the tool."); } + logger.LogInformation("Constructor {Constructor} will be used.", pickedCtor); logger.LogInformation("Creating new tool instance..."); // Instantiate the tool diff --git a/Src/GBX.NET.Tool.Razor/ExampleJsInterop.cs b/Src/GBX.NET.Tool.Razor/ExampleJsInterop.cs new file mode 100644 index 000000000..b6553e348 --- /dev/null +++ b/Src/GBX.NET.Tool.Razor/ExampleJsInterop.cs @@ -0,0 +1,35 @@ +using Microsoft.JSInterop; + +namespace GBX.NET.Tool.Razor; +// This class provides an example of how JavaScript functionality can be wrapped +// in a .NET class for easy consumption. The associated JavaScript module is +// loaded on demand when first needed. +// +// This class can be registered as scoped DI service and then injected into Blazor +// components for use. + +public class ExampleJsInterop : IAsyncDisposable +{ + private readonly Lazy> moduleTask; + + public ExampleJsInterop(IJSRuntime jsRuntime) + { + moduleTask = new(() => jsRuntime.InvokeAsync( + "import", "./_content/GBX.NET.Tool.Razor/exampleJsInterop.js").AsTask()); + } + + public async ValueTask Prompt(string message) + { + var module = await moduleTask.Value; + return await module.InvokeAsync("showPrompt", message); + } + + public async ValueTask DisposeAsync() + { + if (moduleTask.IsValueCreated) + { + var module = await moduleTask.Value; + await module.DisposeAsync(); + } + } +} diff --git a/Src/GBX.NET.Tool.Razor/GBX.NET.Tool.Razor.csproj b/Src/GBX.NET.Tool.Razor/GBX.NET.Tool.Razor.csproj new file mode 100644 index 000000000..0069de6bd --- /dev/null +++ b/Src/GBX.NET.Tool.Razor/GBX.NET.Tool.Razor.csproj @@ -0,0 +1,22 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + + + diff --git a/Src/GBX.NET.Tool.Razor/ToolComponent.razor b/Src/GBX.NET.Tool.Razor/ToolComponent.razor new file mode 100644 index 000000000..9e944c71d --- /dev/null +++ b/Src/GBX.NET.Tool.Razor/ToolComponent.razor @@ -0,0 +1,3 @@ +
+ This component is defined in the GBX.NET.Tool.Razor library. +
diff --git a/Src/GBX.NET.Tool.Razor/ToolComponent.razor.css b/Src/GBX.NET.Tool.Razor/ToolComponent.razor.css new file mode 100644 index 000000000..c6afca404 --- /dev/null +++ b/Src/GBX.NET.Tool.Razor/ToolComponent.razor.css @@ -0,0 +1,6 @@ +.my-component { + border: 2px dashed red; + padding: 1em; + margin: 1em 0; + background-image: url('background.png'); +} diff --git a/Src/GBX.NET.Tool.Razor/_Imports.razor b/Src/GBX.NET.Tool.Razor/_Imports.razor new file mode 100644 index 000000000..77285129d --- /dev/null +++ b/Src/GBX.NET.Tool.Razor/_Imports.razor @@ -0,0 +1 @@ +@using Microsoft.AspNetCore.Components.Web diff --git a/Src/GBX.NET.Tool.Razor/wwwroot/background.png b/Src/GBX.NET.Tool.Razor/wwwroot/background.png new file mode 100644 index 000000000..e15a3bde6 Binary files /dev/null and b/Src/GBX.NET.Tool.Razor/wwwroot/background.png differ diff --git a/Src/GBX.NET.Tool.Razor/wwwroot/exampleJsInterop.js b/Src/GBX.NET.Tool.Razor/wwwroot/exampleJsInterop.js new file mode 100644 index 000000000..ea8d76ad2 --- /dev/null +++ b/Src/GBX.NET.Tool.Razor/wwwroot/exampleJsInterop.js @@ -0,0 +1,6 @@ +// This is a JavaScript module that is loaded on demand. It can export any number of +// functions, and may import other JavaScript modules if required. + +export function showPrompt(message) { + return prompt(message, 'Type anything here'); +} diff --git a/Src/GBX.NET.Tool/GBX.NET.Tool.csproj b/Src/GBX.NET.Tool/GBX.NET.Tool.csproj index 964179ed5..5e214b20a 100644 --- a/Src/GBX.NET.Tool/GBX.NET.Tool.csproj +++ b/Src/GBX.NET.Tool/GBX.NET.Tool.csproj @@ -39,11 +39,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Src/GBX.NET.ZLib/GBX.NET.ZLib.csproj b/Src/GBX.NET.ZLib/GBX.NET.ZLib.csproj index ff4105611..8d671eb9d 100644 --- a/Src/GBX.NET.ZLib/GBX.NET.ZLib.csproj +++ b/Src/GBX.NET.ZLib/GBX.NET.ZLib.csproj @@ -40,11 +40,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Src/GBX.NET/Components/GbxRefTable.cs b/Src/GBX.NET/Components/GbxRefTable.cs index f3b5f4ffc..4cae30a80 100644 --- a/Src/GBX.NET/Components/GbxRefTable.cs +++ b/Src/GBX.NET/Components/GbxRefTable.cs @@ -23,19 +23,20 @@ public sealed class GbxRefTable /// public string? FileSystemPath { get; init; } - private string GetFilePath(GbxRefTableFile file) + private string GetFilePath(string filePath) { var ancestor = string.Concat(Enumerable.Repeat(".." + Path.DirectorySeparatorChar, AncestorLevel)); return string.IsNullOrEmpty(FileSystemPath) - ? Path.Combine(ancestor, file.FilePath) - : Path.Combine(FileSystemPath, ancestor, file.FilePath); + ? Path.Combine(ancestor, filePath) + : Path.Combine(FileSystemPath, ancestor, filePath); } - public string GetFullFilePath(GbxRefTableFile file) - { - return Path.GetFullPath(GetFilePath(file)); - } + private string GetFilePath(GbxRefTableFile file) => GetFilePath(file.FilePath); + private string GetFilePath(UnlinkedGbxRefTableFile file) => GetFilePath(file.FilePath); + + public string GetFullFilePath(GbxRefTableFile file) => Path.GetFullPath(GetFilePath(file)); + public string GetFullFilePath(UnlinkedGbxRefTableFile file) => Path.GetFullPath(GetFilePath(file)); public CMwNod? LoadNode(GbxRefTableFile file, GbxReadSettings settings = default, bool exceptions = false) { diff --git a/Src/GBX.NET/Engines/Function/CFuncKeysSkel.chunkl b/Src/GBX.NET/Engines/Function/CFuncKeysSkel.chunkl index 2eb7e17a2..9b96483e9 100644 --- a/Src/GBX.NET/Engines/Function/CFuncKeysSkel.chunkl +++ b/Src/GBX.NET/Engines/Function/CFuncKeysSkel.chunkl @@ -4,4 +4,8 @@ CFuncKeysSkel 0x05006000 0x000 CFuncSkel Skel -0x001 (ignore) \ No newline at end of file +0x001 + +archive Loc + quat + vec3 \ No newline at end of file diff --git a/Src/GBX.NET/Engines/Function/CFuncKeysSkel.cs b/Src/GBX.NET/Engines/Function/CFuncKeysSkel.cs new file mode 100644 index 000000000..97939699e --- /dev/null +++ b/Src/GBX.NET/Engines/Function/CFuncKeysSkel.cs @@ -0,0 +1,54 @@ +namespace GBX.NET.Engines.Function; + +public partial class CFuncKeysSkel +{ + public partial class Chunk05006001 + { + public Loc[][]? U01; + + public override void Read(CFuncKeysSkel n, GbxReader r) + { + var count = r.ReadInt32(); + U01 = new Loc[count][]; + + for (var i = 0; i < count; i++) + { + var count2 = n.skel?.Bones?.Length ?? 0; + U01[i] = new Loc[count2]; + + for (var j = 0; j < count2; j++) + { + var loc = new Loc(); + loc.Read(r); + U01[i][j] = loc; + } + } + } + + public override void Write(CFuncKeysSkel n, GbxWriter w) + { + w.Write(U01?.Length ?? 0); + + if (U01 is null) + { + return; + } + + foreach (var locs in U01) + { + if (locs.Length != n.skel?.Bones?.Length) + { + throw new InvalidOperationException("Invalid bone count."); + } + + foreach (var loc in locs) + { + loc.Write(w); + } + } + } + } + + [ArchiveGenerationOptions(StructureKind = StructureKind.SeparateReadAndWrite)] + public partial class Loc; +} diff --git a/Src/GBX.NET/Engines/Game/CGameCtnCollection.chunkl b/Src/GBX.NET/Engines/Game/CGameCtnCollection.chunkl index c5e5e3fb2..bbd10d461 100644 --- a/Src/GBX.NET/Engines/Game/CGameCtnCollection.chunkl +++ b/Src/GBX.NET/Engines/Game/CGameCtnCollection.chunkl @@ -13,7 +13,7 @@ CGameCtnCollection 0x03033000 v2+ int SortIndex v3+ - id DefaultZone + id DefaultZoneId v4+ ident Vehicle v5+ @@ -42,7 +42,7 @@ CGameCtnCollection 0x03033000 0x009 id Collection CGameCtnZone[]_deprec CompleteListZoneList (external) - int + CGameCtnZone DefaultZone (external) bool NeedUnlock float SquareSize float SquareHeight @@ -320,4 +320,4 @@ archive Water v2+ int? // NodeRef? v7+ - int? // not seen in code \ No newline at end of file + int? // not seen in code diff --git a/Src/GBX.NET/Engines/Game/CGameCtnGhost.cs b/Src/GBX.NET/Engines/Game/CGameCtnGhost.cs index afc79b08a..4fddd5bc5 100644 --- a/Src/GBX.NET/Engines/Game/CGameCtnGhost.cs +++ b/Src/GBX.NET/Engines/Game/CGameCtnGhost.cs @@ -27,8 +27,8 @@ public partial class CGameCtnGhost private string? validate_RaceSettings; public string? Validate_RaceSettings { get => validate_RaceSettings; set => validate_RaceSettings = value; } - private ImmutableArray inputs; - public ImmutableArray Inputs { get => inputs; set => inputs = value; } + private ImmutableList? inputs; + public ImmutableList? Inputs { get => inputs; set => inputs = value; } private bool steeringWheelSensitivity; public bool SteeringWheelSensitivity { get => steeringWheelSensitivity; set => steeringWheelSensitivity = value; } @@ -94,7 +94,7 @@ private void ReadInputs(CGameCtnGhost n, GbxReader r) var numEntries = r.ReadInt32(); U02 = r.ReadInt32(); // CountLimit? - var inputs = ImmutableArray.CreateBuilder(numEntries); + var inputs = ImmutableList.CreateBuilder(); for (var i = 0; i < numEntries; i++) { @@ -112,16 +112,21 @@ private void ReadInputs(CGameCtnGhost n, GbxReader r) private void WriteInputs(CGameCtnGhost n, GbxWriter w) { - var inputNames = n.inputs + var inputNames = n.inputs? .Select(NET.Inputs.Input.GetName) .Distinct() - .ToImmutableList(); + .ToImmutableList() ?? ImmutableList.Empty; w.WriteListId(inputNames); - w.Write(n.inputs.Length); + w.Write(n.inputs?.Count ?? 0); w.Write(U02); + if (n.inputs is null) + { + return; + } + foreach (var input in n.inputs) { w.Write(input.Time.TotalMilliseconds + 100000); @@ -193,4 +198,12 @@ public override void ReadWrite(CGameCtnGhost n, GbxReaderWriter rw) rw.UInt256(ref U01); } } + + public partial class Checkpoint + { + public override string ToString() + { + return $"{Time.ToTmString()} ({(Speed.HasValue ? $"{Speed}km/h, " : "")}{StuntsScore} pts.)"; + } + } } diff --git a/Src/GBX.NET/Engines/Game/CGameCtnReplayRecord.cs b/Src/GBX.NET/Engines/Game/CGameCtnReplayRecord.cs index 6f42e86bb..7cbdf7834 100644 --- a/Src/GBX.NET/Engines/Game/CGameCtnReplayRecord.cs +++ b/Src/GBX.NET/Engines/Game/CGameCtnReplayRecord.cs @@ -116,7 +116,7 @@ public CGameCtnChallenge? Challenge public ImmutableList? SceneryVortexKeys { get; private set; } public int SceneryCapturableCount { get; private set; } public string? PlaygroundScript { get; private set; } - public ImmutableArray InterfaceScriptInfos { get; private set; } + public ImmutableList? InterfaceScriptInfos { get; private set; } /// /// Inputs (keyboard, pad, wheel) of the replay from TM1.0, TMO, Sunrise and ESWC. For inputs stored in TMU, TMUF, TMTurbo and TM2: see in . TM2020 and Shootmania inputs are available in in . Can be null if is 0, which can happen when you save the replay in editor. @@ -534,7 +534,7 @@ public override void Read(CGameCtnReplayRecord n, GbxReader r) throw new ChunkVersionNotSupportedException(Version); } - n.InterfaceScriptInfos = ImmutableArray.Create(r.ReadArrayReadable()); + n.InterfaceScriptInfos = ImmutableList.Create(r.ReadArrayReadable()); } } diff --git a/Src/GBX.NET/Engines/Game/CGameUserFileList.chunkl b/Src/GBX.NET/Engines/Game/CGameUserFileList.chunkl new file mode 100644 index 000000000..41cdb6990 --- /dev/null +++ b/Src/GBX.NET/Engines/Game/CGameUserFileList.chunkl @@ -0,0 +1,11 @@ +CGameUserFileList 0x031B7000 + +0x000 + version + FileInfo[] Files (version: Version) + +archive FileInfo + +enum FileType + Map + Ghost \ No newline at end of file diff --git a/Src/GBX.NET/Engines/Game/CGameUserFileList.cs b/Src/GBX.NET/Engines/Game/CGameUserFileList.cs new file mode 100644 index 000000000..69c6b5863 --- /dev/null +++ b/Src/GBX.NET/Engines/Game/CGameUserFileList.cs @@ -0,0 +1,55 @@ +namespace GBX.NET.Engines.Game; + +public partial class CGameUserFileList +{ + public partial class FileInfo + { + private string name = ""; + private string mapUid = ""; + private string? mapName; + private string? ghostKind; + private FileType type; + + public ulong U01; + public ulong U02; + public int? U04; + + public string Name { get => name; set => name = value; } + public string MapUid { get => mapUid; set => mapUid = value; } + public string? MapName { get => mapName; set => mapName = value; } + public string? GhostKind { get => ghostKind; set => ghostKind = value; } + public FileType Type { get => type; set => type = value; } + + public void ReadWrite(GbxReaderWriter rw, int v = 0) + { + rw.String(ref name!); + rw.Byte(0); + + rw.UInt64(ref U01); + rw.UInt64(ref U02); + rw.EnumInt32(ref type); + + switch (type) + { + case FileType.Map: + rw.Id(ref mapUid!); + rw.String(ref mapName); + rw.Byte(0); + break; + case FileType.Ghost: + rw.String(ref ghostKind); + rw.Byte(0); + rw.Id(ref mapUid!); + rw.Int32(ref U04); + break; + default: + throw new ThisShouldNotHappenException(); + } + } + + public override string ToString() + { + return name ?? "[unknown file]"; + } + } +} diff --git a/Src/GBX.NET/Engines/Graphic/GxFog.chunkl b/Src/GBX.NET/Engines/Graphic/GxFog.chunkl new file mode 100644 index 000000000..ffb0d6a25 --- /dev/null +++ b/Src/GBX.NET/Engines/Graphic/GxFog.chunkl @@ -0,0 +1 @@ +GxFog 0x04004000 \ No newline at end of file diff --git a/Src/GBX.NET/Engines/Graphic/GxFogBlender.chunkl b/Src/GBX.NET/Engines/Graphic/GxFogBlender.chunkl new file mode 100644 index 000000000..c42b24e21 --- /dev/null +++ b/Src/GBX.NET/Engines/Graphic/GxFogBlender.chunkl @@ -0,0 +1,10 @@ +GxFogBlender 0x04008000 + +0x000 + bool + int + list Keys + +archive Key + timefloat Time + GxFog Fog \ No newline at end of file diff --git a/Src/GBX.NET/Engines/Motion/CMotionEmitterLeaves.chunkl b/Src/GBX.NET/Engines/Motion/CMotionEmitterLeaves.chunkl index 5cb3ac76f..71088b3a6 100644 --- a/Src/GBX.NET/Engines/Motion/CMotionEmitterLeaves.chunkl +++ b/Src/GBX.NET/Engines/Motion/CMotionEmitterLeaves.chunkl @@ -1,6 +1,13 @@ CMotionEmitterLeaves 0x0804C000 - inherits: CMotionManaged +0x000 + CMotionManagerLeaves ManagerModel (external) + float + float + float + float + 0x001 CMotionManagerLeaves ManagerModel (external) float diff --git a/Src/GBX.NET/Engines/Motion/CMotionManagerLeaves.chunkl b/Src/GBX.NET/Engines/Motion/CMotionManagerLeaves.chunkl index 053b29c06..55e108d36 100644 --- a/Src/GBX.NET/Engines/Motion/CMotionManagerLeaves.chunkl +++ b/Src/GBX.NET/Engines/Motion/CMotionManagerLeaves.chunkl @@ -1,5 +1,5 @@ CMotionManagerLeaves 0x0804D000 - inherits: CMotionManager -0x000 +0x0804C000 CSceneMobilLeaves MobilLeaves \ No newline at end of file diff --git a/Src/GBX.NET/Engines/Plug/CPlugClouds.chunkl b/Src/GBX.NET/Engines/Plug/CPlugClouds.chunkl new file mode 100644 index 000000000..49043229b --- /dev/null +++ b/Src/GBX.NET/Engines/Plug/CPlugClouds.chunkl @@ -0,0 +1,20 @@ +CPlugClouds 0x09180000 + +0x000 + float BottomNearZ + float BottomFarZ + float + CPlugFileImg ImageColorMin (external) + CPlugFileImg ImageColorMax (external) + int Lighting + +0x002 + vec2[] PointHeights + +0x003 + int HeightCenter + vec2 HeightCenterXZ + +0x004 (base: 0x000) + base + float SpeedScale \ No newline at end of file diff --git a/Src/GBX.NET/Engines/Plug/CPlugCrystal.chunkl b/Src/GBX.NET/Engines/Plug/CPlugCrystal.chunkl index 974eef885..4d18b0abc 100644 --- a/Src/GBX.NET/Engines/Plug/CPlugCrystal.chunkl +++ b/Src/GBX.NET/Engines/Plug/CPlugCrystal.chunkl @@ -174,6 +174,10 @@ archive SpawnPositionLayer (inherits: ModifierLayer, contextual) base int SpawnPositionVersion vec3 SpawnPosition + float HorizontalAngle + float VerticalAngle + if SpawnPositionVersion >= 1 + float RollAngle archive VoxelSpace throw diff --git a/Src/GBX.NET/Engines/Plug/CPlugCrystal.cs b/Src/GBX.NET/Engines/Plug/CPlugCrystal.cs index 04a6f1e75..02845dce5 100644 --- a/Src/GBX.NET/Engines/Plug/CPlugCrystal.cs +++ b/Src/GBX.NET/Engines/Plug/CPlugCrystal.cs @@ -580,6 +580,7 @@ public void Write(GbxWriter w, CPlugCrystal n, int v = 0) if (Version >= 33) { + // this can write 255 in case of -1, which is not correct? w.WriteOptimizedInt(materialIndex, n.Materials.Count); } else diff --git a/Src/GBX.NET/Engines/Plug/CPlugMaterial.chunkl b/Src/GBX.NET/Engines/Plug/CPlugMaterial.chunkl index 175122a49..f2c8df780 100644 --- a/Src/GBX.NET/Engines/Plug/CPlugMaterial.chunkl +++ b/Src/GBX.NET/Engines/Plug/CPlugMaterial.chunkl @@ -15,8 +15,9 @@ CPlugMaterial 0x09079000 0x00D -0x00E - int +0x00E (demonstration) + short SurfaceId + short 0x00F int diff --git a/Src/GBX.NET/Engines/Plug/CPlugMaterial.cs b/Src/GBX.NET/Engines/Plug/CPlugMaterial.cs index 696963a45..56ff3db72 100644 --- a/Src/GBX.NET/Engines/Plug/CPlugMaterial.cs +++ b/Src/GBX.NET/Engines/Plug/CPlugMaterial.cs @@ -4,7 +4,7 @@ public partial class CPlugMaterial { private CPlug? shader; [AppliedWithChunk] - public CPlug? Shader { get => shaderFile?.GetNode(ref shader) ?? shader; set => shader = value; } + public CPlug? Shader { get => shaderFile?.GetNode(ref shader) ?? shader; set => shader = value; } // probably m_Material private Components.GbxRefTableFile? shaderFile; public Components.GbxRefTableFile? ShaderFile { get => shaderFile; set => shaderFile = value; } public CPlug? GetShader(GbxReadSettings settings = default, bool exceptions = false) => shaderFile?.GetNode(ref shader, settings, exceptions) ?? shader; @@ -13,6 +13,9 @@ public partial class CPlugMaterial [AppliedWithChunk] public DeviceMat[]? DeviceMaterials { get => deviceMaterials; set => deviceMaterials = value; } + [AppliedWithChunk] + public CPlugSurface.MaterialId SurfaceId { get; set; } + public partial class Chunk0907900D { public int[]? U01; @@ -21,7 +24,7 @@ public override void ReadWrite(CPlugMaterial n, GbxReaderWriter rw) { rw.NodeRef(ref n.shader, ref n.shaderFile); - if (n.shader is not null && n.shaderFile is not null) + if (n.shader is not null || n.shaderFile is not null) { return; } @@ -31,4 +34,13 @@ public override void ReadWrite(CPlugMaterial n, GbxReaderWriter rw) rw.Array(ref U01); } } + + public partial class Chunk0907900E + { + public override void ReadWrite(CPlugMaterial n, GbxReaderWriter rw) + { + n.SurfaceId = (CPlugSurface.MaterialId)rw.Int16((short)n.SurfaceId); + rw.Int16(ref U01); + } + } } diff --git a/Src/GBX.NET/Engines/Plug/CPlugMaterialCustom.chunkl b/Src/GBX.NET/Engines/Plug/CPlugMaterialCustom.chunkl index d9abcc1ba..796fb00b4 100644 --- a/Src/GBX.NET/Engines/Plug/CPlugMaterialCustom.chunkl +++ b/Src/GBX.NET/Engines/Plug/CPlugMaterialCustom.chunkl @@ -1,2 +1,36 @@ CPlugMaterialCustom 0x0903A000 -- inherits: CPlug \ No newline at end of file +- inherits: CPlug + +0x004 + int[] + +0x006 + Bitmap[] Textures + +0x00A + GpuFx[] GpuFxs1 + GpuFx[] GpuFxs2 + +0x00C + BitmapSkip[] SkipSamplers + +0x00D + ulong U01 + ulong U02 + if (U01 & 1) != 0 // SPlugVisibleFilter + short U03 + short U04 + +archive Bitmap + id + int + int + v1+ + int + int + +archive GpuFx + +archive BitmapSkip + id Name + bool \ No newline at end of file diff --git a/Src/GBX.NET/Engines/Plug/CPlugMaterialCustom.cs b/Src/GBX.NET/Engines/Plug/CPlugMaterialCustom.cs new file mode 100644 index 000000000..dce3410a4 --- /dev/null +++ b/Src/GBX.NET/Engines/Plug/CPlugMaterialCustom.cs @@ -0,0 +1,49 @@ +namespace GBX.NET.Engines.Plug; + +public partial class CPlugMaterialCustom +{ + [ChunkGenerationOptions(StructureKind = StructureKind.SeparateReadAndWrite)] + public partial class Chunk0903A00A; + + [ArchiveGenerationOptions(StructureKind = StructureKind.SeparateReadAndWrite)] + public partial class GpuFx + { + public string U01 { get; set; } = ""; + public bool U02 { get; set; } + public float[][] U03 { get; set; } = []; + + public void Read(GbxReader r, int v = 0) + { + U01 = r.ReadId(); + var count1 = r.ReadInt32(); + var count2 = r.ReadInt32(); + U02 = r.ReadBoolean(); + + U03 = new float[count2][]; + + for (var i = 0; i < count2; i++) + { + U03[i] = r.ReadArray(count1); + } + } + + public void Write(GbxWriter w, int v = 0) + { + var count2 = U03.Length; + var count1 = U03.Length > 0 ? U03[0].Length : 0; + + w.WriteIdAsString(U01); + w.Write(count1); + w.Write(count2); + w.Write(U02); + + for (var i = 0; i < count2; i++) + { + for (var j = 0; j < count1; j++) + { + w.Write(U03[i][j]); + } + } + } + } +} diff --git a/Src/GBX.NET/Engines/Plug/CPlugSolid2Model.cs b/Src/GBX.NET/Engines/Plug/CPlugSolid2Model.cs index 31828295e..9b6dc5885 100644 --- a/Src/GBX.NET/Engines/Plug/CPlugSolid2Model.cs +++ b/Src/GBX.NET/Engines/Plug/CPlugSolid2Model.cs @@ -1,4 +1,6 @@ -namespace GBX.NET.Engines.Plug; +using GBX.NET.Extensions.Exporters; + +namespace GBX.NET.Engines.Plug; public partial class CPlugSolid2Model { @@ -37,6 +39,19 @@ public partial class CPlugSolid2Model public CPlugMaterialUserInst[]? MaterialInsts { get => materialInsts; set => materialInsts = value; } public Material[]? CustomMaterials { get => customMaterials; set => customMaterials = value; } + public void ExportToObj(TextWriter objWriter, TextWriter mtlWriter, int? mergeVerticesDigitThreshold = null, int lod = 0) + { + ObjExporter.Export(this, objWriter, mtlWriter, mergeVerticesDigitThreshold, lod); + } + + public void ExportToObj(string objFilePath, string mtlFilePath, int? mergeVerticesDigitThreshold = null, int lod = 0) + { + using var objWriter = new StreamWriter(objFilePath); + using var mtlWriter = new StreamWriter(mtlFilePath); + + ExportToObj(objWriter, mtlWriter, mergeVerticesDigitThreshold, lod); + } + public partial class Chunk090BB000 : IVersionable { public int Version { get; set; } diff --git a/Src/GBX.NET/Engines/Plug/CPlugSurface.chunkl b/Src/GBX.NET/Engines/Plug/CPlugSurface.chunkl index a8be7832f..2eca4921a 100644 --- a/Src/GBX.NET/Engines/Plug/CPlugSurface.chunkl +++ b/Src/GBX.NET/Engines/Plug/CPlugSurface.chunkl @@ -10,11 +10,6 @@ CPlugSurface 0x0900C000 0x003 [TMT.v2, MP4.v2] archive SurfMaterial - bool IsMaterial - if IsMaterial - CPlugMaterial Material (external) - if !IsMaterial - short SurfaceId archive Sphere float Size diff --git a/Src/GBX.NET/Engines/Plug/CPlugSurface.cs b/Src/GBX.NET/Engines/Plug/CPlugSurface.cs index 424480f86..d6c7ef1f4 100644 --- a/Src/GBX.NET/Engines/Plug/CPlugSurface.cs +++ b/Src/GBX.NET/Engines/Plug/CPlugSurface.cs @@ -129,6 +129,14 @@ internal static void WriteSurf(ISurf? surf, GbxWriter w, int version) public sealed partial class SurfMaterial { + private CPlugMaterial? material; + public CPlugMaterial? Material { get => materialFile?.GetNode(ref material) ?? material; set => material = value; } + private Components.GbxRefTableFile? materialFile; + public Components.GbxRefTableFile? MaterialFile { get => materialFile; set => materialFile = value; } + public CPlugMaterial? GetMaterial(GbxReadSettings settings = default, bool exceptions = false) => materialFile?.GetNode(ref material, settings, exceptions) ?? material; + + public MaterialId? SurfaceId { get; set; } + public void ReadWrite(GbxReaderWriter rw, int version = 0) { if (rw.Boolean(material is not null || materialFile is not null)) @@ -137,7 +145,7 @@ public void ReadWrite(GbxReaderWriter rw, int version = 0) } else { - rw.Int16(ref surfaceId); + SurfaceId = (MaterialId)rw.Int16((short)SurfaceId.GetValueOrDefault()); } } } diff --git a/Src/GBX.NET/Engines/Plug/CPlugTree.cs b/Src/GBX.NET/Engines/Plug/CPlugTree.cs index 0001786a5..ae6d573a4 100644 --- a/Src/GBX.NET/Engines/Plug/CPlugTree.cs +++ b/Src/GBX.NET/Engines/Plug/CPlugTree.cs @@ -64,6 +64,18 @@ static IEnumerable GetAllChildren(CPlugTree tree, bool includeVisualM location = Iso4.Identity; } + if (tree is CPlugTreeVisualMip mip) + { + var lodChild = GetLodTree(mip, lod); + + var newLocation = MultiplyAddIso4(location, lodChild.Location.GetValueOrDefault(Iso4.Identity)); + + foreach (var descendant in GetAllChildren(lodChild, lod, newLocation)) + { + yield return descendant; + } + } + if (tree.Children is null) { yield break; @@ -75,20 +87,6 @@ static IEnumerable GetAllChildren(CPlugTree tree, bool includeVisualM var newLocation = MultiplyAddIso4(location, childLocation); - if (child is CPlugTreeVisualMip mip) - { - var lodChild = GetLodTree(mip, lod); - - newLocation = MultiplyAddIso4(newLocation, lodChild.Location.GetValueOrDefault(Iso4.Identity)); - - foreach (var descendant in GetAllChildren(lodChild, lod, newLocation)) - { - yield return descendant; - } - - continue; - } - yield return (child, newLocation); foreach (var descendant in GetAllChildren(child, lod, newLocation)) diff --git a/Src/GBX.NET/Engines/Plug/CPlugVertexStream.cs b/Src/GBX.NET/Engines/Plug/CPlugVertexStream.cs index 3310878f0..6f1439b75 100644 --- a/Src/GBX.NET/Engines/Plug/CPlugVertexStream.cs +++ b/Src/GBX.NET/Engines/Plug/CPlugVertexStream.cs @@ -21,6 +21,8 @@ public partial class CPlugVertexStream private Vec3[]? tangentVs; public Vec3[]? Positions { get => positions; set => positions = value; } + public SortedDictionary UVs { get => uvs; set => uvs = value; } + public SortedDictionary Colors { get => colors; set => colors = value; } public partial class Chunk09056000 : IVersionable { @@ -197,7 +199,7 @@ public void ReadWrite(GbxReaderWriter rw, int v = 0) rw.UInt16(ref offset); if (((ushort)(Flags2 >> 2) & 0x3FF) != Offset) { - throw new(""); + throw new Exception("Offset mismatch"); } } } diff --git a/Src/GBX.NET/Engines/Plug/CPlugWeather.chunkl b/Src/GBX.NET/Engines/Plug/CPlugWeather.chunkl new file mode 100644 index 000000000..83b441bac --- /dev/null +++ b/Src/GBX.NET/Engines/Plug/CPlugWeather.chunkl @@ -0,0 +1,57 @@ +CPlugWeather 0x0917E000 + +0x007 + +0x00B + int + int + int + int + int + int + +0x00D + vec2 + vec2 LDirSpecIntens + vec2 LDirSpecPower + vec3 SeaTwkWaterColor_Night + vec3 SeaTwkWaterColor_Day + vec2 + vec2 + vec2 + +0x00E + data[28] + data[28] + id + CPlugFileImg ImageLightAmb (external) + CPlugFileImg ImageLightDirSun (external) + CPlugFileImg ImageLightDirMoon (external) + CPlugFileImg BitmapFlareSun (external) + CPlugFileImg BitmapFlareMoon (external) + float FlareAngularSizeSun + float FlareAngularSizeMoon + float + float + float + float CameraFarZ + CPlugBitmap BitmapRainFid + CMwNod SceneFxFid + +0x00F + CPlugFileImg ImageLightDirDblSided (external) + +0x011 + CPlugFileImg BitmapSkyGradV (external) + +0x013 + CPlugFileImg ImageFogColor (external) + +0x014 + CPlugFileImg ImageSeaColor (external) + +0x016 + CPlugClouds Clouds + +0x017 + GxFogBlender FogBlender \ No newline at end of file diff --git a/Src/GBX.NET/Engines/Plug/CPlugWeather.cs b/Src/GBX.NET/Engines/Plug/CPlugWeather.cs new file mode 100644 index 000000000..13a57fbf5 --- /dev/null +++ b/Src/GBX.NET/Engines/Plug/CPlugWeather.cs @@ -0,0 +1,12 @@ +namespace GBX.NET.Engines.Plug; + +public partial class CPlugWeather +{ + public partial class Chunk0917E007 + { + public override void ReadWrite(CPlugWeather n, GbxReaderWriter rw) + { + // empty + } + } +} diff --git a/Src/GBX.NET/Engines/Plug/CPlugWeatherModel.chunkl b/Src/GBX.NET/Engines/Plug/CPlugWeatherModel.chunkl new file mode 100644 index 000000000..119c3c4de --- /dev/null +++ b/Src/GBX.NET/Engines/Plug/CPlugWeatherModel.chunkl @@ -0,0 +1,5 @@ +CPlugWeatherModel 0x090BF000 + +0x001 + CPlugWeather[]_deprec Weathers + CPlugBitmap BitmapSpecularDir \ No newline at end of file diff --git a/Src/GBX.NET/Engines/Scene/CSceneMobilLeaves.chunkl b/Src/GBX.NET/Engines/Scene/CSceneMobilLeaves.chunkl index e6e313056..0b766aa43 100644 --- a/Src/GBX.NET/Engines/Scene/CSceneMobilLeaves.chunkl +++ b/Src/GBX.NET/Engines/Scene/CSceneMobilLeaves.chunkl @@ -64,7 +64,7 @@ CSceneMobilLeaves 0x0A05E000 float 0x003 - CMwNod + CMwNod (external) float float int diff --git a/Src/GBX.NET/Exceptions/TextFormatNotSupportedException.cs b/Src/GBX.NET/Exceptions/TextFormatNotSupportedException.cs index fcfe012a8..7ce27c804 100644 --- a/Src/GBX.NET/Exceptions/TextFormatNotSupportedException.cs +++ b/Src/GBX.NET/Exceptions/TextFormatNotSupportedException.cs @@ -3,7 +3,7 @@ [Serializable] public class TextFormatNotSupportedException : Exception { - public TextFormatNotSupportedException() : base("Text-formatted Gbx files are not supported.") { } + public TextFormatNotSupportedException() : base("Text-formatted Gbx files are not YET supported.") { } public TextFormatNotSupportedException(string message) : base(message) { } public TextFormatNotSupportedException(string message, Exception? innerException) : base(message, innerException) { } } diff --git a/Src/GBX.NET/Extensions/Exporters/ObjExporter.cs b/Src/GBX.NET/Extensions/Exporters/ObjExporter.cs index b5606a50f..c826e7017 100644 --- a/Src/GBX.NET/Extensions/Exporters/ObjExporter.cs +++ b/Src/GBX.NET/Extensions/Exporters/ObjExporter.cs @@ -315,4 +315,170 @@ public static void Export(CPlugSolid solid, TextWriter objWriter, TextWriter mtl objWriter.WriteLine(); } } + + public static void Export(CPlugSolid2Model solid, TextWriter objWriter, TextWriter mtlWriter, int? mergeVerticesDigitThreshold = null, int lod = 0) + { + objWriter.WriteLine("# GBX.NET 2 - CPlugSolid2Model - OBJ Exporter (.obj)"); + objWriter.WriteLine("# Exported on {0}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", Invariant)); + objWriter.WriteLine(); + + mtlWriter.WriteLine("# GBX.NET 2 - CPlugSolid2Model - OBJ Exporter (.mtl)"); + mtlWriter.WriteLine("# Exported on {0}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", Invariant)); + mtlWriter.WriteLine(); + + var materials = new HashSet(); + + var positionsDict = mergeVerticesDigitThreshold.HasValue + ? new Dictionary(new Vec3EqualityComparer(mergeVerticesDigitThreshold.Value)) : []; + + if (solid.Visuals is null || solid.Visuals.Length == 0) + { + throw new Exception("CPlugSolid2Model has no Visuals."); + } + + foreach (var geom in solid.ShadedGeoms ?? []) + { + if (solid.Visuals?[geom.VisualIndex] is not CPlugVisualIndexedTriangles visual) + { + continue; + } + + var materialName = GetMaterialName(solid, geom.MaterialIndex); + + if (!materials.Contains(materialName)) + { + mtlWriter.WriteLine("newmtl {0}", materialName); + mtlWriter.WriteLine("Ns 250.000000"); // Specular exponent + mtlWriter.WriteLine("Ka 1.0000 1.0000 1.0000"); // Ambient color + mtlWriter.WriteLine("Kd 1.0000 1.0000 1.0000"); // Diffuse color + mtlWriter.WriteLine("Ks 0.5000 0.5000 0.5000"); // Specular color + mtlWriter.WriteLine("Ke 0.0000 0.0000 0.0000"); + mtlWriter.WriteLine("Ni 1.4500"); // Optical density + mtlWriter.WriteLine("d 1.0000"); // Dissolve + mtlWriter.WriteLine("illum 2"); // Illumination model + + mtlWriter.WriteLine(); + + materials.Add(materialName); + } + + foreach (var pos in visual.VertexStreams + .SelectMany(x => x.Positions ?? []) + .Concat(visual.Vertices.Select(x => x.Position))) + { + if (positionsDict.ContainsKey(pos)) + { + continue; + } + + objWriter.WriteLine("v {0} {1} {2}", + pos.X.ToString(Invariant), + pos.Y.ToString(Invariant), + pos.Z.ToString(Invariant)); + + positionsDict.Add(pos, positionsDict.Count); + } + } + + var uvs = new Dictionary(); + + foreach (var geom in solid.ShadedGeoms ?? []) + { + if (solid.Visuals?[geom.VisualIndex] is not CPlugVisualIndexedTriangles visual) + { + continue; + } + + if (visual.TexCoords.Length > 0) + { + foreach (var uv in visual.TexCoords[0].TexCoords.Select(x => x.UV)) + { + if (uvs.ContainsKey(uv)) + { + continue; + } + + objWriter.WriteLine("vt {0} {1}", + uv.X.ToString(Invariant), + uv.Y.ToString(Invariant)); + + uvs.Add(uv, uvs.Count); + } + } + + foreach (var uv in visual.VertexStreams + .SelectMany(x => x.UVs.Values.FirstOrDefault() ?? [])) + { + if (uvs.ContainsKey(uv)) + { + continue; + } + + objWriter.WriteLine("vt {0} {1}", + uv.X.ToString(Invariant), + uv.Y.ToString(Invariant)); + + uvs.Add(uv, uvs.Count); + } + } + + foreach (var geom in solid.ShadedGeoms ?? []) + { + if (solid.Visuals?[geom.VisualIndex] is not CPlugVisualIndexedTriangles visual) + { + continue; + } + + if (visual.IndexBuffer is null) + { + continue; + } + + var materialName = GetMaterialName(solid, geom.MaterialIndex); + + objWriter.WriteLine("g {0}", materialName); + objWriter.WriteLine("usemtl {0}", materialName); + + var triangleCounter = 0; + + foreach (var index in visual.IndexBuffer.Indices) + { + objWriter.Write('f'); + + var v = visual.VertexStreams.FirstOrDefault()?.Positions?[index] ?? visual.Vertices[index].Position; + + var uv = visual.TexCoords.Length == 0 + ? visual.VertexStreams[0].UVs.Values.First()[index] + : visual.TexCoords[0].TexCoords[index].UV; + + var uvIndex = uvs[uv]; + + var faceIndex = $" {positionsDict[v] + 1}/{uvIndex + 1}"; + + objWriter.Write(faceIndex); + + if (++triangleCounter % 3 == 0) + { + objWriter.WriteLine(); + } + } + + objWriter.WriteLine(); + } + } + + private static string GetMaterialName(CPlugSolid2Model solid, int materialIndex) + { + if (solid.CustomMaterials is { Length: > 0 } customMaterials) + { + return customMaterials[materialIndex].MaterialUserInst?.Link ?? "Unknown"; + } + + if (solid.Materials is { Length: > 0 } materialsArray) + { + return GbxPath.GetFileNameWithoutExtension(materialsArray[materialIndex].File?.FilePath) ?? "Unknown"; + } + + return "Unknown"; + } } diff --git a/Src/GBX.NET/GBX.NET.csproj b/Src/GBX.NET/GBX.NET.csproj index f05d4c523..c08d16eac 100644 --- a/Src/GBX.NET/GBX.NET.csproj +++ b/Src/GBX.NET/GBX.NET.csproj @@ -2,7 +2,7 @@ GBX.NET - 2.0.6 + 2.0.7 BigBang1112 General purpose library for Gbx files - data from Nadeo games like Trackmania or Shootmania. It supports high performance serialization and deserialization of 200+ Gbx classes. Copyright (c) 2024 Petr Pivoňka @@ -41,11 +41,11 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Src/GBX.NET/Gbx.cs b/Src/GBX.NET/Gbx.cs index 45e2b9e3e..1445131a2 100644 --- a/Src/GBX.NET/Gbx.cs +++ b/Src/GBX.NET/Gbx.cs @@ -14,7 +14,14 @@ public interface IGbx string? FilePath { get; set; } GbxHeader Header { get; } GbxRefTable? RefTable { get; } + GbxBody Body { get; } + GbxReadSettings ReadSettings { get; } CMwNod? Node { get; } + int? IdVersion { get; set; } + byte? PackDescVersion { get; set; } + int? DeprecVersion { get; set; } + ClassIdRemapMode ClassIdRemapMode { get; set; } + GbxCompression BodyCompression { get; set; } void Save(Stream stream, GbxWriteSettings settings = default); void Save(string filePath, GbxWriteSettings settings = default); diff --git a/Src/GBX.NET/GbxPath.cs b/Src/GBX.NET/GbxPath.cs index 0e2be7a66..e6b53a04e 100644 --- a/Src/GBX.NET/GbxPath.cs +++ b/Src/GBX.NET/GbxPath.cs @@ -1,12 +1,69 @@ -using System.Diagnostics.CodeAnalysis; +using System.Buffers; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; namespace GBX.NET; public static class GbxPath { + /// + /// Array of characters that are not allowed in Windows file names. + /// + public static ImmutableArray InvalidFileNameChars { get; } = ImmutableArray.Create([ + '\"', '<', '>', '|', '\0', + (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, + (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20, + (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, + (char)31, ':', '*', '?', '\\', '/' + ]); + +#if NET8_0_OR_GREATER + /// + /// for characters that are not allowed in Windows file names. This has faster Contains operations than . + /// + public static SearchValues InvalidFileNameCharSearchValues { get; } = SearchValues.Create([ + '\"', '<', '>', '|', '\0', + (char)1, (char)2, (char)3, (char)4, (char)5, (char)6, (char)7, (char)8, (char)9, (char)10, + (char)11, (char)12, (char)13, (char)14, (char)15, (char)16, (char)17, (char)18, (char)19, (char)20, + (char)21, (char)22, (char)23, (char)24, (char)25, (char)26, (char)27, (char)28, (char)29, (char)30, + (char)31, ':', '*', '?', '\\', '/' + ]); +#endif + + /// + /// Gets the file name without the extension, also stripping the Gbx's double extension properly. + /// + /// File path. + /// The file name without extension. [return: NotNullIfNotNull(nameof(path))] public static string? GetFileNameWithoutExtension(string? path) { return Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(path)); } + + /// + /// Gets a valid file name by replacing invalid Windows characters with underscores. + /// + /// Name of the file, WITHOUT the directory path. + /// File name that has no issues on Windows. + public static string GetValidFileName(string fileName) + { +#if NET8_0_OR_GREATER + var invalidChars = InvalidFileNameCharSearchValues; +#else + var invalidChars = InvalidFileNameChars; +#endif + var buffer = ArrayPool.Shared.Rent(fileName.Length); + var bufferIndex = 0; + + foreach (var c in fileName) + { + buffer[bufferIndex++] = invalidChars.Contains(c) ? '_' : c; + } + + var result = new string(buffer, 0, bufferIndex); + ArrayPool.Shared.Return(buffer); + + return result; + } } diff --git a/Src/GBX.NET/Serialization/GbxHeaderReader.cs b/Src/GBX.NET/Serialization/GbxHeaderReader.cs index c35177adf..fce195214 100644 --- a/Src/GBX.NET/Serialization/GbxHeaderReader.cs +++ b/Src/GBX.NET/Serialization/GbxHeaderReader.cs @@ -19,6 +19,11 @@ internal sealed class GbxHeaderReader(GbxReader reader) var basic = GbxHeaderBasic.Parse(reader); logger?.LogDebug("Basic: {Version} {Format} {RefTableCompression} {BodyCompression} {UnknownByte}", basic.Version, basic.Format, basic.CompressionOfRefTable, basic.CompressionOfBody, basic.UnknownByte); + if (basic.Format != GbxFormat.Binary) + { + throw new TextFormatNotSupportedException(); + } + var classId = ReadClassId(); var expectedClassId = ClassManager.GetId(); @@ -55,6 +60,11 @@ public GbxHeader Parse(out IClass? node) var basic = GbxHeaderBasic.Parse(reader); logger?.LogDebug("Basic: {Version} {Format} {RefTableCompression} {BodyCompression} {UnknownByte}", basic.Version, basic.Format, basic.CompressionOfRefTable, basic.CompressionOfBody, basic.UnknownByte); + if (basic.Format != GbxFormat.Binary) + { + throw new TextFormatNotSupportedException(); + } + var classId = ReadClassId(); node = ClassManager.New(classId); diff --git a/Src/GBX.NET/Serialization/GbxReader.cs b/Src/GBX.NET/Serialization/GbxReader.cs index d5f637775..c56dbc047 100644 --- a/Src/GBX.NET/Serialization/GbxReader.cs +++ b/Src/GBX.NET/Serialization/GbxReader.cs @@ -252,7 +252,18 @@ internal void LoadRefTable(IReadOnlyDictionary refTable) public bool ReadGbxMagic() { +#if NETSTANDARD2_0 return base.ReadByte() == 'G' && base.ReadByte() == 'B' && base.ReadByte() == 'X'; +#else + Span buffer = stackalloc byte[3]; + + if (BaseStream.Read(buffer) != 3) + { + return false; + } + + return buffer[0] == 'G' && buffer[1] == 'B' && buffer[2] == 'X'; +#endif } public override byte ReadByte() diff --git a/Src/GBX.NET/Serialization/GbxWriter.cs b/Src/GBX.NET/Serialization/GbxWriter.cs index 51fa45203..2b846b3f1 100644 --- a/Src/GBX.NET/Serialization/GbxWriter.cs +++ b/Src/GBX.NET/Serialization/GbxWriter.cs @@ -878,7 +878,7 @@ public void WriteOptimizedInt(int value, int determineFrom) Write(value); break; case >= byte.MaxValue: - Write((uint)value); + Write((ushort)value); break; default: Write((byte)value); diff --git a/Tests/GBX.NET.Tests/Files/Gbx/CGameCtnBlockInfoClassic/GBX-NET 2 CGameCtnBlockInfo TMF 001.TMEDClassic.Gbx b/Tests/GBX.NET.Tests/Files/Gbx/CGameCtnBlockInfoClassic/GBX-NET 2 CGameCtnBlockInfo TMF 001.TMEDClassic.Gbx deleted file mode 100644 index fdb612cc5..000000000 Binary files a/Tests/GBX.NET.Tests/Files/Gbx/CGameCtnBlockInfoClassic/GBX-NET 2 CGameCtnBlockInfo TMF 001.TMEDClassic.Gbx and /dev/null differ diff --git a/Tests/GBX.NET.Tests/Files/Gbx/CGameItemModel/GBX-NET 2 CGameItemModel TM2020 005.Item.Gbx b/Tests/GBX.NET.Tests/Files/Gbx/CGameItemModel/GBX-NET 2 CGameItemModel TM2020 005.Item.Gbx new file mode 100644 index 000000000..9408a7207 Binary files /dev/null and b/Tests/GBX.NET.Tests/Files/Gbx/CGameItemModel/GBX-NET 2 CGameItemModel TM2020 005.Item.Gbx differ diff --git a/Tests/GBX.NET.Tests/Files/Gbx/CGameItemModel/GBX-NET 2 CGameItemModel TM2020 006.Item.Gbx b/Tests/GBX.NET.Tests/Files/Gbx/CGameItemModel/GBX-NET 2 CGameItemModel TM2020 006.Item.Gbx new file mode 100644 index 000000000..473e5bb78 Binary files /dev/null and b/Tests/GBX.NET.Tests/Files/Gbx/CGameItemModel/GBX-NET 2 CGameItemModel TM2020 006.Item.Gbx differ diff --git a/Tests/GBX.NET.Tests/Files/Gbx/CGameSpawnModel/GBX-NET 2 CGameSpawnModel TMT 001.Spawn.Gbx b/Tests/GBX.NET.Tests/Files/Gbx/CGameSpawnModel/GBX-NET 2 CGameSpawnModel TMT 001.Spawn.Gbx deleted file mode 100644 index 2c6607f5e..000000000 Binary files a/Tests/GBX.NET.Tests/Files/Gbx/CGameSpawnModel/GBX-NET 2 CGameSpawnModel TMT 001.Spawn.Gbx and /dev/null differ diff --git a/Tests/GBX.NET.Tests/Files/Gbx/CPlugPrefab/GBX-NET 2 CPlugPrefab TM2020 001.Prefab.Gbx b/Tests/GBX.NET.Tests/Files/Gbx/CPlugPrefab/GBX-NET 2 CPlugPrefab TM2020 001.Prefab.Gbx deleted file mode 100644 index 01f705ecb..000000000 Binary files a/Tests/GBX.NET.Tests/Files/Gbx/CPlugPrefab/GBX-NET 2 CPlugPrefab TM2020 001.Prefab.Gbx and /dev/null differ diff --git a/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid/GBX-NET 2 CPlugSolid MP4 001.Solid.Gbx b/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid/GBX-NET 2 CPlugSolid MP4 001.Solid.Gbx deleted file mode 100644 index 43278dd7e..000000000 Binary files a/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid/GBX-NET 2 CPlugSolid MP4 001.Solid.Gbx and /dev/null differ diff --git a/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid/GBX-NET 2 CPlugSolid TM10 001.Solid.Gbx b/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid/GBX-NET 2 CPlugSolid TM10 001.Solid.Gbx deleted file mode 100644 index 31b722935..000000000 Binary files a/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid/GBX-NET 2 CPlugSolid TM10 001.Solid.Gbx and /dev/null differ diff --git a/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid/GBX-NET 2 CPlugSolid TMF 001.Solid.Gbx b/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid/GBX-NET 2 CPlugSolid TMF 001.Solid.Gbx deleted file mode 100644 index becba7f50..000000000 Binary files a/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid/GBX-NET 2 CPlugSolid TMF 001.Solid.Gbx and /dev/null differ diff --git a/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid/GBX-NET 2 CPlugSolid TMNESWC 001.Solid.Gbx b/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid/GBX-NET 2 CPlugSolid TMNESWC 001.Solid.Gbx deleted file mode 100644 index 4eab0b30c..000000000 Binary files a/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid/GBX-NET 2 CPlugSolid TMNESWC 001.Solid.Gbx and /dev/null differ diff --git a/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid/GBX-NET 2 CPlugSolid TMSX 001.Solid.Gbx b/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid/GBX-NET 2 CPlugSolid TMSX 001.Solid.Gbx deleted file mode 100644 index 7a8ed6178..000000000 Binary files a/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid/GBX-NET 2 CPlugSolid TMSX 001.Solid.Gbx and /dev/null differ diff --git a/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid/GBX-NET 2 CPlugSolid TMT 001.Solid.Gbx b/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid/GBX-NET 2 CPlugSolid TMT 001.Solid.Gbx deleted file mode 100644 index e7b3de9aa..000000000 Binary files a/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid/GBX-NET 2 CPlugSolid TMT 001.Solid.Gbx and /dev/null differ diff --git a/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid2Model/GBX-NET 2 CPlugSolid2Model MP4 001.Mesh.Gbx b/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid2Model/GBX-NET 2 CPlugSolid2Model MP4 001.Mesh.Gbx deleted file mode 100644 index 062fdd8d6..000000000 Binary files a/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid2Model/GBX-NET 2 CPlugSolid2Model MP4 001.Mesh.Gbx and /dev/null differ diff --git a/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid2Model/GBX-NET 2 CPlugSolid2Model TMT 001.Solid2.Gbx b/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid2Model/GBX-NET 2 CPlugSolid2Model TMT 001.Solid2.Gbx deleted file mode 100644 index 870a93d6b..000000000 Binary files a/Tests/GBX.NET.Tests/Files/Gbx/CPlugSolid2Model/GBX-NET 2 CPlugSolid2Model TMT 001.Solid2.Gbx and /dev/null differ diff --git a/Tests/GBX.NET.Tests/Files/Gbx/CPlugTrainWagonModel/GBX-NET 2 CPlugTrainWagonModel MP4 001.Wagon.Gbx b/Tests/GBX.NET.Tests/Files/Gbx/CPlugTrainWagonModel/GBX-NET 2 CPlugTrainWagonModel MP4 001.Wagon.Gbx deleted file mode 100644 index d329125bf..000000000 Binary files a/Tests/GBX.NET.Tests/Files/Gbx/CPlugTrainWagonModel/GBX-NET 2 CPlugTrainWagonModel MP4 001.Wagon.Gbx and /dev/null differ diff --git a/Tests/GBX.NET.Tests/GBX.NET.Tests.csproj b/Tests/GBX.NET.Tests/GBX.NET.Tests.csproj index 8efa941ce..5ba0892ae 100644 --- a/Tests/GBX.NET.Tests/GBX.NET.Tests.csproj +++ b/Tests/GBX.NET.Tests/GBX.NET.Tests.csproj @@ -19,7 +19,7 @@ - + diff --git a/Tests/GBX.NET.Tests/Integration/GbxEqualTests.cs b/Tests/GBX.NET.Tests/Integration/GbxEqualTests.cs index 1addfa123..6472551df 100644 --- a/Tests/GBX.NET.Tests/Integration/GbxEqualTests.cs +++ b/Tests/GBX.NET.Tests/Integration/GbxEqualTests.cs @@ -47,23 +47,13 @@ public GbxEqualTests(ITestOutputHelper output) [InlineData("CGameItemModel/GBX-NET 2 CGameItemModel TM2020 002.Item.Gbx")] [InlineData("CGameItemModel/GBX-NET 2 CGameItemModel TM2020 003.Item.Gbx")] [InlineData("CGameItemModel/GBX-NET 2 CGameItemModel TM2020 004.Block.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid TM10 001.Solid.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid TMSX 001.Solid.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid TMNESWC 001.Solid.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid TMF 001.Solid.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid TMT 001.Solid.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid MP4 001.Solid.Gbx")] - [InlineData("CPlugSolid2Model/GBX-NET 2 CPlugSolid2Model TMT 001.Solid2.Gbx")] - [InlineData("CPlugSolid2Model/GBX-NET 2 CPlugSolid2Model MP4 001.Mesh.Gbx")] - [InlineData("CPlugPrefab/GBX-NET 2 CPlugPrefab TM2020 001.Prefab.Gbx")] - [InlineData("CPlugTrainWagonModel/GBX-NET 2 CPlugTrainWagonModel MP4 001.Wagon.Gbx")] - [InlineData("CGameCtnBlockInfoClassic/GBX-NET 2 CGameCtnBlockInfo TMF 001.TMEDClassic.Gbx")] + [InlineData("CGameItemModel/GBX-NET 2 CGameItemModel TM2020 005.Item.Gbx")] + [InlineData("CGameItemModel/GBX-NET 2 CGameItemModel TM2020 006.Item.Gbx")] [InlineData("CGameCtnMacroBlockInfo/GBX-NET 2 CGameCtnMacroBlockInfo MP4 001.Macroblock.Gbx")] [InlineData("CGameCtnMacroBlockInfo/GBX-NET 2 CGameCtnMacroBlockInfo TM2020 001.Macroblock.Gbx")] [InlineData("CSystemConfig/GBX-NET 2 CSystemConfig TMF 001.SystemConfig.Gbx")] [InlineData("CSystemConfig/GBX-NET 2 CSystemConfig MP3 001.SystemConfig.Gbx")] [InlineData("CSystemConfig/GBX-NET 2 CSystemConfig MP4 001.SystemConfig.Gbx")] - [InlineData("CGameSpawnModel/GBX-NET 2 CGameSpawnModel TMT 001.Spawn.Gbx")] [InlineData("CGameCtnMediaClip/GBX-NET 2 CGameCtnMediaClip TMF 001.Clip.Gbx")] [InlineData("CGameCtnMediaClip/GBX-NET 2 CGameCtnMediaClip MP4 001.Clip.Gbx")] [InlineData("CGameCtnMediaClip/GBX-NET 2 CGameCtnMediaClip TM2020 001.Clip.Gbx")] @@ -110,23 +100,13 @@ public void TestGbxEqualDataImplicit(string filePath) [InlineData("CGameItemModel/GBX-NET 2 CGameItemModel TM2020 002.Item.Gbx")] [InlineData("CGameItemModel/GBX-NET 2 CGameItemModel TM2020 003.Item.Gbx")] [InlineData("CGameItemModel/GBX-NET 2 CGameItemModel TM2020 004.Block.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid TM10 001.Solid.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid TMSX 001.Solid.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid TMNESWC 001.Solid.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid TMF 001.Solid.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid TMT 001.Solid.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid MP4 001.Solid.Gbx")] - [InlineData("CPlugSolid2Model/GBX-NET 2 CPlugSolid2Model TMT 001.Solid2.Gbx")] - [InlineData("CPlugSolid2Model/GBX-NET 2 CPlugSolid2Model MP4 001.Mesh.Gbx")] - [InlineData("CPlugPrefab/GBX-NET 2 CPlugPrefab TM2020 001.Prefab.Gbx")] - [InlineData("CPlugTrainWagonModel/GBX-NET 2 CPlugTrainWagonModel MP4 001.Wagon.Gbx")] - [InlineData("CGameCtnBlockInfoClassic/GBX-NET 2 CGameCtnBlockInfo TMF 001.TMEDClassic.Gbx")] + [InlineData("CGameItemModel/GBX-NET 2 CGameItemModel TM2020 005.Item.Gbx")] + [InlineData("CGameItemModel/GBX-NET 2 CGameItemModel TM2020 006.Item.Gbx")] [InlineData("CGameCtnMacroBlockInfo/GBX-NET 2 CGameCtnMacroBlockInfo MP4 001.Macroblock.Gbx")] [InlineData("CGameCtnMacroBlockInfo/GBX-NET 2 CGameCtnMacroBlockInfo TM2020 001.Macroblock.Gbx")] [InlineData("CSystemConfig/GBX-NET 2 CSystemConfig TMF 001.SystemConfig.Gbx")] [InlineData("CSystemConfig/GBX-NET 2 CSystemConfig MP3 001.SystemConfig.Gbx")] [InlineData("CSystemConfig/GBX-NET 2 CSystemConfig MP4 001.SystemConfig.Gbx")] - [InlineData("CGameSpawnModel/GBX-NET 2 CGameSpawnModel TMT 001.Spawn.Gbx")] [InlineData("CGameCtnMediaClip/GBX-NET 2 CGameCtnMediaClip TMF 001.Clip.Gbx")] [InlineData("CGameCtnMediaClip/GBX-NET 2 CGameCtnMediaClip MP4 001.Clip.Gbx")] [InlineData("CGameCtnMediaClip/GBX-NET 2 CGameCtnMediaClip TM2020 001.Clip.Gbx")] @@ -177,23 +157,13 @@ public void TestGbxEqualObjectsImplicit(string filePath) [InlineData("CGameItemModel/GBX-NET 2 CGameItemModel TM2020 002.Item.Gbx")] [InlineData("CGameItemModel/GBX-NET 2 CGameItemModel TM2020 003.Item.Gbx")] [InlineData("CGameItemModel/GBX-NET 2 CGameItemModel TM2020 004.Block.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid TM10 001.Solid.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid TMSX 001.Solid.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid TMNESWC 001.Solid.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid TMF 001.Solid.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid TMT 001.Solid.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid MP4 001.Solid.Gbx")] - [InlineData("CPlugSolid2Model/GBX-NET 2 CPlugSolid2Model TMT 001.Solid2.Gbx")] - [InlineData("CPlugSolid2Model/GBX-NET 2 CPlugSolid2Model MP4 001.Mesh.Gbx")] - [InlineData("CPlugPrefab/GBX-NET 2 CPlugPrefab TM2020 001.Prefab.Gbx")] - [InlineData("CPlugTrainWagonModel/GBX-NET 2 CPlugTrainWagonModel MP4 001.Wagon.Gbx")] - [InlineData("CGameCtnBlockInfoClassic/GBX-NET 2 CGameCtnBlockInfo TMF 001.TMEDClassic.Gbx")] + [InlineData("CGameItemModel/GBX-NET 2 CGameItemModel TM2020 005.Item.Gbx")] + [InlineData("CGameItemModel/GBX-NET 2 CGameItemModel TM2020 006.Item.Gbx")] [InlineData("CGameCtnMacroBlockInfo/GBX-NET 2 CGameCtnMacroBlockInfo MP4 001.Macroblock.Gbx")] [InlineData("CGameCtnMacroBlockInfo/GBX-NET 2 CGameCtnMacroBlockInfo TM2020 001.Macroblock.Gbx")] [InlineData("CSystemConfig/GBX-NET 2 CSystemConfig TMF 001.SystemConfig.Gbx")] [InlineData("CSystemConfig/GBX-NET 2 CSystemConfig MP3 001.SystemConfig.Gbx")] [InlineData("CSystemConfig/GBX-NET 2 CSystemConfig MP4 001.SystemConfig.Gbx")] - [InlineData("CGameSpawnModel/GBX-NET 2 CGameSpawnModel TMT 001.Spawn.Gbx")] [InlineData("CGameCtnMediaClip/GBX-NET 2 CGameCtnMediaClip TMF 001.Clip.Gbx")] [InlineData("CGameCtnMediaClip/GBX-NET 2 CGameCtnMediaClip MP4 001.Clip.Gbx")] [InlineData("CGameCtnMediaClip/GBX-NET 2 CGameCtnMediaClip TM2020 001.Clip.Gbx")] @@ -239,23 +209,13 @@ public async Task TestGbxEqualDataImplicitAsync(string filePath) [InlineData("CGameItemModel/GBX-NET 2 CGameItemModel TM2020 002.Item.Gbx")] [InlineData("CGameItemModel/GBX-NET 2 CGameItemModel TM2020 003.Item.Gbx")] [InlineData("CGameItemModel/GBX-NET 2 CGameItemModel TM2020 004.Block.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid TM10 001.Solid.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid TMSX 001.Solid.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid TMNESWC 001.Solid.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid TMF 001.Solid.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid TMT 001.Solid.Gbx")] - [InlineData("CPlugSolid/GBX-NET 2 CPlugSolid MP4 001.Solid.Gbx")] - [InlineData("CPlugSolid2Model/GBX-NET 2 CPlugSolid2Model TMT 001.Solid2.Gbx")] - [InlineData("CPlugSolid2Model/GBX-NET 2 CPlugSolid2Model MP4 001.Mesh.Gbx")] - [InlineData("CPlugPrefab/GBX-NET 2 CPlugPrefab TM2020 001.Prefab.Gbx")] - [InlineData("CPlugTrainWagonModel/GBX-NET 2 CPlugTrainWagonModel MP4 001.Wagon.Gbx")] - [InlineData("CGameCtnBlockInfoClassic/GBX-NET 2 CGameCtnBlockInfo TMF 001.TMEDClassic.Gbx")] + [InlineData("CGameItemModel/GBX-NET 2 CGameItemModel TM2020 005.Item.Gbx")] + [InlineData("CGameItemModel/GBX-NET 2 CGameItemModel TM2020 006.Item.Gbx")] [InlineData("CGameCtnMacroBlockInfo/GBX-NET 2 CGameCtnMacroBlockInfo MP4 001.Macroblock.Gbx")] [InlineData("CGameCtnMacroBlockInfo/GBX-NET 2 CGameCtnMacroBlockInfo TM2020 001.Macroblock.Gbx")] [InlineData("CSystemConfig/GBX-NET 2 CSystemConfig TMF 001.SystemConfig.Gbx")] [InlineData("CSystemConfig/GBX-NET 2 CSystemConfig MP3 001.SystemConfig.Gbx")] [InlineData("CSystemConfig/GBX-NET 2 CSystemConfig MP4 001.SystemConfig.Gbx")] - [InlineData("CGameSpawnModel/GBX-NET 2 CGameSpawnModel TMT 001.Spawn.Gbx")] [InlineData("CGameCtnMediaClip/GBX-NET 2 CGameCtnMediaClip TMF 001.Clip.Gbx")] [InlineData("CGameCtnMediaClip/GBX-NET 2 CGameCtnMediaClip MP4 001.Clip.Gbx")] [InlineData("CGameCtnMediaClip/GBX-NET 2 CGameCtnMediaClip TM2020 001.Clip.Gbx")] @@ -312,51 +272,42 @@ public void TestGbxEqualDataExplicitCGameCtnChallenge(string filePath) [InlineData("GBX-NET 2 CGameItemModel TM2020 002.Item.Gbx")] [InlineData("GBX-NET 2 CGameItemModel TM2020 003.Item.Gbx")] [InlineData("GBX-NET 2 CGameItemModel TM2020 004.Block.Gbx")] + [InlineData("GBX-NET 2 CGameItemModel TM2020 005.Item.Gbx")] + [InlineData("GBX-NET 2 CGameItemModel TM2020 006.Item.Gbx")] public void TestGbxEqualDataExplicitCGameItemModel(string filePath) { TestGbxEqualDataExplicit(filePath); } - [Theory] - [InlineData("GBX-NET 2 CPlugSolid TM10 001.Solid.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid TMSX 001.Solid.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid TMNESWC 001.Solid.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid TMF 001.Solid.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid TMT 001.Solid.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid MP4 001.Solid.Gbx")] + /*[Theory] public void TestGbxEqualDataExplicitCPlugSolid(string filePath) { TestGbxEqualDataExplicit(filePath); - } + }*/ - [Theory] - [InlineData("GBX-NET 2 CPlugSolid2Model TMT 001.Solid2.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid2Model MP4 001.Mesh.Gbx")] + /*[Theory] public void TestGbxEqualDataExplicitCPlugSolid2Model(string filePath) { TestGbxEqualDataExplicit(filePath); - } + }*/ - [Theory] - [InlineData("GBX-NET 2 CPlugPrefab TM2020 001.Prefab.Gbx")] + /*[Theory] public void TestGbxEqualDataExplicitCPlugPrefab(string filePath) { TestGbxEqualDataExplicit(filePath); - } + }*/ - [Theory] - [InlineData("GBX-NET 2 CPlugTrainWagonModel MP4 001.Wagon.Gbx")] + /*[Theory] public void TestGbxEqualDataExplicitCPlugTrainWagonModel(string filePath) { TestGbxEqualDataExplicit(filePath); - } + }*/ - [Theory] - [InlineData("GBX-NET 2 CGameCtnBlockInfo TMF 001.TMEDClassic.Gbx")] + /*[Theory] public void TestGbxEqualDataExplicitCGameCtnBlockInfoClassic(string filePath) { TestGbxEqualDataExplicit(filePath); - } + }*/ [Theory] [InlineData("GBX-NET 2 CGameCtnMacroBlockInfo MP4 001.Macroblock.Gbx")] @@ -375,12 +326,11 @@ public void TestGbxEqualDataExplicitCSystemConfig(string filePath) TestGbxEqualDataExplicit(filePath); } - [Theory] - [InlineData("GBX-NET 2 CGameSpawnModel TMT 001.Spawn.Gbx")] + /*[Theory] public void TestGbxEqualDataExplicitCGameSpawnModel(string filePath) { TestGbxEqualDataExplicit(filePath); - } + }*/ [Theory] [InlineData("GBX-NET 2 CGameCtnMediaClip TMF 001.Clip.Gbx")] @@ -426,51 +376,42 @@ public void TestGbxEqualObjectsExplicitCGameCtnChallenge(string filePath) [InlineData("GBX-NET 2 CGameItemModel TM2020 002.Item.Gbx")] [InlineData("GBX-NET 2 CGameItemModel TM2020 003.Item.Gbx")] [InlineData("GBX-NET 2 CGameItemModel TM2020 004.Block.Gbx")] + [InlineData("GBX-NET 2 CGameItemModel TM2020 005.Item.Gbx")] + [InlineData("GBX-NET 2 CGameItemModel TM2020 006.Item.Gbx")] public void TestGbxEqualObjectsExplicitCGameItemModel(string filePath) { TestGbxEqualObjectsExplicit(filePath); } - [Theory] - [InlineData("GBX-NET 2 CPlugSolid TM10 001.Solid.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid TMSX 001.Solid.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid TMNESWC 001.Solid.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid TMF 001.Solid.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid TMT 001.Solid.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid MP4 001.Solid.Gbx")] + /*[Theory] public void TestGbxEqualObjectsExplicitCPlugSolid(string filePath) { TestGbxEqualObjectsExplicit(filePath); - } + }*/ - [Theory] - [InlineData("GBX-NET 2 CPlugSolid2Model TMT 001.Solid2.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid2Model MP4 001.Mesh.Gbx")] + /*[Theory] public void TestGbxEqualObjectsExplicitCPlugSolid2Model(string filePath) { TestGbxEqualObjectsExplicit(filePath); - } + }*/ - [Theory] - [InlineData("GBX-NET 2 CPlugPrefab TM2020 001.Prefab.Gbx")] + /*[Theory] public void TestGbxEqualObjectsExplicitCPlugPrefab(string filePath) { TestGbxEqualObjectsExplicit(filePath); - } + }*/ - [Theory] - [InlineData("GBX-NET 2 CPlugTrainWagonModel MP4 001.Wagon.Gbx")] + /*[Theory] public void TestGbxEqualObjectsExplicitCPlugTrainWagonModel(string filePath) { TestGbxEqualObjectsExplicit(filePath); - } + }*/ - [Theory] - [InlineData("GBX-NET 2 CGameCtnBlockInfo TMF 001.TMEDClassic.Gbx")] + /*[Theory] public void TestGbxEqualObjectsExplicitCGameCtnBlockInfoClassic(string filePath) { TestGbxEqualObjectsExplicit(filePath); - } + }*/ [Theory] [InlineData("GBX-NET 2 CGameCtnMacroBlockInfo MP4 001.Macroblock.Gbx")] @@ -489,12 +430,11 @@ public void TestGbxEqualObjectsExplicitCSystemConfig(string filePath) TestGbxEqualObjectsExplicit(filePath); } - [Theory] - [InlineData("GBX-NET 2 CGameSpawnModel TMT 001.Spawn.Gbx")] + /*[Theory] public void TestGbxEqualObjectsExplicitCGameSpawnModel(string filePath) { TestGbxEqualObjectsExplicit(filePath); - } + }*/ [Theory] [InlineData("GBX-NET 2 CGameCtnMediaClip TMF 001.Clip.Gbx")] @@ -540,51 +480,42 @@ public async Task TestGbxEqualDataExplicitCGameCtnChallengeAsync(string filePath [InlineData("GBX-NET 2 CGameItemModel TM2020 002.Item.Gbx")] [InlineData("GBX-NET 2 CGameItemModel TM2020 003.Item.Gbx")] [InlineData("GBX-NET 2 CGameItemModel TM2020 004.Block.Gbx")] + [InlineData("GBX-NET 2 CGameItemModel TM2020 005.Item.Gbx")] + [InlineData("GBX-NET 2 CGameItemModel TM2020 006.Item.Gbx")] public async Task TestGbxEqualDataExplicitCGameItemModelAsync(string filePath) { await TestGbxEqualDataExplicitAsync(filePath); } - [Theory] - [InlineData("GBX-NET 2 CPlugSolid TM10 001.Solid.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid TMSX 001.Solid.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid TMNESWC 001.Solid.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid TMF 001.Solid.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid TMT 001.Solid.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid MP4 001.Solid.Gbx")] + /*[Theory] public async Task TestGbxEqualDataExplicitCPlugSolidAsync(string filePath) { await TestGbxEqualDataExplicitAsync(filePath); - } + }*/ - [Theory] - [InlineData("GBX-NET 2 CPlugSolid2Model TMT 001.Solid2.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid2Model MP4 001.Mesh.Gbx")] + /*[Theory] public async Task TestGbxEqualDataExplicitCPlugSolid2ModelAsync(string filePath) { await TestGbxEqualDataExplicitAsync(filePath); - } + }*/ - [Theory] - [InlineData("GBX-NET 2 CPlugPrefab TM2020 001.Prefab.Gbx")] + /*[Theory] public async Task TestGbxEqualDataExplicitCPlugPrefabAsync(string filePath) { await TestGbxEqualDataExplicitAsync(filePath); - } + }*/ - [Theory] - [InlineData("GBX-NET 2 CPlugTrainWagonModel MP4 001.Wagon.Gbx")] + /*[Theory] public async Task TestGbxEqualDataExplicitCPlugTrainWagonModelAsync(string filePath) { await TestGbxEqualDataExplicitAsync(filePath); - } + }*/ - [Theory] - [InlineData("GBX-NET 2 CGameCtnBlockInfo TMF 001.TMEDClassic.Gbx")] + /*[Theory] public async Task TestGbxEqualDataExplicitCGameCtnBlockInfoClassicAsync(string filePath) { await TestGbxEqualDataExplicitAsync(filePath); - } + }*/ [Theory] [InlineData("GBX-NET 2 CGameCtnMacroBlockInfo MP4 001.Macroblock.Gbx")] @@ -603,12 +534,11 @@ public async Task TestGbxEqualDataExplicitCSystemConfigAsync(string filePath) await TestGbxEqualDataExplicitAsync(filePath); } - [Theory] - [InlineData("GBX-NET 2 CGameSpawnModel TMT 001.Spawn.Gbx")] + /*[Theory] public async Task TestGbxEqualDataExplicitCGameSpawnModelAsync(string filePath) { await TestGbxEqualDataExplicitAsync(filePath); - } + }*/ [Theory] [InlineData("GBX-NET 2 CGameCtnMediaClip TMF 001.Clip.Gbx")] @@ -654,51 +584,42 @@ public async Task TestGbxEqualObjectsExplicitCGameCtnChallengeAsync(string fileP [InlineData("GBX-NET 2 CGameItemModel TM2020 002.Item.Gbx")] [InlineData("GBX-NET 2 CGameItemModel TM2020 003.Item.Gbx")] [InlineData("GBX-NET 2 CGameItemModel TM2020 004.Block.Gbx")] + [InlineData("GBX-NET 2 CGameItemModel TM2020 005.Item.Gbx")] + [InlineData("GBX-NET 2 CGameItemModel TM2020 006.Item.Gbx")] public async Task TestGbxEqualObjectsExplicitCGameItemModelAsync(string filePath) { await TestGbxEqualObjectsExplicitAsync(filePath); } - [Theory] - [InlineData("GBX-NET 2 CPlugSolid TM10 001.Solid.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid TMSX 001.Solid.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid TMNESWC 001.Solid.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid TMF 001.Solid.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid TMT 001.Solid.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid MP4 001.Solid.Gbx")] + /*[Theory] public async Task TestGbxEqualObjectsExplicitCPlugSolidAsync(string filePath) { await TestGbxEqualObjectsExplicitAsync(filePath); - } + }*/ - [Theory] - [InlineData("GBX-NET 2 CPlugSolid2Model TMT 001.Solid2.Gbx")] - [InlineData("GBX-NET 2 CPlugSolid2Model MP4 001.Mesh.Gbx")] + /*[Theory] public async Task TestGbxEqualObjectsExplicitCPlugSolid2ModelAsync(string filePath) { await TestGbxEqualObjectsExplicitAsync(filePath); - } + }*/ - [Theory] - [InlineData("GBX-NET 2 CPlugPrefab TM2020 001.Prefab.Gbx")] + /*[Theory] public async Task TestGbxEqualObjectsExplicitCPlugPrefabAsync(string filePath) { await TestGbxEqualObjectsExplicitAsync(filePath); - } + }*/ - [Theory] - [InlineData("GBX-NET 2 CPlugTrainWagonModel MP4 001.Wagon.Gbx")] + /*[Theory] public async Task TestGbxEqualObjectsExplicitCPlugTrainWagonModelAsync(string filePath) { await TestGbxEqualObjectsExplicitAsync(filePath); - } + }*/ - [Theory] - [InlineData("GBX-NET 2 CGameCtnBlockInfo TMF 001.TMEDClassic.Gbx")] + /*[Theory] public async Task TestGbxEqualObjectsExplicitCGameCtnBlockInfoClassicAsync(string filePath) { await TestGbxEqualObjectsExplicitAsync(filePath); - } + }*/ [Theory] [InlineData("GBX-NET 2 CGameCtnMacroBlockInfo MP4 001.Macroblock.Gbx")] @@ -717,12 +638,11 @@ public async Task TestGbxEqualObjectsExplicitCSystemConfigAsync(string filePath) await TestGbxEqualObjectsExplicitAsync(filePath); } - [Theory] - [InlineData("GBX-NET 2 CGameSpawnModel TMT 001.Spawn.Gbx")] + /*[Theory] public async Task TestGbxEqualObjectsExplicitCGameSpawnModelAsync(string filePath) { await TestGbxEqualObjectsExplicitAsync(filePath); - } + }*/ [Theory] [InlineData("GBX-NET 2 CGameCtnMediaClip TMF 001.Clip.Gbx")] diff --git a/Tests/GBX.NET.Tests/Unit/Serialization/GbxReaderTests.cs b/Tests/GBX.NET.Tests/Unit/Serialization/GbxReaderTests.cs index 844280909..5cf6889a2 100644 --- a/Tests/GBX.NET.Tests/Unit/Serialization/GbxReaderTests.cs +++ b/Tests/GBX.NET.Tests/Unit/Serialization/GbxReaderTests.cs @@ -66,7 +66,7 @@ public void ReadGbxMagic_HasIncorrectMagic() // Assert Assert.False(result, "GBX magic is valid but it shouldn't be."); - Assert.Equal(expected: 2, actual: ms.Position); + Assert.Equal(expected: 3, actual: ms.Position); } [Fact] @@ -79,8 +79,10 @@ public void ReadGbxMagic_MissingData_ThrowsEndOfStream() ms.Write([(byte)'G', (byte)'B']); ms.Position = 0; - // Act & Assert - Assert.Throws(() => r.ReadGbxMagic()); + // Act + var result = r.ReadGbxMagic(); + + Assert.False(result); } [Fact] diff --git a/Tools/CryCryptSwitch/CryCryptSwitch.csproj b/Tools/CryCryptSwitch/CryCryptSwitch.csproj new file mode 100644 index 000000000..ccd52c9a9 --- /dev/null +++ b/Tools/CryCryptSwitch/CryCryptSwitch.csproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + enable + enable + true + true + + + + + + + + diff --git a/Tools/CryCryptSwitch/Program.cs b/Tools/CryCryptSwitch/Program.cs new file mode 100644 index 000000000..3684ee7c2 --- /dev/null +++ b/Tools/CryCryptSwitch/Program.cs @@ -0,0 +1,61 @@ +using GBX.NET; +using GBX.NET.Crypto; +using GBX.NET.LZO; +using System.Text; + +Gbx.LZO = new Lzo(); + +foreach (var arg in args) +{ + if (!File.Exists(arg)) + { + Console.WriteLine($"File not found: {arg}"); + continue; + } + + using var fsIn = File.OpenRead(arg); + + if (IsTextFile(fsIn)) + { + fsIn.Position = 0; + + using var r = new StreamReader(fsIn); + using var fsOut = File.Create(Path.GetFileNameWithoutExtension(arg) + ".cry"); + Cry.Encrypt(fsOut, r.ReadToEnd()); + Console.WriteLine("Encrypted!"); + } + else + { + fsIn.Position = 0; + + var decrypted = Cry.Decrypt(fsIn); + Console.WriteLine(decrypted); + File.WriteAllText(Path.GetFileNameWithoutExtension(arg) + ".txt", decrypted); + } +} + +static bool IsTextFile(Stream stream) +{ + try + { + using var r = new StreamReader(stream, Encoding.UTF8, true, 1024, true); + + while (!r.EndOfStream) + { + int charValue = r.Read(); + if (charValue == 0) + { + // file has null byte, considered binary + return false; + } + } + + // file doesn't contain null bytes or invalid UTF-8 sequences, considered text + return true; + } + catch (DecoderFallbackException) + { + // invalid UTF-8 sequence, considered binary + return false; + } +} \ No newline at end of file diff --git a/Tools/GbxDiscordBot/GbxDiscordBot.csproj b/Tools/GbxDiscordBot/GbxDiscordBot.csproj index f0a3caff4..451057566 100644 --- a/Tools/GbxDiscordBot/GbxDiscordBot.csproj +++ b/Tools/GbxDiscordBot/GbxDiscordBot.csproj @@ -11,7 +11,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -27,12 +27,12 @@ - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Tools/GbxExplorer/Client/GbxExplorer.Client.csproj b/Tools/GbxExplorer/Client/GbxExplorer.Client.csproj index b6b2079a6..58f36cb5b 100644 --- a/Tools/GbxExplorer/Client/GbxExplorer.Client.csproj +++ b/Tools/GbxExplorer/Client/GbxExplorer.Client.csproj @@ -8,8 +8,8 @@ - - + + diff --git a/Tools/GbxExplorer/Component/GbxExplorer.Component.csproj b/Tools/GbxExplorer/Component/GbxExplorer.Component.csproj index 684f0b34b..778044ed9 100644 --- a/Tools/GbxExplorer/Component/GbxExplorer.Component.csproj +++ b/Tools/GbxExplorer/Component/GbxExplorer.Component.csproj @@ -14,7 +14,7 @@ - + diff --git a/Tools/GbxExplorer/Server/GbxExplorer.Server.csproj b/Tools/GbxExplorer/Server/GbxExplorer.Server.csproj index f94a44f36..8dedca9b8 100644 --- a/Tools/GbxExplorer/Server/GbxExplorer.Server.csproj +++ b/Tools/GbxExplorer/Server/GbxExplorer.Server.csproj @@ -20,7 +20,7 @@ - + diff --git a/Tools/GbxExplorerOld/Client/GbxExplorerOld.Client.csproj b/Tools/GbxExplorerOld/Client/GbxExplorerOld.Client.csproj index 14eb25a3e..9c269c2ba 100644 --- a/Tools/GbxExplorerOld/Client/GbxExplorerOld.Client.csproj +++ b/Tools/GbxExplorerOld/Client/GbxExplorerOld.Client.csproj @@ -24,9 +24,9 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Tools/GbxExplorerOld/Server/GbxExplorerOld.Server.csproj b/Tools/GbxExplorerOld/Server/GbxExplorerOld.Server.csproj index bc48e6927..eacbea56e 100644 --- a/Tools/GbxExplorerOld/Server/GbxExplorerOld.Server.csproj +++ b/Tools/GbxExplorerOld/Server/GbxExplorerOld.Server.csproj @@ -11,7 +11,7 @@ - + diff --git a/Tools/GbxToJson/GbxToJson.csproj b/Tools/GbxToJson/GbxToJson.csproj index 7df1aa4f3..b81afa8d6 100644 --- a/Tools/GbxToJson/GbxToJson.csproj +++ b/Tools/GbxToJson/GbxToJson.csproj @@ -10,6 +10,7 @@ + diff --git a/Tools/GbxToJson/Program.cs b/Tools/GbxToJson/Program.cs index 232163f01..e081202f5 100644 --- a/Tools/GbxToJson/Program.cs +++ b/Tools/GbxToJson/Program.cs @@ -1,6 +1,7 @@ using GBX.NET; using GBX.NET.NewtonsoftJson; using GBX.NET.LZO; +using GBX.NET.ZLib; if (args.Length == 0) { @@ -10,6 +11,7 @@ } Gbx.LZO = new Lzo(); +Gbx.ZLib = new ZLib(); var gbx = Gbx.Parse(args[0]); diff --git a/Tools/MuxCryptSwitch/MuxCryptSwitch.csproj b/Tools/MuxCryptSwitch/MuxCryptSwitch.csproj new file mode 100644 index 000000000..7c58042cf --- /dev/null +++ b/Tools/MuxCryptSwitch/MuxCryptSwitch.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + true + true + + + + + + + diff --git a/Tools/MuxCryptSwitch/Program.cs b/Tools/MuxCryptSwitch/Program.cs new file mode 100644 index 000000000..267877e33 --- /dev/null +++ b/Tools/MuxCryptSwitch/Program.cs @@ -0,0 +1,39 @@ +using GBX.NET.Crypto; +using System.Security.Cryptography; +using System.Text; + +foreach (var arg in args) +{ + if (!File.Exists(arg)) + { + Console.WriteLine($"File not found: {arg}"); + continue; + } + + using var fsIn = File.OpenRead(arg); + var magic = new byte[9]; + fsIn.ReadExactly(magic); + fsIn.Position = 0; + + if (Encoding.ASCII.GetString(magic) == "NadeoFile") + { + using var fsOut = File.Create(Path.GetFileNameWithoutExtension(arg) + ".ogg"); + using var muxStream = Mux.Decrypt(fsIn); + + muxStream.CopyTo(fsOut); + + Console.WriteLine("Decrypted!"); + } + else + { + var salt = RandomNumberGenerator.GetInt32(int.MaxValue); + Console.WriteLine($"Salt: {salt}"); + + using var fsOut = File.Create(Path.GetFileNameWithoutExtension(arg) + ".mux"); + using var muxStream = Mux.Encrypt(fsOut, salt); + + fsIn.CopyTo(muxStream); + + Console.WriteLine("Encrypted!"); + } +} \ No newline at end of file