diff --git a/.github/workflows/build-nkxtool.yml b/.github/workflows/build-nkxtool.yml index b6874a1..f49ae9c 100644 --- a/.github/workflows/build-nkxtool.yml +++ b/.github/workflows/build-nkxtool.yml @@ -3,10 +3,10 @@ name: Build NkxTool Executable # Nom du workflow affiché sur GitHub on: push: branches: - - main # Déclenche le workflow sur les pushes vers la branche 'main' + - dev-nki # Déclenche le workflow sur les pushes vers la branche 'main' pull_request: branches: - - main # Déclenche le workflow sur les pull requests vers la branche 'main' + - dev-nki # Déclenche le workflow sur les pull requests vers la branche 'main' jobs: build: diff --git a/nki/NkiChunkScanner.cs b/nki/NkiChunkScanner.cs new file mode 100644 index 0000000..9f19565 --- /dev/null +++ b/nki/NkiChunkScanner.cs @@ -0,0 +1,179 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.IO.Compression; +using System.Text; + +namespace NkiTool +{ + public class NkiRegion + { + public string Tag = ""; + public long Offset; + public int Size; + public byte[] Payload = Array.Empty(); + public bool IsInflated; + public List Children = new(); + public List ZlibCandidateOffsets = new(); + } + + public static class NkiChunkScanner + { + // Fenêtre de recherche d'en-tête ZLIB (0x78 ..) à l'intérieur d'un buffer. + // 234 Ko de fichier -> on peut se permettre de scanner large sans souci de perf. + private const int ZlibSearchWindow = 1_000_000; + + public static List Scan(byte[] data) + { + var regions = new List(); + TryScanAsChunks(data, 0, data.Length, regions); + return regions; + } + + private static void TryScanAsChunks(byte[] data, int start, int end, List outRegions) + { + int pos = start; + while (pos + 8 <= end) + { + string tag = SafeAscii(data, pos, 4); + uint size = BitConverter.ToUInt32(data, pos + 4); + + if (size == 0 || pos + 8 + size > end || size > 200_000_000) + { + var opaque = new NkiRegion + { + Tag = "RAW", + Offset = pos, + Size = end - pos, + Payload = Slice(data, pos, end - pos) + }; + + // Correctif : on tente quand même de trouver et décompresser + // un flux ZLIB n'importe où dans ce bloc opaque, plutôt que + // d'abandonner silencieusement. + TryInflateAndRecurse(opaque); + ScanForKnownMarkers(opaque); + + outRegions.Add(opaque); + return; + } + + var region = new NkiRegion + { + Tag = tag, + Offset = pos, + Size = (int)size, + Payload = Slice(data, pos + 8, (int)size) + }; + + TryInflateAndRecurse(region); + outRegions.Add(region); + + pos += 8 + (int)size; + } + + if (pos < end) + { + var tail = new NkiRegion + { + Tag = "TAIL", + Offset = pos, + Size = end - pos, + Payload = Slice(data, pos, end - pos) + }; + TryInflateAndRecurse(tail); + outRegions.Add(tail); + } + } + + private static void TryInflateAndRecurse(NkiRegion region) + { + var (inflated, foundAtOffset) = TryZlibInflateAnywhere(region.Payload); + if (inflated != null) + { + region.IsInflated = true; + region.ZlibCandidateOffsets.Add(region.Offset + foundAtOffset); + TryScanAsChunks(inflated, 0, inflated.Length, region.Children); + region.Payload = inflated; + } + } + + /// + /// Recherche un en-tête ZLIB (0x78 suivi d'un second octet plausible) + /// n'importe où dans le buffer (pas seulement au début), sur une fenêtre + /// raisonnable, et tente une décompression à chaque candidat trouvé. + /// + public static (byte[]? data, int offset) TryZlibInflateAnywhere(byte[] buffer) + { + int limit = Math.Min(buffer.Length - 2, ZlibSearchWindow); + for (int offset = 0; offset < limit; offset++) + { + if (buffer[offset] != 0x78) continue; + + byte second = buffer[offset + 1]; + // Bytes valides usuels après 0x78 pour un flux zlib : 0x01, 0x5E, 0x9C, 0xDA + if (second != 0x01 && second != 0x5E && second != 0x9C && second != 0xDA) continue; + + try + { + using var input = new MemoryStream(buffer, offset, buffer.Length - offset); + using var zlib = new ZLibStream(input, CompressionMode.Decompress); + using var output = new MemoryStream(); + zlib.CopyTo(output); + var result = output.ToArray(); + if (result.Length > 0) return (result, offset); + } + catch + { + // pas un flux valide à cet offset, on continue + } + } + return (null, -1); + } + + private static readonly string[] KnownMarkers = { "hsin", "DSIN", "2SAM", "PRES", "PROG", "PLST", "FNTB", "PARS" }; + + /// + /// Recherche des tags/marqueurs connus (issus de la documentation + /// communautaire du format NI DSIN) n'importe où dans un bloc, pour + /// aider à localiser la structure même sans specs officielles. + /// + private static void ScanForKnownMarkers(NkiRegion region) + { + foreach (var marker in KnownMarkers) + { + var markerBytes = Encoding.ASCII.GetBytes(marker); + for (int i = 0; i + markerBytes.Length <= region.Payload.Length; i++) + { + bool match = true; + for (int j = 0; j < markerBytes.Length; j++) + { + if (region.Payload[i + j] != markerBytes[j]) { match = false; break; } + } + if (match) + { + region.ZlibCandidateOffsets.Add(-(region.Offset + i)); // négatif = marqueur texte, pas zlib + } + } + } + } + + private static string SafeAscii(byte[] data, int offset, int len) + { + var sb = new StringBuilder(); + for (int i = 0; i < len; i++) + { + byte b = data[offset + i]; + sb.Append(b >= 32 && b < 127 ? (char)b : '.'); + } + return sb.ToString(); + } + + private static byte[] Slice(byte[] data, int offset, int len) + { + var result = new byte[len]; + Array.Copy(data, offset, result, 0, len); + return result; + } + } +} diff --git a/nki/NkiDumpCommand.cs b/nki/NkiDumpCommand.cs new file mode 100644 index 0000000..e22867f --- /dev/null +++ b/nki/NkiDumpCommand.cs @@ -0,0 +1,101 @@ +using System; +using System.IO; +using System.Text; + +namespace NkiTool +{ + public static class NkiDumpCommand + { + public static int Run(string path, string? outPath) + { + byte[] data = File.ReadAllBytes(path); + var sb = new StringBuilder(); + + sb.AppendLine($"Fichier: {path}"); + sb.AppendLine($"Taille: {data.Length} octets"); + sb.AppendLine($"En-tête (16 premiers octets, hex): {BitConverter.ToString(data, 0, Math.Min(16, data.Length))}"); + sb.AppendLine(); + + sb.AppendLine("=== Arborescence de chunks détectée ==="); + var regions = NkiChunkScanner.Scan(data); + DumpRegions(regions, 0, sb); + + sb.AppendLine(); + sb.AppendLine("=== Références d'échantillons trouvées (.wav / .ncw) ==="); + var found = StringExtractor.FindSampleReferences(data); + foreach (var f in found) + { + sb.AppendLine($" offset={f.Offset,-10} encodage={f.Encoding,-8} valeur=\"{f.Value}\""); + } + + sb.AppendLine(); + sb.AppendLine("=== Scan récursif complémentaire dans chaque région (y compris blocs décompressés) ==="); + ScanRegionsForStrings(regions, sb); + + int totalFound = found.Count + CountNestedStrings(regions); + if (totalFound == 0) + { + sb.AppendLine(); + sb.AppendLine("AUCUNE référence .wav/.ncw trouvée nulle part (racine + sous-blocs décompressés)."); + sb.AppendLine("Voir la liste des marqueurs/offsets ZLIB candidats ci-dessus pour diagnostiquer."); + } + + string report = sb.ToString(); + Console.WriteLine(report); + + if (outPath != null) + { + File.WriteAllText(outPath, report); + Console.WriteLine($"\nRapport écrit dans: {outPath}"); + } + + return 0; + } + + private static void DumpRegions(System.Collections.Generic.List regions, int depth, StringBuilder sb) + { + string indent = new string(' ', depth * 2); + foreach (var r in regions) + { + string flag = r.IsInflated ? " [ZLIB -> décompressé]" : ""; + sb.AppendLine($"{indent}- tag=\"{r.Tag}\" offset={r.Offset} taille={r.Size}{flag}"); + + foreach (var candidate in r.ZlibCandidateOffsets) + { + if (candidate >= 0) + sb.AppendLine($"{indent} [candidat ZLIB trouvé à l'offset absolu {candidate}]"); + else + sb.AppendLine($"{indent} [marqueur texte connu trouvé à l'offset absolu {-candidate}]"); + } + + if (r.Children.Count > 0) + DumpRegions(r.Children, depth + 1, sb); + } + } + + private static void ScanRegionsForStrings(System.Collections.Generic.List regions, StringBuilder sb) + { + foreach (var r in regions) + { + var found = StringExtractor.FindSampleReferences(r.Payload, r.Offset); + foreach (var f in found) + { + sb.AppendLine($" [chunk \"{r.Tag}\"{(r.IsInflated ? " décompressé" : "")}] offset={f.Offset,-10} encodage={f.Encoding,-8} valeur=\"{f.Value}\""); + } + if (r.Children.Count > 0) + ScanRegionsForStrings(r.Children, sb); + } + } + + private static int CountNestedStrings(System.Collections.Generic.List regions) + { + int count = 0; + foreach (var r in regions) + { + count += StringExtractor.FindSampleReferences(r.Payload, r.Offset).Count; + count += CountNestedStrings(r.Children); + } + return count; + } + } +} \ No newline at end of file diff --git a/nki/NkiInfoScan.cs b/nki/NkiInfoScan.cs new file mode 100644 index 0000000..4cbd7a3 --- /dev/null +++ b/nki/NkiInfoScan.cs @@ -0,0 +1,185 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; + +namespace NkiTool +{ + public static class NkiInfoScan + { + private static readonly Regex VersionPattern = + new Regex(@"\b\d{1,3}\.\d{1,3}(\.\d{1,5}){0,2}\b", RegexOptions.Compiled); + + public static int Run(string path, int scanLength, string? outPath) + { + byte[] data = File.ReadAllBytes(path); + int limit = Math.Min(scanLength, data.Length); + var sb = new StringBuilder(); + + sb.AppendLine($"Fichier: {path}"); + sb.AppendLine($"Taille totale: {data.Length} octets"); + sb.AppendLine($"Zone scannée: 0 à {limit} (sur {scanLength} demandés)"); + sb.AppendLine(); + + sb.AppendLine("=== Chaînes candidates de version (ASCII) ==="); + var asciiHits = ScanAscii(data, limit); + foreach (var hit in asciiHits) + sb.AppendLine($" offset={hit.Offset,-6} valeur=\"{hit.Value}\" (contexte: \"{hit.Context}\")"); + if (asciiHits.Count == 0) sb.AppendLine(" (aucune)"); + + sb.AppendLine(); + sb.AppendLine("=== Chaînes candidates de version (UTF-16LE) ==="); + var utf16Hits = ScanUtf16Le(data, limit); + foreach (var hit in utf16Hits) + sb.AppendLine($" offset={hit.Offset,-6} valeur=\"{hit.Value}\" (contexte: \"{hit.Context}\")"); + if (utf16Hits.Count == 0) sb.AppendLine(" (aucune)"); + + sb.AppendLine(); + sb.AppendLine("=== Toutes les chaînes lisibles trouvées (>=4 caractères), pour contexte manuel ==="); + foreach (var s in ExtractAllPrintableStrings(data, limit, "ASCII")) + sb.AppendLine($" [ASCII] offset={s.Offset,-6} \"{s.Value}\""); + foreach (var s in ExtractAllPrintableStrings(data, limit, "UTF16LE")) + sb.AppendLine($" [UTF16LE] offset={s.Offset,-6} \"{s.Value}\""); + + string report = sb.ToString(); + Console.WriteLine(report); + + if (outPath != null) + { + File.WriteAllText(outPath, report); + Console.WriteLine($"\nRapport écrit dans: {outPath}"); + } + + return 0; + } + + private record Hit(long Offset, string Value, string Context); + + private static List ScanAscii(byte[] data, int limit) + { + var hits = new List(); + var sb = new StringBuilder(); + long start = 0; + + for (int i = 0; i < limit; i++) + { + byte b = data[i]; + bool printable = b >= 32 && b < 127; + if (printable) + { + if (sb.Length == 0) start = i; + sb.Append((char)b); + } + else + { + FlushAsciiCandidate(sb, start, hits); + sb.Clear(); + } + } + FlushAsciiCandidate(sb, start, hits); + return hits; + } + + private static void FlushAsciiCandidate(StringBuilder sb, long start, List hits) + { + if (sb.Length < 3) return; + string s = sb.ToString(); + foreach (Match m in VersionPattern.Matches(s)) + { + hits.Add(new Hit(start + m.Index, m.Value, s)); + } + } + + private static List ScanUtf16Le(byte[] data, int limit) + { + var hits = new List(); + var sb = new StringBuilder(); + long start = 0; + int i = 0; + + while (i + 1 < limit) + { + char c = (char)(data[i] | (data[i + 1] << 8)); + bool printable = c >= 32 && c < 127; + if (printable) + { + if (sb.Length == 0) start = i; + sb.Append(c); + i += 2; + } + else + { + FlushUtf16Candidate(sb, start, hits); + sb.Clear(); + i += 1; + } + } + FlushUtf16Candidate(sb, start, hits); + return hits; + } + + private static void FlushUtf16Candidate(StringBuilder sb, long start, List hits) + { + if (sb.Length < 3) return; + string s = sb.ToString(); + foreach (Match m in VersionPattern.Matches(s)) + { + hits.Add(new Hit(start + m.Index * 2, m.Value, s)); + } + } + + private record StringHit(long Offset, string Value); + + private static List ExtractAllPrintableStrings(byte[] data, int limit, string encoding) + { + var results = new List(); + var sb = new StringBuilder(); + long start = 0; + + if (encoding == "ASCII") + { + for (int i = 0; i < limit; i++) + { + byte b = data[i]; + bool printable = b >= 32 && b < 127; + if (printable) + { + if (sb.Length == 0) start = i; + sb.Append((char)b); + } + else + { + if (sb.Length >= 4) results.Add(new StringHit(start, sb.ToString())); + sb.Clear(); + } + } + if (sb.Length >= 4) results.Add(new StringHit(start, sb.ToString())); + } + else + { + int i = 0; + while (i + 1 < limit) + { + char c = (char)(data[i] | (data[i + 1] << 8)); + bool printable = c >= 32 && c < 127; + if (printable) + { + if (sb.Length == 0) start = i; + sb.Append(c); + i += 2; + } + else + { + if (sb.Length >= 4) results.Add(new StringHit(start, sb.ToString())); + sb.Clear(); + i += 1; + } + } + if (sb.Length >= 4) results.Add(new StringHit(start, sb.ToString())); + } + + return results; + } + } +} \ No newline at end of file diff --git a/nki/NkiVersionCommand.cs b/nki/NkiVersionCommand.cs new file mode 100644 index 0000000..0161b21 --- /dev/null +++ b/nki/NkiVersionCommand.cs @@ -0,0 +1,120 @@ +using System; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; + +namespace NkiTool +{ + /// + /// Commande dédiée : extrait et affiche UNIQUEMENT le numéro de version + /// minimale requise, tel que trouvé en UTF-16LE dans la zone claire + /// de l'en-tête du NKI (empiriquement observé à l'offset 389 sur les + /// deux échantillons testés, mais recherché dynamiquement pour rester + /// robuste si la position varie légèrement selon les métadonnées). + /// + /// Sortie sur stdout : uniquement la valeur (ex: "8.0.0.0"), rien d'autre, + /// pour un usage direct dans un script PowerShell : + /// $version = & NkxTool.exe nki-version "Instrument.nki" + /// + /// Code de retour : 0 si trouvé, 1 si non trouvé ou erreur. + /// + public static class NkiVersionCommand + { + private static readonly Regex VersionPattern = + new Regex(@"\b\d{1,3}\.\d{1,3}(\.\d{1,5}){0,2}\b", RegexOptions.Compiled); + + private const int ScanLength = 4096; + private const int PreferredOffsetMin = 300; + private const int PreferredOffsetMax = 500; + + public static int Run(string path, bool verbose) + { + byte[] data; + try + { + data = File.ReadAllBytes(path); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error: impossible de lire le fichier ({ex.Message})"); + return 1; + } + + int limit = Math.Min(ScanLength, data.Length); + var candidates = ScanUtf16Le(data, limit); + + if (candidates.Count == 0) + { + Console.Error.WriteLine("Error: aucune chaîne de version trouvée dans les 4096 premiers octets."); + return 1; + } + + // Priorité : un candidat situé dans la fenêtre 300-500, cohérente + // avec les deux échantillons observés (offset 389 dans les deux cas). + (long Offset, string Value)? best = null; + foreach (var c in candidates) + { + if (c.Offset >= PreferredOffsetMin && c.Offset <= PreferredOffsetMax) + { + best = c; + break; + } + } + + // Repli : premier candidat trouvé, si rien dans la fenêtre préférée. + best ??= candidates[0]; + + if (verbose) + { + Console.Error.WriteLine($"[diagnostic] {candidates.Count} candidat(s) trouvé(s) au total."); + foreach (var c in candidates) + { + string marker = (c.Offset == best.Value.Offset) ? " <-- retenu" : ""; + Console.Error.WriteLine($"[diagnostic] offset={c.Offset,-6} valeur=\"{c.Value}\"{marker}"); + } + } + + // Seule ligne envoyée sur stdout : la valeur, pour capture facile en script. + Console.WriteLine(best.Value.Value); + return 0; + } + + private static System.Collections.Generic.List<(long Offset, string Value)> ScanUtf16Le(byte[] data, int limit) + { + var hits = new System.Collections.Generic.List<(long, string)>(); + var sb = new StringBuilder(); + long start = 0; + int i = 0; + + while (i + 1 < limit) + { + char c = (char)(data[i] | (data[i + 1] << 8)); + bool printable = c >= 32 && c < 127; + if (printable) + { + if (sb.Length == 0) start = i; + sb.Append(c); + i += 2; + } + else + { + Flush(sb, start, hits); + sb.Clear(); + i += 1; + } + } + Flush(sb, start, hits); + return hits; + } + + private static void Flush(StringBuilder sb, long start, System.Collections.Generic.List<(long, string)> hits) + { + if (sb.Length < 3) return; + string s = sb.ToString(); + foreach (Match m in VersionPattern.Matches(s)) + { + hits.Add((start + m.Index * 2, m.Value)); + } + } + } +} diff --git a/nki/StringExtractor.cs b/nki/StringExtractor.cs new file mode 100644 index 0000000..1a8cbae --- /dev/null +++ b/nki/StringExtractor.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace NkiTool +{ + public record FoundString(long Offset, string Encoding, string Value); + + /// + /// Extrait toutes les chaînes plausibles (ASCII et UTF-16LE) contenant + /// ".wav" ou ".ncw" dans un buffer, avec leur offset absolu. + /// Objectif : localiser empiriquement, sur vos vrais fichiers, où et + /// comment sont stockés les noms d'échantillons (encodage, longueur de + /// champ fixe ou variable, présence d'un chemin complet ou du nom seul). + /// + public static class StringExtractor + { + private static readonly string[] Needles = { ".wav", ".WAV", ".ncw", ".NCW" }; + + public static List FindSampleReferences(byte[] data, long baseOffset = 0) + { + var results = new List(); + ScanAscii(data, baseOffset, results); + ScanUtf16Le(data, baseOffset, results); + return results; + } + + private static void ScanAscii(byte[] data, long baseOffset, List results) + { + var sb = new StringBuilder(); + long stringStart = 0; + for (int i = 0; i < data.Length; i++) + { + byte b = data[i]; + if (b >= 32 && b < 127) + { + if (sb.Length == 0) stringStart = i; + sb.Append((char)b); + } + else + { + FlushIfMatch(sb, stringStart, baseOffset, "ASCII", results); + sb.Clear(); + } + } + FlushIfMatch(sb, stringStart, baseOffset, "ASCII", results); + } + + private static void ScanUtf16Le(byte[] data, long baseOffset, List results) + { + var sb = new StringBuilder(); + long stringStart = 0; + int i = 0; + while (i + 1 < data.Length) + { + char c = (char)(data[i] | (data[i + 1] << 8)); + bool printable = c >= 32 && c < 127; + if (printable) + { + if (sb.Length == 0) stringStart = i; + sb.Append(c); + i += 2; + } + else + { + FlushIfMatch(sb, stringStart, baseOffset, "UTF16LE", results); + sb.Clear(); + i += 1; // décalage impair pour ne pas rater un flux mal aligné + } + } + FlushIfMatch(sb, stringStart, baseOffset, "UTF16LE", results); + } + + private static void FlushIfMatch(StringBuilder sb, long stringStart, long baseOffset, string encoding, List results) + { + if (sb.Length < 5) return; + string s = sb.ToString(); + foreach (var needle in Needles) + { + if (s.Contains(needle)) + { + results.Add(new FoundString(baseOffset + stringStart, encoding, s)); + break; + } + } + } + } +} diff --git a/program.cs b/program.cs index 0f26864..5446688 100644 --- a/program.cs +++ b/program.cs @@ -6,7 +6,7 @@ using System.Threading; using System.Xml; using System.Text.RegularExpressions; - +using NkiTool; public class Program { private const string PluginDllName = "inNKX.wcx64"; @@ -182,6 +182,82 @@ private static int RunTool(string[] args) return UpdateUserDb(customXmlPath); } + // Commande dump : diagnostic lecture seule d'un fichier NKI + if (operation == "dump") + { + if (argsList.Count < 2) + { + Console.WriteLine("Usage: NkxTool dump [--out report.txt]"); + return 1; + } + + string nkiPath = Path.GetFullPath(argsList[1]); + if (!File.Exists(nkiPath)) + { + Console.WriteLine($"Error: The source file '{nkiPath}' does not exist."); + return 1; + } + + string? outReport = null; + int outIndex = argsList.IndexOf("--out"); + if (outIndex >= 0 && outIndex + 1 < argsList.Count) + { + outReport = Path.GetFullPath(argsList[outIndex + 1]); + } + + return NkiDumpCommand.Run(nkiPath, outReport); + } + if (operation == "nki-infoscan") + { + if (argsList.Count < 2) + { + Console.WriteLine("Usage: NkxTool nki-infoscan [--length N] [--out report.txt]"); + return 1; + } + + string nkiPath = Path.GetFullPath(argsList[1]); + if (!File.Exists(nkiPath)) + { + Console.WriteLine($"Error: The source file '{nkiPath}' does not exist."); + return 1; + } + + int scanLength = 4096; + int lenIndex = argsList.IndexOf("--length"); + if (lenIndex >= 0 && lenIndex + 1 < argsList.Count) + { + int.TryParse(argsList[lenIndex + 1], out scanLength); + } + + string? outReport = null; + int outIndex = argsList.IndexOf("--out"); + if (outIndex >= 0 && outIndex + 1 < argsList.Count) + { + outReport = Path.GetFullPath(argsList[outIndex + 1]); + } + + return NkiInfoScan.Run(nkiPath, scanLength, outReport); + } + if (operation == "nki-version") + { + if (argsList.Count < 2) + { + Console.WriteLine("Usage: NkxTool nki-version [-v]"); + return 1; + } + + string nkiPath = Path.GetFullPath(argsList[1]); + if (!File.Exists(nkiPath)) + { + Console.Error.WriteLine($"Error: The source file '{nkiPath}' does not exist."); + return 1; + } + + bool verbose = argsList.Contains("-v") || argsList.Contains("--verbose"); + + return NkiVersionCommand.Run(nkiPath, verbose); + } + if (argsList.Count < 2) { ShowUsage(); @@ -262,15 +338,23 @@ private static void ShowUsage() Console.WriteLine(" NkxTool pack [rootPath]"); Console.WriteLine(" NkxTool list [outputList.txt]"); Console.WriteLine(" NkxTool update [-f ]"); + Console.WriteLine(" NkxTool dump [--out rapport.txt]"); + Console.WriteLine(" NkxTool nki-infoscan [--length N] [--out report.txt]"); + Console.WriteLine(" NkxTool nki-version [-v]"); Console.WriteLine(); Console.WriteLine("Examples:"); Console.WriteLine(" NkxTool unpack archive.nkx output_folder"); Console.WriteLine(" NkxTool update"); Console.WriteLine(" NkxTool update -f \"C:\\Program Files\\Common Files\\Native Instruments\\Service Center\\NativeAccess.xml\""); + Console.WriteLine(" NkxTool nki-infoscan \"Piano.nki\" --length 4096 --out version_report.txt"); + Console.WriteLine(" NkxTool nki-version \"Piano.nki\""); Console.WriteLine(); Console.WriteLine("Options:"); Console.WriteLine(" -y : Overwrite existing files without skipping (unpack only)"); Console.WriteLine(" -f : Specify a custom path to NativeAccess.xml (update only)"); + Console.WriteLine(" --length N : Number of bytes to scan from the start of the file (nki-infoscan only, default 4096)"); + Console.WriteLine(" --out : Write the report to a file instead of (or in addition to) the console"); + Console.WriteLine(" -v : Verbose mode, lists all version candidates found (nki-version only)"); Console.WriteLine(); Console.WriteLine("Supported extensions: .nkx, .nkr, .nicnt, .nks"); }