feat: rename cli

This commit is contained in:
tresabhi committed 2025-01-28 12:59:59 -06:00
1 parent 157fa0c1dd
commit 74a8df6842
18 files changed
+1419 -33

No files matched your search

+17
View File
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../submodules/CUE4Parse/CUE4Parse/CUE4Parse.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="DotNet.Glob" Version="3.1.3" />
<PackageReference Include="Google.Protobuf" Version="3.29.3" />
</ItemGroup>
</Project>
+26
View File
@@ -0,0 +1,26 @@
using BlitzKit.CLI.Models;
namespace BlitzKit.CLI.Functions
{
public static class Mangler
{
public static void Mangle()
{
BlitzProvider provider = new();
foreach (var nation in provider.RootDirectory.GetDirectory("Blitz/Content/Tanks").Directories)
{
if (nation.Key == "TankStub")
continue;
foreach (var tank in nation.Value.Directories)
{
if (!tank.Value.HasFile($"DA_{tank.Key}_Attributes.uasset"))
{
Console.WriteLine($"{nation.Key}/{tank.Key}");
}
}
}
}
}
}
+201
View File
@@ -0,0 +1,201 @@
using System.Text.Json;
using BlitzKit.CLI.Models;
using BlitzKit.CLI.Utils;
using CUE4Parse.FileProvider.Objects;
using CUE4Parse.UE4.Pak;
using CUE4Parse.UE4.Readers;
using DotNet.Globbing;
namespace BlitzKit.CLI.Functions
{
public static class Unpacker
{
private const string CONTENT_GROUP = "p14y73c7_p5e_b10gg3RS_big_play";
private const string VFS_PATH = "../../temp/vfs/";
// this may be different from the one provided in args so check this and not args[1]
// we don't wanna delete my local installation of the game when debugging haha!
private const string TEMP_DEPOT_DIR = "../../temp/depot/";
private const string WG_DLC_DOMAIN = "http://dl-wotblitz-gc.wargaming.net";
private const string BUILD_MANIFEST_OS = "Windows";
public static async Task Unpack(string[] args)
{
if (args.Length < 2)
{
throw new ArgumentException("Missing required initial paks directory");
}
UnpackDepot(args[1]);
await UnpackDLC();
}
private static async Task UnpackDLC()
{
string defaultDlcPath = Path.Combine(VFS_PATH, "Blitz/Config/DefaultDlc.ini");
IniParser iniParser = new(defaultDlcPath);
string? contentBuildId = iniParser.GetString("DefaultContentBuildId");
if (string.IsNullOrEmpty(contentBuildId))
{
throw new Exception("Failed to read content build id from DefaultDlc.ini");
}
Console.WriteLine($"Exploring DLC \"{contentBuildId}\" within group \"{CONTENT_GROUP}\"");
DlcManifest manifest = await FetchManifest(CONTENT_GROUP, contentBuildId);
List<PakFile> nonHdPaks = [];
List<PakFile> hdPaks = [];
Console.WriteLine("Filtering HD and non-HD containers");
foreach (var pakFile in manifest.PakFiles)
{
long chunkId = pakFile.ChunkId;
manifest.ChunkTags.TryGetValue(chunkId.ToString(), out var tags);
if (tags == null)
{
throw new Exception($"Failed to find tags for chunk {chunkId}");
}
bool isHd = tags.GameplayTags.Any(tag => tag.TagName == "Dlc.HdTextures");
if (isHd)
{
hdPaks.Add(pakFile);
}
else
{
nonHdPaks.Add(pakFile);
}
}
Console.WriteLine(
$"Found {nonHdPaks.Count} non-HD containers and {hdPaks.Count} HD containers"
);
Console.WriteLine($"Exergizing non-HD containers");
foreach (var pak in nonHdPaks)
{
await ExergizeManifestPakFile(pak);
}
Console.WriteLine($"Exergizing HD containers");
foreach (var pak in hdPaks)
{
await ExergizeManifestPakFile(pak);
}
}
private static async Task ExergizeManifestPakFile(PakFile pak)
{
Console.WriteLine($"Exergizing DLC container \"{pak.FileName}\"");
string url = $"{WG_DLC_DOMAIN}/dlc/{CONTENT_GROUP}/dlc/{pak.RelativeUrl}";
using HttpClient httpClient = new();
Stream httpStream = await httpClient.GetStreamAsync(url);
MemoryStream memoryStream = new();
await httpStream.CopyToAsync(memoryStream);
memoryStream.Seek(0, SeekOrigin.Begin);
FStreamArchive archive = new(url, memoryStream);
PakFileReader reader = new(archive);
Exergize(reader);
memoryStream.Dispose();
httpStream.Dispose();
archive.Dispose();
}
public static async Task<DlcManifest> FetchManifest(string group, string build)
{
string url =
$"{WG_DLC_DOMAIN}/dlc/{group}/dlc/BuildManifest-{BUILD_MANIFEST_OS}-{build}.json";
using HttpClient client = new();
string jsonString = await client.GetStringAsync(url);
DlcManifest manifest =
JsonSerializer.Deserialize<DlcManifest>(jsonString)
?? throw new Exception("Failed to deserialize manifest");
return manifest;
}
private static void UnpackDepot(string workingDirectory)
{
string paksDirectory = Path.Combine(workingDirectory, "Blitz/Content/Paks");
string[] containers = Directory.GetFiles(paksDirectory, "*.pak");
foreach (string container in containers)
{
Console.WriteLine($"Exergizing disk container \"{container}\"");
PakFileReader reader = new(container);
Exergize(reader);
}
if (Directory.Exists(TEMP_DEPOT_DIR))
{
Console.WriteLine("Deleting temp depots directory");
Directory.Delete(TEMP_DEPOT_DIR, true);
}
}
private static void Exergize(PakFileReader reader)
{
reader.Mount();
foreach (var file in reader.Files)
{
GameFile gameFile = file.Value;
bool exergizable = ShouldExergize(gameFile.Path);
if (!exergizable)
{
Console.WriteLine($"Exergize 🔴 {gameFile.Path}");
continue;
}
string writePath = Path.Combine(VFS_PATH, gameFile.Path);
if (File.Exists(writePath))
{
Console.WriteLine($"Exergize 🟡 {gameFile.Path}");
}
else
{
Console.WriteLine($"Exergize 🟢 {gameFile.Path}");
}
string? directory = Path.GetDirectoryName(writePath);
if (directory != null && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
File.WriteAllBytes(writePath, gameFile.Read());
}
}
private static readonly string[] ExergizableGlobsRaw =
[
"Blitz/Config/DefaultDlc.ini",
"Blitz/Content/Tanks/**",
"Blitz/Content/TankSets/*",
];
private static readonly Glob[] ExergizableGlobs = [.. ExergizableGlobsRaw.Select(Glob.Parse)];
private static bool ShouldExergize(string path)
{
// return true;
return ExergizableGlobs.Any(glob => glob.IsMatch(path));
}
}
}
+87
View File
@@ -0,0 +1,87 @@
using CUE4Parse.FileProvider;
using CUE4Parse.FileProvider.Objects;
namespace BlitzKit.CLI.Models
{
public class BlitzProvider : DefaultFileProvider
{
private static readonly string LOCAL_USER = "coola";
private static readonly string LOCAL_DLC_CONTAINERS_PATH =
$"C:/Users/{LOCAL_USER}/AppData/Local/Blitz/Saved/PersistentDownloadDir/Dlc/PakCache";
private static readonly string LOCAL_BUNDLED_CONTAINERS_PATH =
"C:/Program Files (x86)/Steam/steamapps/common/World of Tanks Blitz Playtest/Blitz/Content/Paks";
public readonly VfsDirectory RootDirectory = new();
public BlitzProvider()
: base(
directory: new DirectoryInfo(LOCAL_BUNDLED_CONTAINERS_PATH),
extraDirectories: [new(LOCAL_DLC_CONTAINERS_PATH)],
searchOption: SearchOption.TopDirectoryOnly
)
{
Initialize();
Mount();
foreach (var file in Files)
{
var gameFile = file.Value;
var path = gameFile.Path.Split('/');
var lastPathSegment = path.Last();
var directory = RootDirectory;
foreach (var segment in path)
{
bool isLastSegment = lastPathSegment == segment;
if (isLastSegment)
{
directory.AddFile(segment, gameFile);
}
else if (directory.HasDirectory(segment))
{
directory = directory.GetDirectory(segment);
}
else
{
VfsDirectory newDirectory = new();
directory.AddDirectory(segment, newDirectory);
directory = newDirectory;
}
}
}
}
}
public class VfsDirectory
{
public Dictionary<string, VfsDirectory> Directories = [];
public Dictionary<string, GameFile> Files = [];
public void AddFile(string name, GameFile file) => Files.TryAdd(name, file);
public bool HasFile(string name) => Files.ContainsKey(name);
public GameFile GetFile(string name) => Files[name];
public bool HasDirectory(string name) => Directories.ContainsKey(name);
public VfsDirectory GetDirectory(string name)
{
var segments = name.Split('/');
var directory = this;
foreach (var segment in segments)
{
directory = directory.GetImmediateDirectory(segment);
}
return directory;
}
private VfsDirectory GetImmediateDirectory(string name) => Directories[name];
public void AddDirectory(string name, VfsDirectory directory) =>
Directories.Add(name, directory);
}
}
+61
View File
@@ -0,0 +1,61 @@
using System.Text.Json.Serialization;
namespace BlitzKit.CLI.Models
{
public partial class DlcManifest
{
[JsonPropertyName("contentBuildId")]
public string ContentBuildId { get; set; }
[JsonPropertyName("pakFiles")]
public List<PakFile> PakFiles { get; set; }
[JsonPropertyName("chunkTags")]
public Dictionary<string, ChunkTag> ChunkTags { get; set; }
[JsonPropertyName("chunkDependencies")]
public List<ChunkDependency> ChunkDependencies { get; set; }
}
public partial class ChunkDependency
{
[JsonPropertyName("chunkId")]
public long ChunkId { get; set; }
[JsonPropertyName("parentChunkId")]
public long ParentChunkId { get; set; }
}
public partial class ChunkTag
{
[JsonPropertyName("gameplayTags")]
public List<GameplayTag> GameplayTags { get; set; }
}
public partial class GameplayTag
{
[JsonPropertyName("tagName")]
public string TagName { get; set; }
}
public partial class PakFile
{
[JsonPropertyName("fileName")]
public string FileName { get; set; }
[JsonPropertyName("fileSize")]
public long FileSize { get; set; }
[JsonPropertyName("fileVersion")]
public string FileVersion { get; set; }
[JsonPropertyName("chunkId")]
public long ChunkId { get; set; }
[JsonPropertyName("relativeUrl")]
public string RelativeUrl { get; set; }
[JsonPropertyName("availabilityMask")]
public long AvailabilityMask { get; set; }
}
}
+13
View File
@@ -0,0 +1,13 @@
namespace BlitzKit.CLI.Models
{
public class LocalManifestPakFile
{
public long FileSize { get; set; }
public required string FileHash { get; set; }
}
public class LocalManifest
{
public required Dictionary<string, LocalManifestPakFile> PakFiles { get; set; }
}
}
+33
View File
@@ -0,0 +1,33 @@
using BlitzKit.CLI.Functions;
using BlitzKit.CLI.Utils;
namespace BlitzKit.CLI
{
class Program
{
static async Task Main(string[] args)
{
await AgnosticHelpers.Initialize();
switch (args[0])
{
case "unpack":
{
await Unpacker.Unpack(args);
break;
}
case "mangle":
{
Mangler.Mangle();
break;
}
default:
{
throw new ArgumentException("Invalid command");
}
}
}
}
}
+120
View File
@@ -0,0 +1,120 @@
using System.IO.Compression;
using System.Runtime.InteropServices;
using CUE4Parse.Compression;
namespace BlitzKit.CLI.Utils
{
public static class AgnosticHelpers
{
public const string BINARY_DIR = "../../temp/";
public const string ZLIB_BINARY_URL_PREFIX =
"https://github.com/NotOfficer/Zlib-ng.NET/releases/download/1.0.0";
public const string OODLE_BINARY_URL_PREFIX =
"https://github.com/WorkingRobot/OodleUE/releases/download/2024-11-01-726";
public static async Task Initialize()
{
if (!Directory.Exists(BINARY_DIR))
{
Directory.CreateDirectory(BINARY_DIR);
}
InitializeZlib();
await InitializeOodle();
}
public static async Task InitializeOodle()
{
string url;
string extension;
string entry;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
url = $"{OODLE_BINARY_URL_PREFIX}/msvc.zip";
entry = "bin/Release/oodle-data-shared.dll";
extension = "dll";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
url = $"{OODLE_BINARY_URL_PREFIX}/gcc.zip";
entry = "lib/Release/liboodle-data-shared.so";
extension = "so";
}
else
{
throw new PlatformNotSupportedException();
}
string fileName = $"oodle.{extension}";
string filePath = Path.Combine(BINARY_DIR, fileName);
if (File.Exists(filePath))
{
Console.WriteLine($"Oodle found at {filePath}; using that");
OodleHelper.Initialize(filePath);
return;
}
Console.WriteLine(
$"Oodle not found; downloading to {filePath} from {url} and extracting {entry}"
);
HttpClient client = new();
using var response = await client.GetAsync(url).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
await using var responseStream = await response
.Content.ReadAsStreamAsync()
.ConfigureAwait(false);
using var zip = new ZipArchive(responseStream, ZipArchiveMode.Read);
var zipEntry = zip.GetEntry(entry);
ArgumentNullException.ThrowIfNull(zipEntry, "oodle entry in zip not found");
await using var entryStream = zipEntry.Open();
await using var fs = File.Create(filePath);
await entryStream.CopyToAsync(fs).ConfigureAwait(false);
OodleHelper.Initialize(filePath);
}
public static void InitializeZlib()
{
string url;
string extension;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
url = $"{ZLIB_BINARY_URL_PREFIX}/zlib-ng2.dll";
extension = "dll";
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
url = $"{ZLIB_BINARY_URL_PREFIX}/libz-ng.so";
extension = "so";
}
else
{
throw new PlatformNotSupportedException();
}
string fileName = $"zlib.{extension}";
string filePath = Path.Combine(BINARY_DIR, fileName);
if (File.Exists(filePath))
{
Console.WriteLine($"Zlib found at {filePath}; using that");
}
else
{
Console.WriteLine($"Zlib not found; downloading to {filePath} from {url}");
ZlibHelper.DownloadDll(filePath, url);
}
ZlibHelper.Initialize(filePath);
}
}
}
@@ -0,0 +1,95 @@
namespace BlitzKit.CLI.Utils
{
public class ExplicitlySizedStream(Stream stream, long length) : Stream
{
private readonly Stream _stream = stream;
private readonly long _length = length;
private long _position = 0;
public override bool CanRead => _stream.CanRead;
public override bool CanSeek => _stream.CanSeek;
public override bool CanWrite => _stream.CanWrite;
public override long Length => _length;
public override long Position
{
get => _position;
set => _position = value;
}
public override void Flush() => _stream.Flush();
public override int Read(byte[] buffer, int offset, int count)
{
if (_position >= _length)
return 0; // End of stream
var bytesRead = _stream.Read(buffer, offset, count);
_position += bytesRead;
return bytesRead;
}
public override long Seek(long offset, SeekOrigin origin)
{
long targetPosition = origin switch
{
SeekOrigin.Begin => offset,
SeekOrigin.Current => _position + offset,
SeekOrigin.End => _length + offset,
_ => throw new ArgumentOutOfRangeException(nameof(origin), "Invalid SeekOrigin value."),
};
if (targetPosition < 0 || targetPosition > _length)
throw new ArgumentOutOfRangeException(nameof(offset), "Seek position is out of range.");
// Simulate seek by discarding data up to the target position
if (targetPosition != _position)
{
if (targetPosition < _position)
{
throw new NotSupportedException(
"Backward seeking is not supported for non-seekable streams."
);
}
var toSkip = targetPosition - _position;
Skip(toSkip);
_position = targetPosition;
}
return _position;
}
private void Skip(long bytes)
{
byte[] buffer = new byte[8192];
while (bytes > 0)
{
int toRead = (int)Math.Min(buffer.Length, bytes);
int read = _stream.Read(buffer, 0, toRead);
if (read == 0)
throw new EndOfStreamException();
bytes -= read;
}
}
public override void SetLength(long value) => throw new NotSupportedException();
public override void Write(byte[] buffer, int offset, int count)
{
_stream.Write(buffer, offset, count);
_position += count;
}
// public override void Flush() => _stream.Flush();
// public override int Read(byte[] buffer, int offset, int count) =>
// _stream.Read(buffer, offset, count);
// public override long Seek(long offset, SeekOrigin origin) => _stream.Seek(offset, origin);
// public override void SetLength(long value) => throw new NotSupportedException();
// public override void Write(byte[] buffer, int offset, int count) =>
// _stream.Write(buffer, offset, count);
}
}
+51
View File
@@ -0,0 +1,51 @@
namespace BlitzKit.CLI.Utils
{
public class IniParser
{
private readonly Dictionary<string, string> strings = [];
private readonly Dictionary<string, List<string>> arrays = [];
public IniParser(string file)
{
string content = File.ReadAllText(file);
foreach (string line in content.Split('\n'))
{
if (string.IsNullOrWhiteSpace(line))
continue;
string[] parts = line.Split('=');
if (parts.Length < 2)
continue;
string key = parts[0].Trim();
string value = string.Join("=", parts, 1, parts.Length - 1).Trim();
if (key.StartsWith('+'))
{
string keySuffix = key[1..];
if (arrays.TryGetValue(keySuffix, out List<string>? listValue))
{
listValue.Add(value);
}
else
{
arrays[keySuffix] = [value];
}
}
else
{
// Overwrite existing key instead of throwing an exception
strings[key] = value;
}
}
}
public string? GetString(string key) =>
strings.TryGetValue(key, out string? value) ? value : null;
public List<string>? GetArray(string key) =>
arrays.TryGetValue(key, out List<string>? value) ? value : null;
}
}