Last active
September 17, 2026 17:28
-
-
Save DartPower/ce0f01855dbef84198fbd867ba89e09a to your computer and use it in GitHub Desktop.
XNA Ressurector - Project restore
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| using System; | |
| using System.Collections.Generic; | |
| using System.Diagnostics; | |
| using System.Drawing; | |
| using System.Drawing.Imaging; | |
| using System.IO; | |
| using System.Reflection; | |
| using System.Runtime.InteropServices; | |
| using System.Security.Cryptography; | |
| using System.Text; | |
| using System.Threading; | |
| namespace XnaResurrector | |
| { | |
| internal enum LogLevel { Info, Success, Warning, Error, Trace } | |
| internal sealed class CustomConsoleTraceListener : TraceListener | |
| { | |
| private readonly object _syncLock = new object(); | |
| public override void Write(string message) | |
| { | |
| Console.Write(message); | |
| } | |
| public override void WriteLine(string message) | |
| { | |
| lock (_syncLock) | |
| { | |
| if (!Tui.IsOutputRedirected()) | |
| { | |
| try | |
| { | |
| int width = Console.WindowWidth > 1 ? Console.WindowWidth - 1 : 79; | |
| Console.Write("\r" + new string(' ', width) + "\r"); | |
| } | |
| catch { } | |
| } | |
| var originalColor = Console.ForegroundColor; | |
| if (message.StartsWith("[ERR]")) Console.ForegroundColor = ConsoleColor.Red; | |
| else if (message.StartsWith("[WARN]")) Console.ForegroundColor = ConsoleColor.Yellow; | |
| else if (message.StartsWith("[OK]")) Console.ForegroundColor = ConsoleColor.Green; | |
| else if (message.StartsWith("[STEP]")) Console.ForegroundColor = ConsoleColor.Cyan; | |
| else Console.ForegroundColor = ConsoleColor.Gray; | |
| Console.WriteLine(message); | |
| Console.ForegroundColor = originalColor; | |
| } | |
| } | |
| } | |
| internal static class Logger | |
| { | |
| private static TextWriterTraceListener _fileListener; | |
| private static CustomConsoleTraceListener _consoleListener; | |
| public static void Initialize() | |
| { | |
| try | |
| { | |
| string logDir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs"); | |
| if (!Directory.Exists(logDir)) | |
| { | |
| Directory.CreateDirectory(logDir); | |
| } | |
| string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); | |
| string logFilePath = Path.Combine(logDir, string.Format("resurrect_{0}.log", timestamp)); | |
| _fileListener = new TextWriterTraceListener(logFilePath); | |
| _consoleListener = new CustomConsoleTraceListener(); | |
| Trace.Listeners.Clear(); | |
| Trace.Listeners.Add(_fileListener); | |
| Trace.Listeners.Add(_consoleListener); | |
| Trace.AutoFlush = true; | |
| } | |
| catch (Exception ex) | |
| { | |
| Console.WriteLine(string.Format("Logger init fallback error: {0}", ex.Message)); | |
| } | |
| } | |
| public static void Log(LogLevel level, string message) | |
| { | |
| string time = DateTime.Now.ToString("HH:mm:ss"); | |
| string tag = "[INFO]"; | |
| switch (level) | |
| { | |
| case LogLevel.Success: tag = "[OK] "; break; | |
| case LogLevel.Warning: tag = "[WARN]"; break; | |
| case LogLevel.Error: tag = "[ERR] "; break; | |
| case LogLevel.Trace: tag = "[STEP]"; break; | |
| } | |
| string formatted = string.Format("{0} {1} {2}", tag, time, message); | |
| Trace.WriteLine(formatted); | |
| } | |
| public static void Shutdown() | |
| { | |
| if (_fileListener != null) | |
| { | |
| _fileListener.Flush(); | |
| _fileListener.Close(); | |
| } | |
| } | |
| } | |
| internal static class Tui | |
| { | |
| private static readonly object DrawLock = new object(); | |
| // Добавьте этот вспомогательный метод в класс Tui или Program: | |
| internal static bool IsOutputRedirected() | |
| { | |
| try | |
| { | |
| int dummy = Console.CursorLeft; | |
| return false; | |
| } | |
| catch | |
| { | |
| return true; | |
| } | |
| } | |
| public static void DrawBanner() | |
| { | |
| Console.ForegroundColor = ConsoleColor.Magenta; | |
| Console.WriteLine(@" _ ___ _ _ ___ _ "); | |
| Console.WriteLine(@" \ \/ / \ | | / \ | _ \ ___ ___ _ _ _ __ _ __ ___ ___| |_ ___ _ __ "); | |
| Console.WriteLine(@" \ /| \| | / _ \ | |_) / _ \/ __| | | | '__| '__/ _ \/ __| __/ _ \| '__|"); | |
| Console.WriteLine(@" / \| |\ |/ ___ \ | _ < __/\__ \ |_| | | | | | __/ (__| || (_) | | "); | |
| Console.WriteLine(@" /_/\_\_| \_/_/ \_\ |_| \_\___||___/\__,_|_| |_| \___|\___|\__\___/|_| "); | |
| Console.WriteLine(@" .NET 4 / VS2010"); | |
| Console.ResetColor(); | |
| Console.WriteLine(new string('=', 78)); | |
| } | |
| public static void SetStatus(string text) | |
| { | |
| try | |
| { | |
| Console.Title = string.Format("XNA Resurrector 4.0 :: {0}", text); | |
| } | |
| catch | |
| { | |
| // Silently bypass headless / redirected consoles | |
| } | |
| } | |
| public static void DrawProgress(int current, int total, string currentItem) | |
| { | |
| //if (Console.IsOutputRedirected) return; | |
| lock (DrawLock) | |
| { | |
| int width = 30; | |
| double progress = total > 0 ? (double)current / total : 1.0; | |
| int filled = (int)(progress * width); | |
| int empty = width - filled; | |
| string bar = new string('#', filled) + new string('-', empty); | |
| string itemTruncated = currentItem.Length > 32 | |
| ? "..." + currentItem.Substring(currentItem.Length - 29) | |
| : currentItem.PadRight(32); | |
| Console.Write("\r[{0}] {1,3}% | {2}", bar, (int)(progress * 100), itemTruncated); | |
| if (current >= total) | |
| { | |
| Console.WriteLine(); | |
| } | |
| } | |
| } | |
| } | |
| internal sealed class ContentItem | |
| { | |
| public string RelativePath { get; set; } | |
| public string AssetName { get; set; } | |
| public string Importer { get; set; } | |
| public string Processor { get; set; } | |
| } | |
| internal sealed class ResurrectOptions | |
| { | |
| public string SourcePath { get; set; } | |
| public string OutputPath { get; set; } | |
| public string ProjectName { get; set; } | |
| public bool IsProjectNameLocked { get; set; } | |
| public string CustomAssemblyPath { get; set; } | |
| public string Platform { get; set; } | |
| public bool TryDecompile { get; set; } | |
| public bool IsSilent { get; set; } | |
| } | |
| internal class Program | |
| { | |
| private static int Main(string[] args) | |
| { | |
| Logger.Initialize(); | |
| Console.OutputEncoding = Encoding.UTF8; | |
| Tui.SetStatus("Initializing..."); | |
| try | |
| { | |
| ResurrectOptions options = ParseArguments(args); | |
| if (options == null) | |
| { | |
| Tui.DrawBanner(); | |
| options = RunInteractiveWizard(); | |
| if (options == null) | |
| { | |
| Logger.Log(LogLevel.Warning, "Operation cancelled by user."); | |
| return 1; | |
| } | |
| } | |
| Tui.DrawBanner(); | |
| Logger.Log(LogLevel.Info, string.Format("Starting reconstruction for project: {0}", options.ProjectName)); | |
| Logger.Log(LogLevel.Info, string.Format("Source input path : {0}", options.SourcePath)); | |
| Logger.Log(LogLevel.Info, string.Format("Target output path: {0}", options.OutputPath)); | |
| Logger.Log(LogLevel.Info, string.Format("Target platform : {0}", options.Platform)); | |
| ExecutePipeline(options); | |
| Logger.Log(LogLevel.Success, "All project generation tasks completed successfully!"); | |
| Tui.SetStatus("Complete"); | |
| return 0; | |
| } | |
| catch (Exception ex) | |
| { | |
| Logger.Log(LogLevel.Error, string.Format("Fatal pipeline failure: {0}", ex.Message)); | |
| Trace.WriteLine(ex.StackTrace); | |
| Tui.SetStatus("Error"); | |
| return -1; | |
| } | |
| finally | |
| { | |
| Logger.Shutdown(); | |
| } | |
| } | |
| private static void ShowHelp() | |
| { | |
| Console.ForegroundColor = ConsoleColor.Cyan; | |
| Console.WriteLine("Usage: XnaResurrector.exe [options]"); | |
| Console.WriteLine(); | |
| Console.ForegroundColor = ConsoleColor.White; | |
| Console.WriteLine("Options:"); | |
| Console.WriteLine(" -i, --input <path> Input game root directory or main .exe/.dll"); | |
| Console.WriteLine(" -o, --output <path> Output solution directory"); | |
| Console.WriteLine(" -n, --name <name> Project name (optional, defaults to directory/exe name)"); | |
| Console.WriteLine(" -p, --platform <target> Target platform: Windows (default) or Xbox 360"); | |
| Console.WriteLine(" -d, --decompile Attempt automated ILSpyCmd decompilation"); | |
| Console.WriteLine(" -s, --silent Suppress interactive confirmations"); | |
| Console.WriteLine(" -?, -h, --help Show this manual"); | |
| Console.ResetColor(); | |
| } | |
| private static ResurrectOptions ParseArguments(string[] args) | |
| { | |
| if (args == null || args.Length == 0) return null; | |
| var opts = new ResurrectOptions | |
| { | |
| Platform = "Windows", | |
| TryDecompile = false, | |
| IsSilent = false | |
| }; | |
| for (int i = 0; i < args.Length; i++) | |
| { | |
| string arg = args[i].ToLowerInvariant(); | |
| switch (arg) | |
| { | |
| case "-?": | |
| case "-h": | |
| case "--help": | |
| ShowHelp(); | |
| Environment.Exit(0); | |
| break; | |
| case "-i": | |
| case "--input": | |
| if (i + 1 < args.Length) opts.SourcePath = args[++i]; | |
| break; | |
| case "-o": | |
| case "--output": | |
| if (i + 1 < args.Length) opts.OutputPath = args[++i]; | |
| break; | |
| case "-n": | |
| case "--name": | |
| if (i + 1 < args.Length) opts.ProjectName = args[++i]; | |
| break; | |
| case "-p": | |
| case "--platform": | |
| if (i + 1 < args.Length) opts.Platform = args[++i]; | |
| break; | |
| case "-d": | |
| case "--decompile": | |
| opts.TryDecompile = true; | |
| break; | |
| case "-s": | |
| case "--silent": | |
| opts.IsSilent = true; | |
| break; | |
| } | |
| } | |
| if (string.IsNullOrEmpty(opts.SourcePath) || string.IsNullOrEmpty(opts.OutputPath)) | |
| { | |
| return null; | |
| } | |
| NormalizePaths(opts); | |
| return opts; | |
| } | |
| internal sealed class ResurrectOptions | |
| { | |
| public string SourcePath { get; set; } | |
| public string OutputPath { get; set; } | |
| public string ProjectName { get; set; } | |
| public bool IsProjectNameLocked { get; set; } | |
| public string CustomAssemblyPath { get; set; } | |
| public string Platform { get; set; } | |
| public bool TryDecompile { get; set; } | |
| public bool IsSilent { get; set; } | |
| } | |
| private static ResurrectOptions RunInteractiveWizard() | |
| { | |
| var opts = new ResurrectOptions(); | |
| Console.ForegroundColor = ConsoleColor.White; | |
| Console.WriteLine(">> Interactive Configuration Wizard <<\n"); | |
| while (true) | |
| { | |
| Console.Write("Enter path to game folder, PIRS container, or .exe: "); | |
| Console.ForegroundColor = ConsoleColor.Green; | |
| string input = Console.ReadLine(); | |
| Console.ResetColor(); | |
| if (!string.IsNullOrEmpty(input)) | |
| { | |
| input = input.Trim('\"', ' '); | |
| if (Directory.Exists(input) || File.Exists(input)) | |
| { | |
| opts.SourcePath = Path.GetFullPath(input); | |
| break; | |
| } | |
| } | |
| Console.ForegroundColor = ConsoleColor.Red; | |
| Console.WriteLine("Path does not exist. Please re-enter."); | |
| Console.ResetColor(); | |
| } | |
| Console.Write("Enter output directory path: "); | |
| Console.ForegroundColor = ConsoleColor.Green; | |
| string outDir = Console.ReadLine(); | |
| Console.ResetColor(); | |
| if (string.IsNullOrEmpty(outDir)) | |
| { | |
| string root = Directory.Exists(opts.SourcePath) | |
| ? opts.SourcePath | |
| : Path.GetDirectoryName(opts.SourcePath); | |
| outDir = Path.Combine(root, "ResurrectedProject"); | |
| } | |
| opts.OutputPath = Path.GetFullPath(outDir.Trim('\"', ' ')); | |
| Console.Write("Enter project name (press Enter to auto-detect from package/assembly): "); | |
| Console.ForegroundColor = ConsoleColor.Yellow; | |
| string nameInput = Console.ReadLine(); | |
| Console.ResetColor(); | |
| if (!string.IsNullOrEmpty(nameInput) && nameInput.Trim().Length > 0) | |
| { | |
| opts.ProjectName = nameInput.Trim(); | |
| opts.IsProjectNameLocked = true; | |
| } | |
| Console.Write("Select target platform (1 = Windows [Default], 2 = Xbox 360): "); | |
| string platChoice = Console.ReadLine(); | |
| opts.Platform = (platChoice == "2") ? "Xbox 360" : "Windows"; | |
| Console.Write("Attempt code decompilation using ILSpyCmd if found? (y/n): "); | |
| string decompChoice = Console.ReadLine(); | |
| opts.TryDecompile = (decompChoice != null && decompChoice.Trim().ToLowerInvariant() == "y"); | |
| if (opts.TryDecompile) | |
| { | |
| Console.Write("Specify main game assembly/XEX (or press Enter for smart auto-search): "); | |
| Console.ForegroundColor = ConsoleColor.Yellow; | |
| string customAsm = Console.ReadLine(); | |
| Console.ResetColor(); | |
| if (!string.IsNullOrEmpty(customAsm) && customAsm.Trim().Length > 0) | |
| { | |
| opts.CustomAssemblyPath = customAsm.Trim('\"', ' '); | |
| } | |
| } | |
| NormalizePaths(opts); | |
| return opts; | |
| } | |
| private static void NormalizePaths(ResurrectOptions opts) | |
| { | |
| if (string.IsNullOrEmpty(opts.ProjectName)) | |
| { | |
| if (File.Exists(opts.SourcePath)) | |
| { | |
| opts.ProjectName = Path.GetFileNameWithoutExtension(opts.SourcePath); | |
| } | |
| else if (Directory.Exists(opts.SourcePath)) | |
| { | |
| opts.ProjectName = new DirectoryInfo(opts.SourcePath).Name; | |
| } | |
| } | |
| } | |
| private static void ExecutePipeline(ResurrectOptions opts) | |
| { | |
| Tui.SetStatus("Inspecting source..."); | |
| string sourceDir = opts.SourcePath; | |
| // 1. Автоматическая распаковка STFS (PIRS / LIVE / CON) пакета при необходимости | |
| if (File.Exists(opts.SourcePath) && StfsExtractor.IsStfsPackage(opts.SourcePath)) | |
| { | |
| if (!opts.IsProjectNameLocked) | |
| { | |
| string detectedTitle = StfsExtractor.GetPackageTitle(opts.SourcePath); | |
| if (!string.IsNullOrEmpty(detectedTitle)) | |
| { | |
| opts.ProjectName = detectedTitle; | |
| opts.IsProjectNameLocked = true; | |
| Logger.Log(LogLevel.Success, string.Format("Detected game title from STFS package: {0}", opts.ProjectName)); | |
| } | |
| } | |
| string extractRoot = Path.Combine(opts.OutputPath, "_PirsExtracted"); | |
| sourceDir = StfsExtractor.ExtractPackage(opts.SourcePath, extractRoot); | |
| } | |
| else if (File.Exists(opts.SourcePath)) | |
| { | |
| sourceDir = Path.GetDirectoryName(opts.SourcePath); | |
| } | |
| // 2. Поиск и расшифровка сборки игры | |
| string projNameRef = opts.ProjectName; | |
| string exeFile = XexAssemblyScanner.LocateOrExtractGameAssembly(sourceDir, ref projNameRef, opts.IsProjectNameLocked, opts.CustomAssemblyPath); | |
| // Если сборка извлечена и является .NET PE, читаем настоящее имя из манифеста сборки | |
| if (!string.IsNullOrEmpty(exeFile) && File.Exists(exeFile)) | |
| { | |
| try | |
| { | |
| var asmName = AssemblyName.GetAssemblyName(exeFile); | |
| if (asmName != null && !string.IsNullOrEmpty(asmName.Name)) | |
| { | |
| string realName = asmName.Name; | |
| if (!realName.Equals("game", StringComparison.OrdinalIgnoreCase) && | |
| !realName.Equals("default", StringComparison.OrdinalIgnoreCase) && | |
| !realName.Equals("stubbin", StringComparison.OrdinalIgnoreCase)) | |
| { | |
| if (!opts.IsProjectNameLocked) | |
| { | |
| opts.ProjectName = realName; | |
| Logger.Log(LogLevel.Success, string.Format("Resolved project name from Assembly Manifest: {0}", opts.ProjectName)); | |
| } | |
| } | |
| } | |
| } | |
| catch { } | |
| } | |
| if (!opts.IsProjectNameLocked && !string.IsNullOrEmpty(projNameRef) && | |
| !projNameRef.Equals("game", StringComparison.OrdinalIgnoreCase) && | |
| !projNameRef.Equals("default", StringComparison.OrdinalIgnoreCase)) | |
| { | |
| opts.ProjectName = projNameRef; | |
| } | |
| // Структура решения | |
| string solutionRoot = opts.OutputPath; | |
| string gameFolder = Path.Combine(solutionRoot, opts.ProjectName); | |
| string mainProjectDir = Path.Combine(gameFolder, opts.ProjectName); | |
| string contentProjectDir = Path.Combine(gameFolder, opts.ProjectName + "Content"); | |
| EnsureDirectory(solutionRoot); | |
| EnsureDirectory(mainProjectDir); | |
| EnsureDirectory(contentProjectDir); | |
| // Step 1: Декомпиляция исходного кода игры | |
| Tui.SetStatus("Extracting/Processing source code..."); | |
| if (opts.TryDecompile && !string.IsNullOrEmpty(exeFile)) | |
| { | |
| RunDecompiler(exeFile, mainProjectDir); | |
| } | |
| else | |
| { | |
| ScanAndMirrorCSharpCode(sourceDir, mainProjectDir, opts.ProjectName); | |
| } | |
| // Step 2: Сбор ассетов контент-пайплайна | |
| Tui.SetStatus("Harvesting content & generating .contentproj..."); | |
| List<ContentItem> contentItems = ProcessContentDirectory(sourceDir, contentProjectDir); | |
| // Step 3: Генерация GUID | |
| string gameProjGuid = Guid.NewGuid().ToString("B").ToUpperInvariant(); | |
| string contentProjGuid = Guid.NewGuid().ToString("B").ToUpperInvariant(); | |
| // Step 4: Генерация Content Project (.contentproj) | |
| string contentProjFile = Path.Combine(contentProjectDir, opts.ProjectName + "Content.contentproj"); | |
| GenerateContentProject(contentProjFile, contentProjGuid, opts.ProjectName + "Content", opts.Platform, contentItems); | |
| // Step 5: Генерация Game Project (.csproj) | |
| string gameProjFile = Path.Combine(mainProjectDir, opts.ProjectName + ".csproj"); | |
| GenerateGameProject(gameProjFile, gameProjGuid, contentProjGuid, opts.ProjectName, opts.Platform, mainProjectDir); | |
| // Step 6: Генерация Solution (.sln) | |
| string slnFile = Path.Combine(solutionRoot, opts.ProjectName + ".sln"); | |
| GenerateSolution(slnFile, opts.ProjectName, gameProjGuid, contentProjGuid, opts.Platform); | |
| Logger.Log(LogLevel.Success, string.Format("Created Visual Studio Solution: {0}", slnFile)); | |
| } | |
| private static string LocateMainExecutable(string rootDir, string preferredName) | |
| { | |
| try | |
| { | |
| string exact = Path.Combine(rootDir, preferredName + ".exe"); | |
| if (File.Exists(exact)) return exact; | |
| string[] exes = Directory.GetFiles(rootDir, "*.exe", SearchOption.TopDirectoryOnly); | |
| if (exes.Length > 0) return exes[0]; | |
| exes = Directory.GetFiles(rootDir, "*.exe", SearchOption.AllDirectories); | |
| if (exes.Length > 0) return exes[0]; | |
| } | |
| catch (Exception ex) | |
| { | |
| Logger.Log(LogLevel.Warning, string.Format("Executable search skipped: {0}", ex.Message)); | |
| } | |
| return null; | |
| } | |
| private static void RunDecompiler(string assemblyPath, string outputDir) | |
| { | |
| Logger.Log(LogLevel.Trace, "Checking ILSpyCmd availability..."); | |
| string ilspyPath = "ilspycmd"; | |
| string localTool = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "ilspycmd.exe"); | |
| if (File.Exists(localTool)) | |
| { | |
| ilspyPath = localTool; | |
| } | |
| // Формируем уникальные каталоги со сборками (.dll) | |
| var refDirs = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | |
| string gameRoot = Path.GetDirectoryName(assemblyPath); | |
| if (!string.IsNullOrEmpty(gameRoot) && Directory.Exists(gameRoot)) | |
| { | |
| refDirs.Add(gameRoot); | |
| } | |
| string runtimeDir = Path.Combine(gameRoot, "Runtime"); | |
| if (!Directory.Exists(runtimeDir)) | |
| { | |
| string parent = Path.GetDirectoryName(gameRoot); | |
| if (!string.IsNullOrEmpty(parent) && Directory.Exists(Path.Combine(parent, "Runtime"))) | |
| runtimeDir = Path.Combine(parent, "Runtime"); | |
| } | |
| if (Directory.Exists(runtimeDir)) | |
| { | |
| string[] allDlls = Directory.GetFiles(runtimeDir, "*.dll", SearchOption.AllDirectories); | |
| for (int i = 0; i < allDlls.Length; i++) | |
| { | |
| string dir = Path.GetDirectoryName(allDlls[i]); | |
| if (!string.IsNullOrEmpty(dir) && Directory.Exists(dir)) | |
| { | |
| refDirs.Add(dir); | |
| } | |
| } | |
| } | |
| string refArgs = ""; | |
| foreach (string dir in refDirs) | |
| { | |
| refArgs += string.Format(" -r \"{0}\"", dir); | |
| } | |
| try | |
| { | |
| var psi = new ProcessStartInfo | |
| { | |
| FileName = ilspyPath, | |
| Arguments = string.Format("-p -lv CSharp4 -o \"{0}\" \"{1}\"{2}", outputDir, assemblyPath, refArgs), | |
| CreateNoWindow = true, | |
| UseShellExecute = false, | |
| RedirectStandardOutput = true, | |
| RedirectStandardError = true | |
| }; | |
| using (var proc = Process.Start(psi)) | |
| { | |
| Logger.Log(LogLevel.Info, string.Format("Decompiling {0} using ILSpyCmd...", Path.GetFileName(assemblyPath))); | |
| string stdOut = proc.StandardOutput.ReadToEnd(); | |
| string stdErr = proc.StandardError.ReadToEnd(); | |
| proc.WaitForExit(); | |
| if (proc.ExitCode == 0) | |
| { | |
| Logger.Log(LogLevel.Success, "Assembly decompiled successfully."); | |
| return; | |
| } | |
| else | |
| { | |
| Logger.Log(LogLevel.Warning, string.Format("ILSpyCmd exit code {0}: {1}", proc.ExitCode, stdErr.Trim())); | |
| } | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| Logger.Log(LogLevel.Warning, string.Format("ILSpyCmd not executed ({0}). Proceeding to code scaffolding...", ex.Message)); | |
| } | |
| ScanAndMirrorCSharpCode(Path.GetDirectoryName(assemblyPath), outputDir, Path.GetFileNameWithoutExtension(assemblyPath)); | |
| } | |
| private static void ScanAndMirrorCSharpCode(string sourceDir, string targetDir, string projectName) | |
| { | |
| string[] csFiles = new string[0]; | |
| try | |
| { | |
| csFiles = Directory.GetFiles(sourceDir, "*.cs", SearchOption.AllDirectories); | |
| } | |
| catch { } | |
| if (csFiles.Length > 0) | |
| { | |
| Logger.Log(LogLevel.Info, string.Format("Found {0} existing C# source files. Copying...", csFiles.Length)); | |
| for (int i = 0; i < csFiles.Length; i++) | |
| { | |
| string src = csFiles[i]; | |
| string relative = GetRelativePath(sourceDir, src); | |
| string dest = Path.Combine(targetDir, relative); | |
| EnsureDirectory(Path.GetDirectoryName(dest)); | |
| File.Copy(src, dest, true); | |
| Tui.DrawProgress(i + 1, csFiles.Length, Path.GetFileName(src)); | |
| } | |
| } | |
| else | |
| { | |
| Logger.Log(LogLevel.Info, "No .cs files found. Generating game skeleton..."); | |
| CreateSkeletonCode(targetDir, projectName); | |
| } | |
| } | |
| private static void CreateSkeletonCode(string targetDir, string projectName) | |
| { | |
| string propsDir = Path.Combine(targetDir, "Properties"); | |
| EnsureDirectory(propsDir); | |
| // AssemblyInfo.cs | |
| string asmInfo = Path.Combine(propsDir, "AssemblyInfo.cs"); | |
| if (!File.Exists(asmInfo)) | |
| { | |
| File.WriteAllText(asmInfo, string.Format( | |
| @"using System.Reflection; | |
| using System.Runtime.InteropServices; | |
| [assembly: AssemblyTitle(""{0}"")] | |
| [assembly: AssemblyProduct(""{0}"")] | |
| [assembly: AssemblyDescription("""")] | |
| [assembly: AssemblyCompany("""")] | |
| [assembly: AssemblyCopyright(""Copyright © 2026"")] | |
| [assembly: AssemblyTrademark("""")] | |
| [assembly: AssemblyCulture("""")] | |
| [assembly: ComVisible(false)] | |
| [assembly: Guid(""{1}"")] | |
| [assembly: AssemblyVersion(""1.0.0.0"")] | |
| ", projectName, Guid.NewGuid().ToString("D")), Encoding.UTF8); | |
| } | |
| // Program.cs | |
| string progFile = Path.Combine(targetDir, "Program.cs"); | |
| if (!File.Exists(progFile)) | |
| { | |
| File.WriteAllText(progFile, string.Format( | |
| @"using System; | |
| namespace {0} | |
| {{ | |
| #if WINDOWS || XBOX | |
| static class Program | |
| {{ | |
| static void Main(string[] args) | |
| {{ | |
| using (Game1 game = new Game1()) | |
| {{ | |
| game.Run(); | |
| }} | |
| }} | |
| }} | |
| #endif | |
| }} | |
| ", projectName), Encoding.UTF8); | |
| } | |
| // Game1.cs | |
| string gameFile = Path.Combine(targetDir, "Game1.cs"); | |
| if (!File.Exists(gameFile)) | |
| { | |
| File.WriteAllText(gameFile, string.Format( | |
| @"using System; | |
| using Microsoft.Xna.Framework; | |
| using Microsoft.Xna.Framework.Graphics; | |
| using Microsoft.Xna.Framework.Input; | |
| namespace {0} | |
| {{ | |
| public class Game1 : Microsoft.Xna.Framework.Game | |
| {{ | |
| GraphicsDeviceManager graphics; | |
| SpriteBatch spriteBatch; | |
| public Game1() | |
| {{ | |
| graphics = new GraphicsDeviceManager(this); | |
| Content.RootDirectory = ""Content""; | |
| }} | |
| protected override void Initialize() | |
| {{ | |
| base.Initialize(); | |
| }} | |
| protected override void LoadContent() | |
| {{ | |
| spriteBatch = new SpriteBatch(GraphicsDevice); | |
| }} | |
| protected override void UnloadContent() | |
| {{ | |
| }} | |
| protected override void Update(GameTime gameTime) | |
| {{ | |
| if (GamePad.GetState(PlayerIndex.One).Buttons.Back == ButtonState.Pressed || | |
| Keyboard.GetState().IsKeyDown(Keys.Escape)) | |
| this.Exit(); | |
| base.Update(gameTime); | |
| }} | |
| protected override void Draw(GameTime gameTime) | |
| {{ | |
| GraphicsDevice.Clear(Color.CornflowerBlue); | |
| base.Draw(gameTime); | |
| }} | |
| }} | |
| }} | |
| ", projectName), Encoding.UTF8); | |
| } | |
| } | |
| private static List<ContentItem> ProcessContentDirectory(string sourceDir, string contentProjectDir) | |
| { | |
| string foundXgsPath = null; | |
| var items = new List<ContentItem>(); | |
| EnsureDirectory(contentProjectDir); | |
| // Удаляем старую вложенную папку Content от прошлых некорректных запусков | |
| string staleContentFolder = Path.Combine(contentProjectDir, "Content"); | |
| if (Directory.Exists(staleContentFolder)) | |
| { | |
| try { Directory.Delete(staleContentFolder, true); } catch { } | |
| } | |
| string[] candidateFiles = new string[0]; | |
| try | |
| { | |
| candidateFiles = Directory.GetFiles(sourceDir, "*.*", SearchOption.AllDirectories); | |
| } | |
| catch (Exception ex) | |
| { | |
| Logger.Log(LogLevel.Error, string.Format("Error scanning content: {0}", ex.Message)); | |
| return items; | |
| } | |
| Logger.Log(LogLevel.Info, string.Format("Discovered {0} candidate files. Classifying XNA assets...", candidateFiles.Length)); | |
| // Поиск имен Cue в Sound Bank | |
| string[] cueNames = null; | |
| for (int i = 0; i < candidateFiles.Length; i++) | |
| { | |
| if (Path.GetExtension(candidateFiles[i]).Equals(".xsb", StringComparison.OrdinalIgnoreCase)) | |
| { | |
| cueNames = XsbReader.ExtractCueNames(candidateFiles[i]); | |
| if (cueNames != null && cueNames.Length > 0) | |
| { | |
| Logger.Log(LogLevel.Info, string.Format("Found sound bank '{0}' with {1} cues.", | |
| Path.GetFileName(candidateFiles[i]), cueNames.Length)); | |
| break; | |
| } | |
| } | |
| } | |
| int processed = 0; | |
| for (int i = 0; i < candidateFiles.Length; i++) | |
| { | |
| string file = candidateFiles[i]; | |
| string ext = Path.GetExtension(file).ToLowerInvariant(); | |
| // Пропуск бинарников, исполняемых файлов и папки Runtime | |
| if (ext == ".cs" || ext == ".csproj" || ext == ".sln" || ext == ".pdb" || ext == ".xex" || | |
| file.IndexOf("Runtime", StringComparison.OrdinalIgnoreCase) >= 0) | |
| continue; | |
| string relative = GetRelativePath(sourceDir, file); | |
| // Отрезаем ведущий Content\, чтобы не двоить путь | |
| if (relative.StartsWith("Content" + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase)) | |
| { | |
| relative = relative.Substring(8); | |
| } | |
| string destPath = Path.Combine(contentProjectDir, relative); | |
| EnsureDirectory(Path.GetDirectoryName(destPath)); | |
| // 1. Бинарники настроек XACT (копируются как есть) | |
| if (ext == ".xgs" || ext == ".xsb") | |
| { | |
| File.Copy(file, destPath, true); | |
| if (ext == ".xgs") foundXgsPath = destPath; // Запоминаем путь к .xgs | |
| } | |
| // 2. Ассеты XNB -> распаковка в PNG / SpriteFont / FX | |
| else if (ext == ".xnb") | |
| { | |
| string targetFolder = Path.GetDirectoryName(destPath); | |
| string generatedFile; | |
| if (XnbExtractor.TryUnpackXnb(file, targetFolder, out generatedFile)) | |
| { | |
| ContentItem ci = CreateContentItem(generatedFile, contentProjectDir); | |
| if (ci != null) items.Add(ci); | |
| } | |
| else if (EffectExtractor.TryUnpackEffect(file, targetFolder, out generatedFile)) | |
| { | |
| ContentItem ci = CreateContentItem(generatedFile, contentProjectDir); | |
| if (ci != null) items.Add(ci); | |
| } | |
| else | |
| { | |
| // Не удалось распаковать → копируем бинарник как есть | |
| File.Copy(file, destPath, true); | |
| // НЕ создаём ContentItem для бинарных XNB | |
| } | |
| } | |
| // 3. Волновой банк XWB -> распаковка дорожек и создание XAP | |
| else if (ext == ".xwb") | |
| { | |
| if (!Tui.IsOutputRedirected()) | |
| { | |
| try { Console.Write("\r" + new string(' ', 79) + "\r"); } catch { } | |
| } | |
| string musicDir = Path.Combine(contentProjectDir, "Sounds"); | |
| EnsureDirectory(musicDir); | |
| List<string> extractedTracks = XwbExtractor.Extract(file, musicDir, cueNames); | |
| if (extractedTracks.Count > 0) | |
| { | |
| Logger.Log(LogLevel.Success, string.Format("Unpacked Wave Bank '{0}': {1} tracks restored.", | |
| Path.GetFileName(file), extractedTracks.Count)); | |
| string xapPath = Path.Combine(contentProjectDir, Path.GetFileNameWithoutExtension(file) + ".xap"); | |
| // ПЕРЕДАЕМ foundXgsPath В ГЕНЕРАТОР | |
| XapGenerator.GenerateXap(xapPath, Path.GetFileNameWithoutExtension(file), Path.GetFileName(file), extractedTracks, contentProjectDir, foundXgsPath); | |
| Logger.Log(LogLevel.Success, string.Format("Constructed XACT Audio Project: {0}", Path.GetFileName(xapPath))); | |
| ContentItem xapItem = CreateContentItem(xapPath, contentProjectDir); | |
| if (xapItem != null) items.Add(xapItem); | |
| } | |
| else | |
| { | |
| File.Copy(file, destPath, true); | |
| } | |
| } | |
| // 4. Обычные файлы контента (PNG, WLD и др.) | |
| else | |
| { | |
| File.Copy(file, destPath, true); | |
| ContentItem ci = CreateContentItem(destPath, contentProjectDir); | |
| if (ci != null) items.Add(ci); | |
| } | |
| processed++; | |
| Tui.DrawProgress(i + 1, candidateFiles.Length, Path.GetFileName(file)); | |
| } | |
| Logger.Log(LogLevel.Success, string.Format("Cataloged {0} valid pipeline build items.", items.Count)); | |
| return items; | |
| } | |
| private static ContentItem CreateContentItem(string filePath, string contentProjectDir) | |
| { | |
| string ext = Path.GetExtension(filePath).ToLowerInvariant(); | |
| // Относительный путь для Include в .contentproj (например: "Achievement01.png" или "Images\Background_1.png") | |
| string rel = GetRelativePath(contentProjectDir, filePath); | |
| // Имя ассета без расширения | |
| string dirPart = Path.GetDirectoryName(rel); | |
| string filePart = Path.GetFileNameWithoutExtension(filePath); | |
| string assetName = string.IsNullOrEmpty(dirPart) ? filePart : Path.Combine(dirPart, filePart); | |
| assetName = assetName.Replace('/', '\\'); | |
| string importer = null; | |
| string processor = null; | |
| switch (ext) | |
| { | |
| case ".png": | |
| case ".jpg": | |
| case ".dds": | |
| case ".tga": | |
| case ".bmp": | |
| importer = "TextureImporter"; | |
| processor = "TextureProcessor"; | |
| break; | |
| case ".wav": | |
| case ".mp3": | |
| case ".wma": | |
| importer = "WavImporter"; | |
| processor = "SoundEffectProcessor"; | |
| break; | |
| case ".spritefont": | |
| importer = "FontDescriptionImporter"; | |
| processor = "FontDescriptionProcessor"; | |
| break; | |
| case ".fx": | |
| importer = "EffectImporter"; | |
| processor = "EffectProcessor"; | |
| break; | |
| case ".fbx": | |
| importer = "FbxImporter"; | |
| processor = "ModelProcessor"; | |
| break; | |
| case ".x": | |
| importer = "XImporter"; | |
| processor = "ModelProcessor"; | |
| break; | |
| case ".wmv": | |
| importer = "VideoImporter"; | |
| processor = "VideoProcessor"; | |
| break; | |
| case ".xap": | |
| importer = "XactImporter"; | |
| processor = "XactProcessor"; | |
| break; | |
| default: | |
| return null; | |
| } | |
| return new ContentItem | |
| { | |
| RelativePath = rel, | |
| AssetName = assetName, | |
| Importer = importer, | |
| Processor = processor | |
| }; | |
| } | |
| private static void CreateSpriteFontXml(string targetPath, string fontName, int size) | |
| { | |
| string xml = string.Format( | |
| @"<?xml version=""1.0"" encoding=""utf-8""?> | |
| <XnaContent xmlns:Graphics=""Microsoft.Xna.Framework.Content.Pipeline.Graphics""> | |
| <Asset Type=""Graphics:FontDescription""> | |
| <FontName>{0}</FontName> | |
| <Size>{1}</Size> | |
| <Spacing>0</Spacing> | |
| <UseKerning>true</UseKerning> | |
| <Style>Regular</Style> | |
| <CharacterRegions> | |
| <CharacterRegion> | |
| <Start> </Start> | |
| <End>~</End> | |
| </CharacterRegion> | |
| <CharacterRegion> | |
| <Start>А</Start> | |
| <End>я</End> | |
| </CharacterRegion> | |
| </CharacterRegions> | |
| </Asset> | |
| </XnaContent>", fontName, size); | |
| File.WriteAllText(targetPath, xml, Encoding.UTF8); | |
| Logger.Log(LogLevel.Trace, string.Format("Auto-created SpriteFont source: {0}", Path.GetFileName(targetPath))); | |
| } | |
| private static void GenerateContentProject(string filePath, string guid, string projName, string platform, List<ContentItem> items) | |
| { | |
| var sb = new StringBuilder(); | |
| sb.AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>"); | |
| sb.AppendLine("<Project DefaultTargets=\"Build\" ToolsVersion=\"4.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">"); | |
| sb.AppendLine(" <PropertyGroup>"); | |
| sb.AppendFormat(" <ProjectGuid>{0}</ProjectGuid>\n", guid); | |
| sb.AppendLine(" <ProjectTypeGuids>{96E2B04D-8817-42c6-938A-82C39BA4D311};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>"); | |
| sb.AppendLine(" <Configuration Condition=\" '$(Configuration)' == '' \">Debug</Configuration>"); | |
| sb.AppendLine(" <Platform Condition=\" '$(Platform)' == '' \">x86</Platform>"); | |
| sb.AppendLine(" <OutputType>Library</OutputType>"); | |
| sb.AppendLine(" <AppDesignerFolder>Properties</AppDesignerFolder>"); | |
| sb.AppendLine(" <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>"); | |
| sb.AppendLine(" <XnaFrameworkVersion>v4.0</XnaFrameworkVersion>"); | |
| sb.AppendLine(" <OutputPath>bin\\$(Platform)\\$(Configuration)</OutputPath>"); | |
| sb.AppendLine(" <ContentRootDirectory>Content</ContentRootDirectory>"); | |
| sb.AppendLine(" <PlatformTarget>x86</PlatformTarget>"); | |
| sb.AppendLine(" <ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>None</ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>"); | |
| sb.AppendLine(" </PropertyGroup>"); | |
| // Конфигурации платформ (Content Pipeline всегда выполняется как x86 процесс) | |
| string[] configs = new string[] { "Debug|Xbox 360", "Release|Xbox 360", "Debug|x86", "Release|x86" }; | |
| for (int i = 0; i < configs.Length; i++) | |
| { | |
| sb.AppendFormat(" <PropertyGroup Condition=\" '$(Configuration)|$(Platform)' == '{0}' \">\n", configs[i]); | |
| sb.AppendLine(" <PlatformTarget>x86</PlatformTarget>"); | |
| sb.AppendLine(" </PropertyGroup>"); | |
| } | |
| // Чистые ссылки без конфликтующих атрибутов processorArchitecture | |
| sb.AppendLine(" <ItemGroup>"); | |
| sb.AppendLine(" <Reference Include=\"Microsoft.Xna.Framework.Content.Pipeline.EffectImporter\" />"); | |
| sb.AppendLine(" <Reference Include=\"Microsoft.Xna.Framework.Content.Pipeline.FBXImporter\" />"); | |
| sb.AppendLine(" <Reference Include=\"Microsoft.Xna.Framework.Content.Pipeline.TextureImporter\" />"); | |
| sb.AppendLine(" <Reference Include=\"Microsoft.Xna.Framework.Content.Pipeline.XImporter\" />"); | |
| sb.AppendLine(" <Reference Include=\"Microsoft.Xna.Framework.Content.Pipeline.AudioImporters\" />"); | |
| sb.AppendLine(" <Reference Include=\"Microsoft.Xna.Framework.Content.Pipeline.VideoImporters\" />"); | |
| sb.AppendLine(" </ItemGroup>"); | |
| // Ассеты контент-пайплайна | |
| sb.AppendLine(" <ItemGroup>"); | |
| for (int i = 0; i < items.Count; i++) | |
| { | |
| ContentItem item = items[i]; | |
| if (string.IsNullOrEmpty(item.Importer) || string.IsNullOrEmpty(item.Processor)) | |
| continue; | |
| sb.AppendFormat(" <Compile Include=\"{0}\">\n", item.RelativePath); | |
| sb.AppendFormat(" <Name>{0}</Name>\n", item.AssetName); | |
| sb.AppendFormat(" <Importer>{0}</Importer>\n", item.Importer); | |
| sb.AppendFormat(" <Processor>{0}</Processor>\n", item.Processor); | |
| sb.AppendLine(" </Compile>"); | |
| } | |
| sb.AppendLine(" </ItemGroup>"); | |
| sb.AppendLine(" <Import Project=\"$(MSBuildExtensionsPath)\\Microsoft\\XNA Game Studio\\$(XnaFrameworkVersion)\\Microsoft.Xna.GameStudio.ContentPipeline.targets\" />"); | |
| sb.AppendLine("</Project>"); | |
| File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8); | |
| Logger.Log(LogLevel.Success, string.Format("Generated Content Project: {0}", Path.GetFileName(filePath))); | |
| } | |
| private static void CreateXboxAssetsIfMissing(string projectDir) | |
| { | |
| string thumbPath = Path.Combine(projectDir, "GameThumbnail.png"); | |
| string iconPath = Path.Combine(projectDir, "Game.ico"); | |
| // 1. Поиск оригинального арта превью (TitleImage.png / GameIcon.png / app.ico) | |
| string sourceFolder = Path.GetDirectoryName(projectDir); | |
| string[] possibleThumbs = new string[] | |
| { | |
| Path.Combine(sourceFolder, "TitleImage.png"), | |
| Path.Combine(sourceFolder, "GameIcon.png"), | |
| Path.Combine(projectDir, "TitleImage.png"), | |
| Path.Combine(projectDir, "GameIcon.png") | |
| }; | |
| foreach (var t in possibleThumbs) | |
| { | |
| if (File.Exists(t)) | |
| { | |
| File.Copy(t, thumbPath, true); | |
| break; | |
| } | |
| } | |
| if (!File.Exists(thumbPath)) | |
| { | |
| using (var bmp = new Bitmap(64, 64)) | |
| using (var g = Graphics.FromImage(bmp)) | |
| { | |
| g.Clear(Color.FromArgb(40, 40, 40)); | |
| bmp.Save(thumbPath, ImageFormat.Png); | |
| } | |
| } | |
| // 2. Иконка приложения Game.ico | |
| string appIcon = Path.Combine(projectDir, "app.ico"); | |
| if (File.Exists(appIcon)) | |
| { | |
| File.Copy(appIcon, iconPath, true); | |
| Logger.Log(LogLevel.Success, "Applied extracted app.ico as Game.ico"); | |
| } | |
| else if (!File.Exists(iconPath)) | |
| { | |
| using (var bmp = new Bitmap(32, 32)) | |
| using (var ms = new MemoryStream()) | |
| { | |
| using (var g = Graphics.FromImage(bmp)) { g.Clear(Color.CornflowerBlue); } | |
| using (var bw = new BinaryWriter(ms)) | |
| { | |
| bw.Write((short)0); bw.Write((short)1); bw.Write((short)1); | |
| bw.Write((byte)32); bw.Write((byte)32); bw.Write((byte)0); bw.Write((byte)0); | |
| bw.Write((short)1); bw.Write((short)32); | |
| using (var pngMs = new MemoryStream()) | |
| { | |
| bmp.Save(pngMs, ImageFormat.Png); | |
| byte[] pngBytes = pngMs.ToArray(); | |
| bw.Write(pngBytes.Length); | |
| bw.Write(22); | |
| bw.Write(pngBytes); | |
| } | |
| } | |
| File.WriteAllBytes(iconPath, ms.ToArray()); | |
| } | |
| } | |
| } | |
| private static void GenerateGameProject(string filePath, string guid, string contentGuid, string projName, string platform, string mainProjDir) | |
| { | |
| CreateXboxAssetsIfMissing(mainProjDir); | |
| var sb = new StringBuilder(); | |
| sb.AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>"); | |
| sb.AppendLine("<Project DefaultTargets=\"Build\" ToolsVersion=\"4.0\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">"); | |
| sb.AppendLine(" <PropertyGroup>"); | |
| sb.AppendFormat(" <ProjectGuid>{0}</ProjectGuid>\n", guid); | |
| sb.AppendLine(" <ProjectTypeGuids>{6D335F3A-9D43-41b4-9D22-F6F17C4BE596};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>"); | |
| sb.AppendLine(" <Configuration Condition=\" '$(Configuration)' == '' \">Debug</Configuration>"); | |
| sb.AppendFormat(" <Platform Condition=\" '$(Platform)' == '' \">{0}</Platform>\n", platform); | |
| sb.AppendLine(" <OutputType>Exe</OutputType>"); | |
| sb.AppendLine(" <AppDesignerFolder>Properties</AppDesignerFolder>"); | |
| sb.AppendFormat(" <RootNamespace>{0}</RootNamespace>\n", projName); | |
| sb.AppendFormat(" <AssemblyName>{0}</AssemblyName>\n", projName); | |
| sb.AppendLine(" <TargetFrameworkVersion>v4.0</TargetFrameworkVersion>"); | |
| sb.AppendLine(" <TargetFrameworkProfile>Client</TargetFrameworkProfile>"); | |
| sb.AppendLine(" <XnaFrameworkVersion>v4.0</XnaFrameworkVersion>"); | |
| sb.AppendFormat(" <XnaPlatform>{0}</XnaPlatform>\n", platform); | |
| sb.AppendLine(" <XnaProfile>HiDef</XnaProfile>"); | |
| sb.AppendLine(" <XnaCrossPlatformGroupID>" + Guid.NewGuid().ToString("D") + "</XnaCrossPlatformGroupID>"); | |
| sb.AppendLine(" <XnaOutputType>Game</XnaOutputType>"); | |
| sb.AppendLine(" <ApplicationIcon>Game.ico</ApplicationIcon>"); | |
| sb.AppendLine(" <Thumbnail>GameThumbnail.png</Thumbnail>"); | |
| sb.AppendLine(" <ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>None</ResolveAssemblyWarnOrErrorOnTargetArchitectureMismatch>"); | |
| sb.AppendLine(" </PropertyGroup>"); | |
| // Конфигурации для Windows (x86) | |
| sb.AppendLine(" <PropertyGroup Condition=\" '$(Configuration)|$(Platform)' == 'Debug|x86' \">"); | |
| sb.AppendLine(" <DebugSymbols>true</DebugSymbols>"); | |
| sb.AppendLine(" <DebugType>full</DebugType>"); | |
| sb.AppendLine(" <Optimize>false</Optimize>"); | |
| sb.AppendLine(" <OutputPath>bin\\x86\\Debug</OutputPath>"); | |
| sb.AppendLine(" <DefineConstants>DEBUG;TRACE;WINDOWS</DefineConstants>"); | |
| sb.AppendLine(" <ErrorReport>prompt</ErrorReport>"); | |
| sb.AppendLine(" <WarningLevel>4</WarningLevel>"); | |
| sb.AppendLine(" <PlatformTarget>x86</PlatformTarget>"); | |
| sb.AppendLine(" </PropertyGroup>"); | |
| sb.AppendLine(" <PropertyGroup Condition=\" '$(Configuration)|$(Platform)' == 'Release|x86' \">"); | |
| sb.AppendLine(" <DebugType>pdbonly</DebugType>"); | |
| sb.AppendLine(" <Optimize>true</Optimize>"); | |
| sb.AppendLine(" <OutputPath>bin\\x86\\Release</OutputPath>"); | |
| sb.AppendLine(" <DefineConstants>TRACE;WINDOWS</DefineConstants>"); | |
| sb.AppendLine(" <ErrorReport>prompt</ErrorReport>"); | |
| sb.AppendLine(" <WarningLevel>4</WarningLevel>"); | |
| sb.AppendLine(" <PlatformTarget>x86</PlatformTarget>"); | |
| sb.AppendLine(" </PropertyGroup>"); | |
| // Конфигурации для Xbox 360 | |
| sb.AppendLine(" <PropertyGroup Condition=\" '$(Configuration)|$(Platform)' == 'Debug|Xbox 360' \">"); | |
| sb.AppendLine(" <DebugSymbols>true</DebugSymbols>"); | |
| sb.AppendLine(" <DebugType>full</DebugType>"); | |
| sb.AppendLine(" <Optimize>false</Optimize>"); | |
| sb.AppendLine(" <OutputPath>bin\\Xbox 360\\Debug</OutputPath>"); | |
| sb.AppendLine(" <DefineConstants>DEBUG;TRACE;XBOX;XBOX360</DefineConstants>"); | |
| sb.AppendLine(" <ErrorReport>prompt</ErrorReport>"); | |
| sb.AppendLine(" <WarningLevel>4</WarningLevel>"); | |
| sb.AppendLine(" <NoStdLib>true</NoStdLib>"); | |
| sb.AppendLine(" <UseVSHostingProcess>false</UseVSHostingProcess>"); | |
| sb.AppendLine(" <PlatformTarget>x86</PlatformTarget>"); | |
| sb.AppendLine(" <XnaCompressContent>true</XnaCompressContent>"); | |
| sb.AppendLine(" </PropertyGroup>"); | |
| sb.AppendLine(" <PropertyGroup Condition=\" '$(Configuration)|$(Platform)' == 'Release|Xbox 360' \">"); | |
| sb.AppendLine(" <DebugType>pdbonly</DebugType>"); | |
| sb.AppendLine(" <Optimize>true</Optimize>"); | |
| sb.AppendLine(" <OutputPath>bin\\Xbox 360\\Release</OutputPath>"); | |
| sb.AppendLine(" <DefineConstants>TRACE;XBOX;XBOX360</DefineConstants>"); | |
| sb.AppendLine(" <ErrorReport>prompt</ErrorReport>"); | |
| sb.AppendLine(" <WarningLevel>4</WarningLevel>"); | |
| sb.AppendLine(" <NoStdLib>true</NoStdLib>"); | |
| sb.AppendLine(" <UseVSHostingProcess>false</UseVSHostingProcess>"); | |
| sb.AppendLine(" <PlatformTarget>x86</PlatformTarget>"); | |
| sb.AppendLine(" <XnaCompressContent>true</XnaCompressContent>"); | |
| sb.AppendLine(" </PropertyGroup>"); | |
| // Ссылки XNA Framework и BCL | |
| sb.AppendLine(" <ItemGroup>"); | |
| sb.AppendLine(" <Reference Include=\"Microsoft.Xna.Framework\" />"); | |
| sb.AppendLine(" <Reference Include=\"Microsoft.Xna.Framework.Game\" />"); | |
| sb.AppendLine(" <Reference Include=\"Microsoft.Xna.Framework.Graphics\" />"); | |
| sb.AppendLine(" <Reference Include=\"Microsoft.Xna.Framework.GamerServices\" />"); | |
| sb.AppendLine(" <Reference Include=\"Microsoft.Xna.Framework.Xact\" />"); | |
| sb.AppendLine(" <Reference Include=\"Microsoft.Xna.Framework.Video\" />"); | |
| sb.AppendLine(" <Reference Include=\"Microsoft.Xna.Framework.Avatar\" />"); | |
| sb.AppendLine(" <Reference Include=\"Microsoft.Xna.Framework.Net\" />"); | |
| sb.AppendLine(" <Reference Include=\"Microsoft.Xna.Framework.Storage\" />"); | |
| sb.AppendLine(" <Reference Include=\"mscorlib\" />"); | |
| sb.AppendLine(" <Reference Include=\"System\" />"); | |
| sb.AppendLine(" <Reference Include=\"System.Core\" />"); | |
| sb.AppendLine(" <Reference Include=\"System.Xml\" />"); | |
| sb.AppendLine(" <Reference Include=\"System.Xml.Linq\" />"); | |
| sb.AppendLine(" <Reference Include=\"System.Net\" />"); | |
| sb.AppendLine(" </ItemGroup>"); | |
| // Динамическое включение исходников .cs | |
| sb.AppendLine(" <ItemGroup>"); | |
| string[] csFiles = Directory.GetFiles(mainProjDir, "*.cs", SearchOption.AllDirectories); | |
| for (int i = 0; i < csFiles.Length; i++) | |
| { | |
| string rel = GetRelativePath(mainProjDir, csFiles[i]); | |
| sb.AppendFormat(" <Compile Include=\"{0}\" />\n", rel); | |
| } | |
| sb.AppendLine(" </ItemGroup>"); | |
| // Иконка и превью приложения | |
| sb.AppendLine(" <ItemGroup>"); | |
| sb.AppendLine(" <Content Include=\"Game.ico\" />"); | |
| sb.AppendLine(" <Content Include=\"GameThumbnail.png\">"); | |
| sb.AppendLine(" <XnaPlatformSpecific>true</XnaPlatformSpecific>"); | |
| sb.AppendLine(" </Content>"); | |
| sb.AppendLine(" </ItemGroup>"); | |
| // Привязка Content-проекта (.contentproj) | |
| sb.AppendLine(" <ItemGroup>"); | |
| sb.AppendFormat(" <ProjectReference Include=\"..\\{0}Content\\{0}Content.contentproj\">\n", projName); | |
| sb.AppendFormat(" <Project>{0}</Project>\n", contentGuid); | |
| sb.AppendFormat(" <Name>{0}Content</Name>\n", projName); | |
| sb.AppendLine(" <XnaReferenceType>Content</XnaReferenceType>"); | |
| sb.AppendLine(" </ProjectReference>"); | |
| sb.AppendLine(" </ItemGroup>"); | |
| // Импорт стандартных таргетов XNA Game Studio 4.0 | |
| sb.AppendLine(" <Import Project=\"$(MSBuildBinPath)\\Microsoft.CSharp.targets\" />"); | |
| sb.AppendLine(" <Import Project=\"$(MSBuildExtensionsPath)\\Microsoft\\XNA Game Studio\\Microsoft.Xna.GameStudio.targets\" />"); | |
| sb.AppendLine("</Project>"); | |
| File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8); | |
| Logger.Log(LogLevel.Success, string.Format("Generated Game Project: {0}", Path.GetFileName(filePath))); | |
| } | |
| private static void GenerateSolution(string slnPath, string projName, string gameGuid, string contentGuid, string activePlatform) | |
| { | |
| var sb = new StringBuilder(); | |
| sb.AppendLine("Microsoft Visual Studio Solution File, Format Version 11.00"); | |
| sb.AppendLine("# Visual Studio 2010"); | |
| sb.AppendFormat("Project(\"{{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}}\") = \"{0}\", \"{0}\\{0}\\{0}.csproj\", \"{1}\"\n", projName, gameGuid); | |
| sb.AppendLine("EndProject"); | |
| sb.AppendFormat("Project(\"{{96E2B04D-8817-42c6-938A-82C39BA4D311}}\") = \"{0}Content\", \"{0}\\{0}Content\\{0}Content.contentproj\", \"{1}\"\n", projName, contentGuid); | |
| sb.AppendLine("EndProject"); | |
| sb.AppendLine("Global"); | |
| sb.AppendLine("\tGlobalSection(SolutionConfigurationPlatforms) = preSolution"); | |
| sb.AppendLine("\t\tDebug|Windows = Debug|x86"); | |
| sb.AppendLine("\t\tDebug|Xbox 360 = Debug|Xbox 360"); | |
| sb.AppendLine("\t\tRelease|Windows = Release|x86"); | |
| sb.AppendLine("\t\tRelease|Xbox 360 = Release|Xbox 360"); | |
| sb.AppendLine("\tEndGlobalSection"); | |
| sb.AppendLine("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution"); | |
| // Game Project Mappings | |
| sb.AppendFormat("\t\t{0}.Debug|Windows.ActiveCfg = Debug|x86\n", gameGuid); | |
| sb.AppendFormat("\t\t{0}.Debug|Windows.Build.0 = Debug|x86\n", gameGuid); | |
| sb.AppendFormat("\t\t{0}.Release|Windows.ActiveCfg = Release|x86\n", gameGuid); | |
| sb.AppendFormat("\t\t{0}.Release|Windows.Build.0 = Release|x86\n", gameGuid); | |
| sb.AppendFormat("\t\t{0}.Debug|Xbox 360.ActiveCfg = Debug|Xbox 360\n", gameGuid); | |
| sb.AppendFormat("\t\t{0}.Debug|Xbox 360.Build.0 = Debug|Xbox 360\n", gameGuid); | |
| sb.AppendFormat("\t\t{0}.Release|Xbox 360.ActiveCfg = Release|Xbox 360\n", gameGuid); | |
| sb.AppendFormat("\t\t{0}.Release|Xbox 360.Build.0 = Release|Xbox 360\n", gameGuid); | |
| // Content Project Mappings | |
| sb.AppendFormat("\t\t{0}.Debug|Windows.ActiveCfg = Debug|x86\n", contentGuid); | |
| sb.AppendFormat("\t\t{0}.Release|Windows.ActiveCfg = Release|x86\n", contentGuid); | |
| sb.AppendFormat("\t\t{0}.Debug|Xbox 360.ActiveCfg = Debug|Xbox 360\n", contentGuid); | |
| sb.AppendFormat("\t\t{0}.Release|Xbox 360.ActiveCfg = Release|Xbox 360\n", contentGuid); | |
| sb.AppendLine("\tEndGlobalSection"); | |
| sb.AppendLine("\tGlobalSection(SolutionProperties) = preSolution"); | |
| sb.AppendLine("\t\tHideSolutionNode = FALSE"); | |
| sb.AppendLine("\tEndGlobalSection"); | |
| sb.AppendLine("EndGlobal"); | |
| File.WriteAllText(slnPath, sb.ToString(), Encoding.UTF8); | |
| } | |
| private static void EnsureDirectory(string path) | |
| { | |
| if (!string.IsNullOrEmpty(path) && !Directory.Exists(path)) | |
| { | |
| Directory.CreateDirectory(path); | |
| } | |
| } | |
| private static string GetRelativePath(string relativeTo, string path) | |
| { | |
| if (string.IsNullOrEmpty(relativeTo)) return path; | |
| var fromUri = new Uri(AppendDirectorySeparator(relativeTo)); | |
| var toUri = new Uri(path); | |
| if (fromUri.Scheme != toUri.Scheme) { return path; } | |
| Uri relativeUri = fromUri.MakeRelativeUri(toUri); | |
| string relativePath = Uri.UnescapeDataString(relativeUri.ToString()); | |
| return relativePath.Replace('/', Path.DirectorySeparatorChar); | |
| } | |
| private static string AppendDirectorySeparator(string path) | |
| { | |
| if (!path.EndsWith(Path.DirectorySeparatorChar.ToString()) && !path.EndsWith(Path.AltDirectorySeparatorChar.ToString())) | |
| { | |
| return path + Path.DirectorySeparatorChar; | |
| } | |
| return path; | |
| } | |
| } | |
| internal static class XwbExtractor | |
| { | |
| private static readonly uint[] WmaAvgBytesPerSec = new uint[] { 12000, 24000, 4000, 6000, 8000, 20000, 2500 }; | |
| private static readonly uint[] WmaBlockAlign = new uint[] { 929, 1487, 1280, 2230, 8917, 8192, 4459, 5945, 2304, 1536, 1485, 1008, 2731, 4096, 6827, 5462, 1280 }; | |
| public static List<string> Extract(string xwbPath, string outputDir, string[] cueNames) | |
| { | |
| var extractedFiles = new List<string>(); | |
| string bankBaseName = Path.GetFileNameWithoutExtension(xwbPath); | |
| try | |
| { | |
| using (var fs = new FileStream(xwbPath, FileMode.Open, FileAccess.Read, FileShare.Read)) | |
| using (var reader = new EndianReader(fs)) | |
| { | |
| byte[] sig = reader.ReadBytes(4); | |
| if (sig.Length < 4) return extractedFiles; | |
| Endianness endian = Endianness.Little; | |
| if (sig[0] == 'D' && sig[1] == 'N' && sig[2] == 'B' && sig[3] == 'W') | |
| { | |
| endian = Endianness.Big; // Xbox 360 | |
| } | |
| else if (!(sig[0] == 'W' && sig[1] == 'B' && sig[2] == 'N' && sig[3] == 'D')) | |
| { | |
| return extractedFiles; | |
| } | |
| int version = reader.ReadInt32(endian); | |
| reader.ReadInt32(endian); // Header Version | |
| int[] segOffsets = new int[5]; | |
| int[] segLengths = new int[5]; | |
| for (int i = 0; i < 5; i++) | |
| { | |
| segOffsets[i] = reader.ReadInt32(endian); | |
| segLengths[i] = reader.ReadInt32(endian); | |
| } | |
| reader.BaseStream.Seek(segOffsets[0], SeekOrigin.Begin); | |
| uint flags = reader.ReadUInt32(endian); | |
| int trackCount = reader.ReadInt32(endian); | |
| reader.BaseStream.Seek(64, SeekOrigin.Current); // Bank Name | |
| uint metaElemSize = reader.ReadUInt32(endian); | |
| uint nameElemSize = reader.ReadUInt32(endian); | |
| uint alignment = reader.ReadUInt32(endian); | |
| if (alignment == 0) alignment = 2048; | |
| uint compactFormat = reader.ReadUInt32(endian); | |
| bool isCompact = (flags & 0x00020000) != 0; | |
| int metaOffset = segOffsets[1]; | |
| int namesOffset = segOffsets[3]; | |
| int namesLength = segLengths[3]; | |
| int waveDataOffset = segOffsets[4]; | |
| int waveDataLength = segLengths[4]; | |
| // 1. Попытка прочесть встроенные в XWB имена треков (Segment 3) | |
| string[] embeddedNames = TryReadEmbeddedNames(reader, namesOffset, namesLength, nameElemSize, trackCount); | |
| // 2. Попытка найти текстовый треклист (TrackList.txt / <BankName>_tracks.txt) | |
| string[] txtTracks = TryReadExternalTrackList(Path.GetDirectoryName(xwbPath), bankBaseName); | |
| uint[] compactEntries = null; | |
| if (isCompact) | |
| { | |
| compactEntries = new uint[trackCount]; | |
| reader.BaseStream.Seek(metaOffset, SeekOrigin.Begin); | |
| for (int i = 0; i < trackCount; i++) | |
| { | |
| compactEntries[i] = reader.ReadUInt32(endian); | |
| } | |
| } | |
| for (int i = 0; i < trackCount; i++) | |
| { | |
| uint format = 0; | |
| uint playOffset = 0; | |
| uint playLength = 0; | |
| uint duration = 0; | |
| if (isCompact) | |
| { | |
| format = compactFormat; | |
| uint val = compactEntries[i]; | |
| uint dataOffset = val & 0x1FFFFF; | |
| uint lengthDeviation = (val >> 21) & 0x7FF; | |
| playOffset = dataOffset * alignment; | |
| if (i < trackCount - 1) | |
| { | |
| uint nextOffset = (compactEntries[i + 1] & 0x1FFFFF) * alignment; | |
| playLength = nextOffset - playOffset - lengthDeviation; | |
| } | |
| else | |
| { | |
| playLength = (uint)waveDataLength - playOffset - lengthDeviation; | |
| } | |
| duration = playLength * 2; | |
| } | |
| else | |
| { | |
| reader.BaseStream.Seek(metaOffset + (i * metaElemSize), SeekOrigin.Begin); | |
| uint flagsAndDuration = reader.ReadUInt32(endian); | |
| duration = flagsAndDuration >> 4; | |
| format = reader.ReadUInt32(endian); | |
| playOffset = reader.ReadUInt32(endian); | |
| playLength = reader.ReadUInt32(endian); | |
| } | |
| if (playLength == 0 || (waveDataOffset + playOffset + playLength) > fs.Length) | |
| continue; | |
| uint codec = format & 3; // 0=PCM, 1=XMA, 2=ADPCM, 3=WMA | |
| uint channels = (format >> 2) & 7; | |
| uint sampleRate = (format >> 5) & 0x3FFFF; | |
| uint blockAlign = (format >> 23) & 0xFF; | |
| reader.BaseStream.Seek(waveDataOffset + playOffset, SeekOrigin.Begin); | |
| byte[] rawData = reader.ReadBytes((int)playLength); | |
| string trackName = ResolveTrackName(bankBaseName, i, embeddedNames, cueNames, txtTracks); | |
| // Отображение прогресса распаковки треков WaveBank в консоли | |
| Tui.DrawProgress(i + 1, trackCount, string.Format("{0} [{1}]", trackName, codec == 3 ? "WMA" : (codec == 1 ? "XMA" : "WAV"))); | |
| // 1. PCM | |
| if (codec == 0) | |
| { | |
| string outFile = Path.Combine(outputDir, trackName + ".wav"); | |
| using (var outFs = new FileStream(outFile, FileMode.Create, FileAccess.Write)) | |
| using (var bw = new BinaryWriter(outFs)) | |
| { | |
| short bits = 16; | |
| uint bytesPerSec = sampleRate * channels * (uint)(bits / 8); | |
| short block = (short)(channels * (bits / 8)); | |
| bw.Write(Encoding.ASCII.GetBytes("RIFF")); | |
| bw.Write(rawData.Length + 36); | |
| bw.Write(Encoding.ASCII.GetBytes("WAVEfmt ")); | |
| bw.Write(16); | |
| bw.Write((short)1); | |
| bw.Write((short)channels); | |
| bw.Write((int)sampleRate); | |
| bw.Write((int)bytesPerSec); | |
| bw.Write(block); | |
| bw.Write(bits); | |
| bw.Write(Encoding.ASCII.GetBytes("data")); | |
| bw.Write(rawData.Length); | |
| bw.Write(rawData); | |
| } | |
| extractedFiles.Add(outFile); | |
| } | |
| // 2. MS-ADPCM -> WAV | |
| else if (codec == 2) | |
| { | |
| short adpcmBlock = (short)((blockAlign + 22) * channels); | |
| byte[] pcmData = ADPCMConverter.ConvertToPCM(rawData, (short)channels, adpcmBlock); | |
| if (pcmData.Length > 0) | |
| { | |
| string outFile = Path.Combine(outputDir, trackName + ".wav"); | |
| using (var outFs = new FileStream(outFile, FileMode.Create, FileAccess.Write)) | |
| using (var bw = new BinaryWriter(outFs)) | |
| { | |
| short bits = 16; | |
| uint bytesPerSec = sampleRate * channels * (uint)(bits / 8); | |
| short block = (short)(channels * (bits / 8)); | |
| bw.Write(Encoding.ASCII.GetBytes("RIFF")); | |
| bw.Write(pcmData.Length + 36); | |
| bw.Write(Encoding.ASCII.GetBytes("WAVEfmt ")); | |
| bw.Write(16); | |
| bw.Write((short)1); | |
| bw.Write((short)channels); | |
| bw.Write((int)sampleRate); | |
| bw.Write((int)bytesPerSec); | |
| bw.Write(block); | |
| bw.Write(bits); | |
| bw.Write(Encoding.ASCII.GetBytes("data")); | |
| bw.Write(pcmData.Length); | |
| bw.Write(pcmData); | |
| } | |
| extractedFiles.Add(outFile); | |
| } | |
| } | |
| // 3. WMA / xWMA -> .wma | |
| // В методе Extract, замени блок для codec == 3 (WMA) на: | |
| else if (codec == 3) | |
| { | |
| // Попытка 1: сохранить как .wma и сконвертировать через ffmpeg | |
| string wmaFile = Path.Combine(outputDir, trackName + ".wma"); | |
| string wavFile = Path.Combine(outputDir, trackName + ".wav"); | |
| uint bIndex = blockAlign >> 5; | |
| uint aIndex = blockAlign & 0x1F; | |
| uint avgBytesPerSec = bIndex < WmaAvgBytesPerSec.Length ? WmaAvgBytesPerSec[bIndex] : 8000; | |
| uint align = aIndex < WmaBlockAlign.Length ? WmaBlockAlign[aIndex] : 2048; | |
| // Записываем xWMA как есть | |
| using (var outFs = new FileStream(wmaFile, FileMode.Create, FileAccess.Write)) | |
| using (var bw = new BinaryWriter(outFs)) | |
| { | |
| bw.Write(Encoding.ASCII.GetBytes("RIFF")); | |
| bw.Write(rawData.Length + 46); | |
| bw.Write(Encoding.ASCII.GetBytes("XWMAfmt ")); | |
| bw.Write(18); | |
| bw.Write((short)353); | |
| bw.Write((short)channels); | |
| bw.Write((int)sampleRate); | |
| bw.Write((int)avgBytesPerSec); | |
| bw.Write((short)align); | |
| bw.Write((short)16); | |
| bw.Write((short)0); | |
| bw.Write(Encoding.ASCII.GetBytes("data")); | |
| bw.Write(rawData.Length); | |
| bw.Write(rawData); | |
| } | |
| // Попытка конвертации через ffmpeg | |
| bool converted = TryConvertWithFFmpeg(wmaFile, wavFile); | |
| if (!converted) | |
| { | |
| // Fallback: генерируем тишину нужной длительности | |
| int durationSec = (int)(rawData.Length / avgBytesPerSec) + 1; | |
| GenerateSilentWav(wavFile, (int)sampleRate, (int)channels, durationSec); | |
| Logger.Log(LogLevel.Warning, string.Format( | |
| "Generated silent WAV fallback for '{0}' (replace manually)", trackName)); | |
| } | |
| // Удаляем .wma, оставляем .wav | |
| if (File.Exists(wavFile)) | |
| { | |
| extractedFiles.Add(wavFile); | |
| try { File.Delete(wmaFile); } catch { } | |
| } | |
| else | |
| { | |
| extractedFiles.Add(wmaFile); // если ничего не помогло | |
| } | |
| } | |
| } | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| Logger.Log(LogLevel.Trace, string.Format("WaveBank unpack error: {0}", ex.Message)); | |
| } | |
| return extractedFiles; | |
| } | |
| private static bool TryConvertWithFFmpeg(string input, string output) | |
| { | |
| try | |
| { | |
| string ffmpeg = FindTool("ffmpeg.exe"); | |
| if (ffmpeg == null) return false; | |
| var psi = new ProcessStartInfo | |
| { | |
| FileName = ffmpeg, | |
| Arguments = string.Format("-y -i \"{0}\" -acodec pcm_s16le \"{1}\"", input, output), | |
| CreateNoWindow = true, | |
| UseShellExecute = false, | |
| RedirectStandardError = true | |
| }; | |
| using (var proc = Process.Start(psi)) | |
| { | |
| proc.StandardError.ReadToEnd(); | |
| proc.WaitForExit(30000); | |
| return proc.ExitCode == 0 && File.Exists(output); | |
| } | |
| } | |
| catch { return false; } | |
| } | |
| private static string FindTool(string name) | |
| { | |
| string local = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, name); | |
| if (File.Exists(local)) return local; | |
| string pathEnv = Environment.GetEnvironmentVariable("PATH") ?? ""; | |
| foreach (string dir in pathEnv.Split(Path.PathSeparator)) | |
| { | |
| try | |
| { | |
| string p = Path.Combine(dir.Trim(), name); | |
| if (File.Exists(p)) return p; | |
| } | |
| catch { } | |
| } | |
| return null; | |
| } | |
| private static void GenerateSilentWav(string path, int sampleRate, int channels, int durationSec) | |
| { | |
| int dataSize = sampleRate * channels * 2 * durationSec; | |
| using (var fs = new FileStream(path, FileMode.Create)) | |
| using (var bw = new BinaryWriter(fs)) | |
| { | |
| bw.Write(Encoding.ASCII.GetBytes("RIFF")); | |
| bw.Write(dataSize + 36); | |
| bw.Write(Encoding.ASCII.GetBytes("WAVEfmt ")); | |
| bw.Write(16); | |
| bw.Write((short)1); | |
| bw.Write((short)channels); | |
| bw.Write(sampleRate); | |
| bw.Write(sampleRate * channels * 2); | |
| bw.Write((short)(channels * 2)); | |
| bw.Write((short)16); | |
| bw.Write(Encoding.ASCII.GetBytes("data")); | |
| bw.Write(dataSize); | |
| bw.Write(new byte[dataSize]); // тишина | |
| } | |
| } | |
| private static byte[] BuildXma2RiffHeader(int sampleRate, int channels, int rawDataLength, int samplesCount) | |
| { | |
| using (var ms = new MemoryStream()) | |
| using (var bw = new BinaryWriter(ms)) | |
| { | |
| int bitsPerSample = 16; | |
| int blockAlign = 4; | |
| int avgBytesPerSec = rawDataLength; // Используется энкодером | |
| short extraSize = 34; // cbSize структуры XMA2 | |
| // RIFF Chunk | |
| bw.Write(Encoding.ASCII.GetBytes("RIFF")); | |
| bw.Write(rawDataLength + 44 + extraSize); // Полный размер | |
| bw.Write(Encoding.ASCII.GetBytes("WAVE")); | |
| // fmt Chunk | |
| bw.Write(Encoding.ASCII.GetBytes("fmt ")); | |
| bw.Write(18 + extraSize); // Размер fmt с расширением | |
| bw.Write((short)0x0166); // WAVE_FORMAT_XMA2 | |
| bw.Write((short)channels); | |
| bw.Write(sampleRate); | |
| bw.Write(avgBytesPerSec); | |
| bw.Write((short)blockAlign); | |
| bw.Write((short)bitsPerSample); | |
| bw.Write(extraSize); | |
| // Тело XMA2WAVEFORMATEX | |
| bw.Write((short)1); // NumStreams | |
| bw.Write(channels == 2 ? 3 : 1); // ChannelMask (Stereo / Mono) | |
| bw.Write(samplesCount > 0 ? samplesCount : rawDataLength * 2); // SamplesEncoded | |
| bw.Write(0x10000); // BytesPerBlock (64KB) | |
| bw.Write(0); // PlayBegin | |
| bw.Write(samplesCount > 0 ? samplesCount : rawDataLength * 2); // PlayLength | |
| bw.Write(0); // LoopBegin | |
| bw.Write(0); // LoopLength | |
| bw.Write((byte)0); // LoopCount | |
| bw.Write((byte)3); // EncoderVersion (v3/v4) | |
| bw.Write((short)1); // BlockCount | |
| // data Chunk | |
| bw.Write(Encoding.ASCII.GetBytes("data")); | |
| bw.Write(rawDataLength); | |
| return ms.ToArray(); | |
| } | |
| } | |
| private static string[] TryReadEmbeddedNames(EndianReader reader, int offset, int length, uint nameElemSize, int trackCount) | |
| { | |
| if (offset <= 0 || length <= 0 || nameElemSize <= 0 || length < nameElemSize * trackCount) | |
| return null; | |
| try | |
| { | |
| reader.BaseStream.Seek(offset, SeekOrigin.Begin); | |
| string[] names = new string[trackCount]; | |
| char[] invalid = Path.GetInvalidFileNameChars(); | |
| for (int i = 0; i < trackCount; i++) | |
| { | |
| byte[] raw = reader.ReadBytes((int)nameElemSize); | |
| string rawName = Encoding.ASCII.GetString(raw).Trim('\0', ' '); | |
| var sb = new StringBuilder(); | |
| for (int c = 0; c < rawName.Length; c++) | |
| { | |
| if (Array.IndexOf(invalid, rawName[c]) < 0) sb.Append(rawName[c]); | |
| } | |
| names[i] = sb.ToString().Trim(); | |
| } | |
| return names; | |
| } | |
| catch { return null; } | |
| } | |
| private static string[] TryReadExternalTrackList(string dir, string bankName) | |
| { | |
| try | |
| { | |
| string[] possibleFiles = new string[] | |
| { | |
| Path.Combine(dir, bankName + "_tracks.txt"), | |
| Path.Combine(dir, "TrackList.txt"), | |
| Path.Combine(dir, "tracks.txt") | |
| }; | |
| for (int i = 0; i < possibleFiles.Length; i++) | |
| { | |
| if (File.Exists(possibleFiles[i])) | |
| { | |
| var lines = new List<string>(); | |
| string[] allLines = File.ReadAllLines(possibleFiles[i], Encoding.UTF8); | |
| char[] invalid = Path.GetInvalidFileNameChars(); | |
| for (int j = 0; j < allLines.Length; j++) | |
| { | |
| string trimmed = allLines[j].Trim(); | |
| if (string.IsNullOrEmpty(trimmed)) continue; | |
| var sb = new StringBuilder(); | |
| for (int c = 0; c < trimmed.Length; c++) | |
| { | |
| if (Array.IndexOf(invalid, trimmed[c]) < 0) sb.Append(trimmed[c]); | |
| } | |
| lines.Add(sb.ToString().Trim()); | |
| } | |
| if (lines.Count > 0) return lines.ToArray(); | |
| } | |
| } | |
| } | |
| catch { } | |
| return null; | |
| } | |
| private static string ResolveTrackName(string bankBaseName, int index, string[] embedded, string[] cues, string[] txtTracks) | |
| { | |
| if (embedded != null && index < embedded.Length && !string.IsNullOrEmpty(embedded[index])) | |
| return embedded[index]; | |
| if (txtTracks != null && index < txtTracks.Length && !string.IsNullOrEmpty(txtTracks[index])) | |
| return txtTracks[index]; | |
| if (cues != null && index < cues.Length && !string.IsNullOrEmpty(cues[index])) | |
| return cues[index]; | |
| return string.Format("{0}_{1:D2}", bankBaseName, index + 1); | |
| } | |
| } | |
| internal static class ADPCMConverter | |
| { | |
| private static readonly int[] AdaptionTable = new int[16] | |
| { | |
| 230, 230, 230, 230, 307, 409, 512, 614, 768, 614, | |
| 512, 409, 307, 230, 230, 230 | |
| }; | |
| private static readonly int[] AdaptCoeff1 = new int[7] { 256, 512, 0, 192, 240, 460, 392 }; | |
| private static readonly int[] AdaptCoeff2 = new int[7] { 0, -256, 0, 64, 0, -208, -232 }; | |
| private static short CalculateSample(int nibble, int predictor, short[] sample1, short[] sample2, short[] delta) | |
| { | |
| byte b = (byte)nibble; | |
| if ((b & 8) == 8) b -= 16; | |
| // Защита от выхода за пределы таблиц коэффициентов | |
| if (predictor < 0) predictor = 0; | |
| if (predictor >= AdaptCoeff1.Length) predictor = AdaptCoeff1.Length - 1; | |
| if (nibble < 0) nibble = 0; | |
| if (nibble >= AdaptionTable.Length) nibble = AdaptionTable.Length - 1; | |
| int num = (sample1[0] * AdaptCoeff1[predictor] + sample2[0] * AdaptCoeff2[predictor]) / 256; | |
| num += b * delta[0]; | |
| short sample = (num < -32768) ? short.MinValue : ((num > 32767) ? short.MaxValue : (short)num); | |
| sample2[0] = sample1[0]; | |
| sample1[0] = sample; | |
| delta[0] = (short)(AdaptionTable[nibble] * delta[0] / 256); | |
| if (delta[0] < 16) delta[0] = 16; | |
| return sample; | |
| } | |
| public static byte[] ConvertToPCM(byte[] source, short numChannels, short blockAlign) | |
| { | |
| if (source == null || source.Length < blockAlign || blockAlign <= 0) return new byte[0]; | |
| using (var binaryReader = new BinaryReader(new MemoryStream(source))) | |
| using (var memoryStream = new MemoryStream()) | |
| using (var binaryWriter = new BinaryWriter(memoryStream)) | |
| { | |
| long maxPos = source.Length - blockAlign; | |
| if (numChannels == 1) | |
| { | |
| while (binaryReader.BaseStream.Position <= maxPos && binaryReader.BaseStream.Position + 7 <= source.Length) | |
| { | |
| int predictor = binaryReader.ReadByte() & 0xFF; | |
| short delta = binaryReader.ReadInt16(); | |
| short sample1 = binaryReader.ReadInt16(); | |
| short sample2 = binaryReader.ReadInt16(); | |
| binaryWriter.Write(sample2); | |
| binaryWriter.Write(sample1); | |
| short[] d = new short[] { delta }; | |
| short[] s1 = new short[] { sample1 }; | |
| short[] s2 = new short[] { sample2 }; | |
| int samplesToRead = Math.Max(0, (blockAlign - 7) * 2); | |
| for (int i = 0; i < samplesToRead && binaryReader.BaseStream.Position < source.Length; i += 2) | |
| { | |
| int rawByte = binaryReader.ReadByte() & 0xFF; | |
| binaryWriter.Write(CalculateSample((rawByte >> 4) & 0xF, predictor, s1, s2, d)); | |
| binaryWriter.Write(CalculateSample(rawByte & 0xF, predictor, s1, s2, d)); | |
| } | |
| } | |
| } | |
| else if (numChannels == 2) | |
| { | |
| while (binaryReader.BaseStream.Position <= maxPos && binaryReader.BaseStream.Position + 14 <= source.Length) | |
| { | |
| int predictorL = binaryReader.ReadByte() & 0xFF; | |
| int predictorR = binaryReader.ReadByte() & 0xFF; | |
| short deltaL = binaryReader.ReadInt16(); | |
| short deltaR = binaryReader.ReadInt16(); | |
| short sample1L = binaryReader.ReadInt16(); | |
| short sample1R = binaryReader.ReadInt16(); | |
| short sample2L = binaryReader.ReadInt16(); | |
| short sample2R = binaryReader.ReadInt16(); | |
| binaryWriter.Write(sample2L); | |
| binaryWriter.Write(sample2R); | |
| binaryWriter.Write(sample1L); | |
| binaryWriter.Write(sample1R); | |
| short[] dL = new short[] { deltaL }; | |
| short[] dR = new short[] { deltaR }; | |
| short[] s1L = new short[] { sample1L }; | |
| short[] s1R = new short[] { sample1R }; | |
| short[] s2L = new short[] { sample2L }; | |
| short[] s2R = new short[] { sample2R }; | |
| int bytesToRead = Math.Max(0, blockAlign - 14); | |
| for (int k = 0; k < bytesToRead && binaryReader.BaseStream.Position < source.Length; k++) | |
| { | |
| int rawByte = binaryReader.ReadByte() & 0xFF; | |
| binaryWriter.Write(CalculateSample((rawByte >> 4) & 0xF, predictorL, s1L, s2L, dL)); | |
| binaryWriter.Write(CalculateSample(rawByte & 0xF, predictorR, s1R, s2R, dR)); | |
| } | |
| } | |
| } | |
| binaryWriter.Flush(); | |
| return memoryStream.ToArray(); | |
| } | |
| } | |
| } | |
| internal static class XnbExtensions | |
| { | |
| public static int Read7BitEncodedInt(this BinaryReader reader) | |
| { | |
| int num = 0, num2 = 0; | |
| byte b; | |
| do | |
| { | |
| b = reader.ReadByte(); | |
| num |= (b & 0x7F) << num2; | |
| num2 += 7; | |
| } | |
| while ((b & 0x80) != 0); | |
| return num; | |
| } | |
| public static string Read7BitEncodedString(this BinaryReader reader) | |
| { | |
| int count = reader.Read7BitEncodedInt(); | |
| return Encoding.UTF8.GetString(reader.ReadBytes(count)); | |
| } | |
| public static void Fill<T>(this T[] array, T with) | |
| { | |
| for (int i = 0; i < array.Length; i++) array[i] = with; | |
| } | |
| public static bool ReadAndCompareString(this BinaryReader reader, string s) | |
| { | |
| byte[] bytes = Encoding.UTF8.GetBytes(s); | |
| byte[] array = reader.ReadBytes(bytes.Length); | |
| if (array.Length != bytes.Length) return false; | |
| for (int i = 0; i < array.Length; i++) | |
| { | |
| if (array[i] != bytes[i]) return false; | |
| } | |
| return true; | |
| } | |
| } | |
| internal static class DxtUtil | |
| { | |
| public static byte[] DecompressDxt1(byte[] imageData, int width, int height) | |
| { | |
| using (var ms = new MemoryStream(imageData)) return DecompressDxt1(ms, width, height); | |
| } | |
| public static byte[] DecompressDxt1(Stream stream, int width, int height) | |
| { | |
| byte[] array = new byte[width * height * 4]; | |
| using (var r = new BinaryReader(stream)) | |
| { | |
| int bx = (width + 3) / 4, by = (height + 3) / 4; | |
| for (int y = 0; y < by; y++) | |
| for (int x = 0; x < bx; x++) | |
| DecompressDxt1Block(r, x, y, width, height, array); | |
| return array; | |
| } | |
| } | |
| public static byte[] DecompressDxt3(byte[] imageData, int width, int height) | |
| { | |
| using (var ms = new MemoryStream(imageData)) return DecompressDxt3(ms, width, height); | |
| } | |
| public static byte[] DecompressDxt3(Stream stream, int width, int height) | |
| { | |
| byte[] array = new byte[width * height * 4]; | |
| using (var r = new BinaryReader(stream)) | |
| { | |
| int bx = (width + 3) / 4, by = (height + 3) / 4; | |
| for (int y = 0; y < by; y++) | |
| for (int x = 0; x < bx; x++) | |
| DecompressDxt3Block(r, x, y, width, height, array); | |
| return array; | |
| } | |
| } | |
| public static byte[] DecompressDxt5(byte[] imageData, int width, int height) | |
| { | |
| using (var ms = new MemoryStream(imageData)) return DecompressDxt5(ms, width, height); | |
| } | |
| public static byte[] DecompressDxt5(Stream stream, int width, int height) | |
| { | |
| byte[] array = new byte[width * height * 4]; | |
| using (var r = new BinaryReader(stream)) | |
| { | |
| int bx = (width + 3) / 4, by = (height + 3) / 4; | |
| for (int y = 0; y < by; y++) | |
| for (int x = 0; x < bx; x++) | |
| DecompressDxt5Block(r, x, y, width, height, array); | |
| return array; | |
| } | |
| } | |
| private static void ConvertRgb565ToRgb888(ushort color, out byte r, out byte g, out byte b) | |
| { | |
| int num = (color >> 11) * 255 + 16; | |
| r = (byte)((num / 32 + num) / 32); | |
| num = ((color & 0x7E0) >> 5) * 255 + 32; | |
| g = (byte)((num / 64 + num) / 64); | |
| num = (color & 0x1F) * 255 + 16; | |
| b = (byte)((num / 32 + num) / 32); | |
| } | |
| private static void DecompressDxt1Block(BinaryReader r, int x, int y, int width, int height, byte[] target) | |
| { | |
| ushort c0 = r.ReadUInt16(), c1 = r.ReadUInt16(); | |
| byte r0, g0, b0, r1, g1, b1; | |
| ConvertRgb565ToRgb888(c0, out r0, out g0, out b0); | |
| ConvertRgb565ToRgb888(c1, out r1, out g1, out b1); | |
| uint code = r.ReadUInt32(); | |
| for (int i = 0; i < 4; i++) | |
| { | |
| for (int j = 0; j < 4; j++) | |
| { | |
| byte pr = 0, pg = 0, pb = 0, pa = 255; | |
| uint idx = (code >> (2 * (4 * i + j))) & 3; | |
| if (c0 > c1) | |
| { | |
| if (idx == 0) { pr = r0; pg = g0; pb = b0; } | |
| else if (idx == 1) { pr = r1; pg = g1; pb = b1; } | |
| else if (idx == 2) { pr = (byte)((2 * r0 + r1) / 3); pg = (byte)((2 * g0 + g1) / 3); pb = (byte)((2 * b0 + b1) / 3); } | |
| else { pr = (byte)((r0 + 2 * r1) / 3); pg = (byte)((g0 + 2 * g1) / 3); pb = (byte)((b0 + 2 * b1) / 3); } | |
| } | |
| else | |
| { | |
| if (idx == 0) { pr = r0; pg = g0; pb = b0; } | |
| else if (idx == 1) { pr = r1; pg = g1; pb = b1; } | |
| else if (idx == 2) { pr = (byte)((r0 + r1) / 2); pg = (byte)((g0 + g1) / 2); pb = (byte)((b0 + b1) / 2); } | |
| else { pr = pg = pb = pa = 0; } | |
| } | |
| int px = (x << 2) + j, py = (y << 2) + i; | |
| if (px < width && py < height) | |
| { | |
| int ofs = (py * width + px) << 2; | |
| target[ofs] = pr; target[ofs + 1] = pg; target[ofs + 2] = pb; target[ofs + 3] = pa; | |
| } | |
| } | |
| } | |
| } | |
| private static void DecompressDxt3Block(BinaryReader r, int x, int y, int width, int height, byte[] target) | |
| { | |
| byte[] aBytes = r.ReadBytes(8); | |
| ushort c0 = r.ReadUInt16(), c1 = r.ReadUInt16(); | |
| byte r0, g0, b0, r1, g1, b1; | |
| ConvertRgb565ToRgb888(c0, out r0, out g0, out b0); | |
| ConvertRgb565ToRgb888(c1, out r1, out g1, out b1); | |
| uint code = r.ReadUInt32(); | |
| int aIdx = 0; | |
| for (int i = 0; i < 4; i++) | |
| { | |
| for (int j = 0; j < 4; j++) | |
| { | |
| byte aNib = (aIdx % 2 == 0) ? (byte)(aBytes[aIdx / 2] & 0x0F) : (byte)((aBytes[aIdx / 2] & 0xF0) >> 4); | |
| byte pa = (byte)(aNib | (aNib << 4)); | |
| aIdx++; | |
| byte pr = 0, pg = 0, pb = 0; | |
| uint idx = (code >> (2 * (4 * i + j))) & 3; | |
| if (idx == 0) { pr = r0; pg = g0; pb = b0; } | |
| else if (idx == 1) { pr = r1; pg = g1; pb = b1; } | |
| else if (idx == 2) { pr = (byte)((2 * r0 + r1) / 3); pg = (byte)((2 * g0 + g1) / 3); pb = (byte)((2 * b0 + b1) / 3); } | |
| else { pr = (byte)((r0 + 2 * r1) / 3); pg = (byte)((g0 + 2 * g1) / 3); pb = (byte)((b0 + 2 * b1) / 3); } | |
| int px = (x << 2) + j, py = (y << 2) + i; | |
| if (px < width && py < height) | |
| { | |
| int ofs = (py * width + px) << 2; | |
| target[ofs] = pr; target[ofs + 1] = pg; target[ofs + 2] = pb; target[ofs + 3] = pa; | |
| } | |
| } | |
| } | |
| } | |
| private static void DecompressDxt5Block(BinaryReader r, int x, int y, int width, int height, byte[] target) | |
| { | |
| byte a0 = r.ReadByte(), a1 = r.ReadByte(); | |
| ulong aTable = 0; | |
| for (int b = 0; b < 6; b++) aTable |= ((ulong)r.ReadByte()) << (b * 8); | |
| ushort c0 = r.ReadUInt16(), c1 = r.ReadUInt16(); | |
| byte r0, g0, b0, r1, g1, b1; | |
| ConvertRgb565ToRgb888(c0, out r0, out g0, out b0); | |
| ConvertRgb565ToRgb888(c1, out r1, out g1, out b1); | |
| uint code = r.ReadUInt32(); | |
| for (int i = 0; i < 4; i++) | |
| { | |
| for (int j = 0; j < 4; j++) | |
| { | |
| uint aBits = (uint)((aTable >> (3 * (4 * i + j))) & 7); | |
| byte pa = 255; | |
| if (aBits == 0) pa = a0; | |
| else if (aBits == 1) pa = a1; | |
| else if (a0 > a1) pa = (byte)(((8 - aBits) * a0 + (aBits - 1) * a1) / 7); | |
| else if (aBits == 6) pa = 0; | |
| else if (aBits == 7) pa = 255; | |
| else pa = (byte)(((6 - aBits) * a0 + (aBits - 1) * a1) / 5); | |
| byte pr = 0, pg = 0, pb = 0; | |
| uint idx = (code >> (2 * (4 * i + j))) & 3; | |
| if (idx == 0) { pr = r0; pg = g0; pb = b0; } | |
| else if (idx == 1) { pr = r1; pg = g1; pb = b1; } | |
| else if (idx == 2) { pr = (byte)((2 * r0 + r1) / 3); pg = (byte)((2 * g0 + g1) / 3); pb = (byte)((2 * b0 + b1) / 3); } | |
| else { pr = (byte)((r0 + 2 * r1) / 3); pg = (byte)((g0 + 2 * g1) / 3); pb = (byte)((b0 + 2 * b1) / 3); } | |
| int px = (x << 2) + j, py = (y << 2) + i; | |
| if (px < width && py < height) | |
| { | |
| int ofs = (py * width + px) << 2; | |
| target[ofs] = pr; target[ofs + 1] = pg; target[ofs + 2] = pb; target[ofs + 3] = pa; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| internal sealed class HuffTable | |
| { | |
| private short[] table; | |
| private byte[] length; | |
| private int maxSymbols, tableBits; | |
| public short[] Table { get { return table; } } | |
| public byte[] Length { get { return length; } } | |
| public HuffTable(int maxSymbols, int tableBits) | |
| { | |
| this.maxSymbols = maxSymbols; | |
| this.tableBits = tableBits; | |
| table = new short[(1 << tableBits) + (maxSymbols << 1)]; | |
| length = new byte[maxSymbols + 64]; | |
| } | |
| public void MakeDecodeTable() | |
| { | |
| byte b = 1; | |
| int num = 0, num2 = 1 << tableBits, num3 = num2 >> 1, num4 = num3; | |
| while (b <= tableBits) | |
| { | |
| for (short i = 0; i < maxSymbols; i++) | |
| { | |
| if (length[i] == b) | |
| { | |
| int n = num; | |
| if ((num += num3) > num2) return; | |
| int left = num3; | |
| while (left-- > 0) table[n++] = i; | |
| } | |
| } | |
| num3 >>= 1; | |
| b++; | |
| } | |
| if (num != num2) | |
| { | |
| for (short i = (short)num; i < num2; i++) table[i] = 0; | |
| num <<= 16; num2 <<= 16; num3 = 32768; | |
| while (b <= 16) | |
| { | |
| for (short i = 0; i < maxSymbols; i++) | |
| { | |
| if (length[i] == b) | |
| { | |
| int n = num >> 16; | |
| for (int bit = 0; bit < b - tableBits; bit++) | |
| { | |
| if (table[n] == 0) | |
| { | |
| table[num4 << 1] = 0; | |
| table[(num4 << 1) + 1] = 0; | |
| table[n] = (short)num4++; | |
| } | |
| n = table[n] << 1; | |
| if (((num >> (15 - bit)) & 1) == 1) n++; | |
| } | |
| table[n] = i; | |
| if ((num += num3) > num2) return; | |
| } | |
| } | |
| num3 >>= 1; | |
| b++; | |
| } | |
| } | |
| } | |
| public int ReadHuffSym(LzxBuffer buffer) | |
| { | |
| buffer.EnsureBits(16); | |
| int sym = table[buffer.PeekBits((byte)tableBits)]; | |
| if (sym >= maxSymbols) | |
| { | |
| int mask = 1 << (32 - tableBits); | |
| do | |
| { | |
| mask >>= 1; | |
| sym <<= 1; | |
| sym |= ((buffer.BitBuffer & mask) != 0) ? 1 : 0; | |
| if (mask == 0) throw new Exception("Huffman error"); | |
| } | |
| while ((sym = table[sym]) >= maxSymbols); | |
| } | |
| buffer.RemoveBits(length[sym]); | |
| return sym; | |
| } | |
| public void Reset() | |
| { | |
| table.Fill((short)0); | |
| length.Fill((byte)0); | |
| } | |
| } | |
| internal sealed class LzxBuffer | |
| { | |
| private BinaryReader reader; | |
| private int remainingBits, bitBuffer; | |
| public int BitBuffer { get { return bitBuffer; } } | |
| public int RemainingBits { get { return remainingBits; } } | |
| public LzxBuffer(BinaryReader reader) { this.reader = reader; } | |
| public void Reset() { remainingBits = bitBuffer = 0; } | |
| public void EnsureBits(int bitCount) | |
| { | |
| while (remainingBits < bitCount) | |
| { | |
| int b0 = reader.ReadByte() & 0xFF, b1 = reader.ReadByte() & 0xFF; | |
| bitBuffer |= ((b1 << 8) | b0) << (16 - remainingBits); | |
| remainingBits += 16; | |
| } | |
| } | |
| public int PeekBits(int bitCount) { return (int)((uint)bitBuffer >> (32 - bitCount)); } | |
| public void RemoveBits(int bitCount) | |
| { | |
| bitBuffer <<= bitCount; | |
| remainingBits -= bitCount; | |
| } | |
| public int ReadBits(int bitCount) | |
| { | |
| if (bitCount <= 0) return 0; | |
| EnsureBits(bitCount); | |
| int res = PeekBits(bitCount); | |
| RemoveBits(bitCount); | |
| return res; | |
| } | |
| } | |
| internal sealed class LzxDecoder | |
| { | |
| private enum LzxBlockType { Invalid, Verbatim, Aligned, Uncompressed } | |
| private static readonly int[] positionBase = new int[51]; | |
| private static readonly int[] extraBits = new int[52]; | |
| private int R0, R1, R2, windowSize, windowPos, blockLength, blockRemaining, intelFileSize, intelCurrentPosition; | |
| private byte[] window; | |
| private bool intelStarted, readHeader; | |
| private LzxBlockType blockType; | |
| private HuffTable preTree, mainTree, lengthTree, alignedTree; | |
| static LzxDecoder() | |
| { | |
| int num = 0; | |
| for (int i = 0; i <= 50; i += 2) | |
| { | |
| extraBits[i] = extraBits[i + 1] = num; | |
| if (i != 0 && num < 17) num++; | |
| } | |
| int baseVal = 0; | |
| for (int j = 0; j <= 50; j++) | |
| { | |
| positionBase[j] = baseVal; | |
| baseVal += 1 << extraBits[j]; | |
| } | |
| } | |
| public LzxDecoder() | |
| { | |
| preTree = new HuffTable(20, 6); | |
| mainTree = new HuffTable(656, 12); | |
| lengthTree = new HuffTable(250, 12); | |
| alignedTree = new HuffTable(8, 7); | |
| } | |
| private void Reset() | |
| { | |
| R0 = R1 = R2 = 1; | |
| readHeader = true; | |
| windowSize = 65536; | |
| window = new byte[windowSize]; | |
| window.Fill((byte)220); | |
| windowPos = 0; | |
| blockRemaining = 0; | |
| blockType = LzxBlockType.Invalid; | |
| intelCurrentPosition = 0; | |
| intelStarted = false; | |
| preTree.Reset(); mainTree.Reset(); lengthTree.Reset(); alignedTree.Reset(); | |
| } | |
| public void Decompress(BinaryReader input, int inputLength, MemoryStream output, int outputLength) | |
| { | |
| Reset(); | |
| int end = (int)input.BaseStream.Position + inputLength; | |
| int cur = (int)input.BaseStream.Position; | |
| while ((int)input.BaseStream.Position < end) | |
| { | |
| input.BaseStream.Position = cur; | |
| int hi = input.ReadByte() & 0xFF, lo = input.ReadByte() & 0xFF; | |
| int bSize = (hi << 8) | lo; | |
| int fSize = 32768; | |
| if (hi == 255) | |
| { | |
| int fLo = input.ReadByte() & 0xFF; | |
| fSize = (lo << 8) | fLo; | |
| int bHi = input.ReadByte() & 0xFF, bLo = input.ReadByte() & 0xFF; | |
| bSize = (bHi << 8) | bLo; | |
| cur += 5; | |
| } | |
| else | |
| { | |
| cur += 2; | |
| } | |
| if (bSize != 0 && fSize != 0) | |
| { | |
| DecompressBlock(input, bSize, output, fSize); | |
| cur += bSize; | |
| } | |
| else break; | |
| } | |
| } | |
| private void DecompressBlock(BinaryReader input, int inputLength, MemoryStream output, int outputLength) | |
| { | |
| BinaryWriter bw = new BinaryWriter(output); | |
| int start = (int)input.BaseStream.Position; | |
| LzxBuffer lzxBuffer = new LzxBuffer(input); | |
| if (readHeader) | |
| { | |
| if (lzxBuffer.ReadBits(1) == 1) | |
| { | |
| int hi = lzxBuffer.ReadBits(16), lo = lzxBuffer.ReadBits(16); | |
| intelFileSize = (hi << 16) | lo; | |
| } | |
| readHeader = false; | |
| } | |
| int wPos = windowPos, wSize = windowSize, r0 = R0, r1 = R1, r2 = R2, togo = outputLength; | |
| while (togo > 0) | |
| { | |
| if (blockRemaining == 0) | |
| { | |
| if (blockType == LzxBlockType.Uncompressed) | |
| { | |
| if ((blockLength & 1) == 1) input.ReadByte(); | |
| lzxBuffer.Reset(); | |
| } | |
| int bType = lzxBuffer.ReadBits(3); | |
| if (bType > 3) throw new Exception("Invalid block type"); | |
| blockType = (LzxBlockType)bType; | |
| int lHi = lzxBuffer.ReadBits(16), lLo = lzxBuffer.ReadBits(8); | |
| blockRemaining = blockLength = (lHi << 8) | lLo; | |
| if (blockType == LzxBlockType.Aligned) | |
| { | |
| for (int i = 0; i < 8; i++) alignedTree.Length[i] = (byte)lzxBuffer.ReadBits(3); | |
| alignedTree.MakeDecodeTable(); | |
| } | |
| if (blockType == LzxBlockType.Verbatim || blockType == LzxBlockType.Aligned) | |
| { | |
| ReadLengths(mainTree.Length, 0, 256, lzxBuffer); | |
| ReadLengths(mainTree.Length, 256, 512, lzxBuffer); | |
| mainTree.MakeDecodeTable(); | |
| if (mainTree.Length[232] != 0) intelStarted = true; | |
| ReadLengths(lengthTree.Length, 0, 249, lzxBuffer); | |
| lengthTree.MakeDecodeTable(); | |
| } | |
| else if (blockType == LzxBlockType.Uncompressed) | |
| { | |
| intelStarted = true; | |
| lzxBuffer.EnsureBits(16); | |
| if (lzxBuffer.RemainingBits > 16) input.BaseStream.Position -= 2; | |
| byte n0 = input.ReadByte(), n1 = input.ReadByte(), n2 = input.ReadByte(), n3 = input.ReadByte(); | |
| r0 = n0 | (n1 << 8) | (n2 << 16) | (n3 << 24); | |
| n0 = input.ReadByte(); n1 = input.ReadByte(); n2 = input.ReadByte(); n3 = input.ReadByte(); | |
| r1 = n0 | (n1 << 8) | (n2 << 16) | (n3 << 24); | |
| n0 = input.ReadByte(); n1 = input.ReadByte(); n2 = input.ReadByte(); n3 = input.ReadByte(); | |
| r2 = n0 | (n1 << 8) | (n2 << 16) | (n3 << 24); | |
| } | |
| } | |
| int thisRun = blockRemaining; | |
| if (thisRun > togo) thisRun = togo; | |
| togo -= thisRun; | |
| blockRemaining -= thisRun; | |
| wPos &= wSize - 1; | |
| if (blockType == LzxBlockType.Verbatim || blockType == LzxBlockType.Aligned) | |
| { | |
| while (thisRun > 0) | |
| { | |
| int sym = mainTree.ReadHuffSym(lzxBuffer); | |
| if (sym < 256) | |
| { | |
| window[wPos++] = (byte)sym; | |
| thisRun--; | |
| continue; | |
| } | |
| sym -= 256; | |
| int mLen = sym & 7; | |
| if (mLen == 7) mLen += lengthTree.ReadHuffSym(lzxBuffer); | |
| mLen += 2; | |
| int mPos = sym >> 3; | |
| if (mPos > 2) | |
| { | |
| if (mPos != 3) | |
| { | |
| int extra = extraBits[mPos]; | |
| int offset = 0; | |
| if (blockType == LzxBlockType.Aligned && extra >= 3) | |
| { | |
| offset = lzxBuffer.ReadBits(extra - 3) << 3; | |
| offset += alignedTree.ReadHuffSym(lzxBuffer); | |
| } | |
| else offset = lzxBuffer.ReadBits(extra); | |
| mPos = positionBase[mPos] - 2 + offset; | |
| } | |
| else mPos = 1; | |
| r2 = r1; r1 = r0; r0 = mPos; | |
| } | |
| else | |
| { | |
| int temp = (mPos == 0) ? r0 : ((mPos == 1) ? r1 : r2); | |
| if (mPos == 1) { r1 = r0; r0 = temp; } | |
| else if (mPos == 2) { r2 = r1; r1 = r0; r0 = temp; } | |
| mPos = temp; | |
| } | |
| thisRun -= mLen; | |
| int src = (wPos >= mPos) ? (wPos - mPos) : (wPos + (wSize - mPos)); | |
| int over = mPos - wPos; | |
| if (over < mLen && wPos < mPos) | |
| { | |
| mLen -= over; | |
| wPos += over; | |
| while (over-- > 0) window[wPos - over - 1] = window[src++]; | |
| src = 0; | |
| } | |
| wPos += mLen; | |
| while (mLen-- > 0) window[wPos - mLen - 1] = window[src++]; | |
| } | |
| } | |
| else if (blockType == LzxBlockType.Uncompressed) | |
| { | |
| input.Read(window, wPos, thisRun); | |
| wPos += thisRun; | |
| } | |
| } | |
| int wOut = (wPos == 0) ? wSize : wPos; | |
| bw.Write(window, wOut - outputLength, outputLength); | |
| windowPos = wPos; R0 = r0; R1 = r1; R2 = r2; | |
| } | |
| private void ReadLengths(byte[] lens, int first, int last, LzxBuffer buf) | |
| { | |
| for (int i = 0; i < 20; i++) preTree.Length[i] = (byte)buf.ReadBits(4); | |
| preTree.MakeDecodeTable(); | |
| int idx = first; | |
| while (idx < last) | |
| { | |
| int sym = preTree.ReadHuffSym(buf); | |
| if (sym == 17) | |
| { | |
| int cnt = buf.ReadBits(4) + 4; | |
| while (cnt-- > 0) lens[idx++] = 0; | |
| } | |
| else if (sym == 18) | |
| { | |
| int cnt = buf.ReadBits(5) + 20; | |
| while (cnt-- > 0) lens[idx++] = 0; | |
| } | |
| else if (sym == 19) | |
| { | |
| int cnt = buf.ReadBits(1) + 4; | |
| int prev = lens[idx] - preTree.ReadHuffSym(buf); | |
| if (prev < 0) prev += 17; | |
| while (cnt-- > 0) lens[idx++] = (byte)prev; | |
| } | |
| else | |
| { | |
| int prev = lens[idx] - sym; | |
| if (prev < 0) prev += 17; | |
| lens[idx++] = (byte)prev; | |
| } | |
| } | |
| } | |
| } | |
| internal static class XnbExtractor | |
| { | |
| private static readonly LzxDecoder lzx = new LzxDecoder(); | |
| public static bool TryUnpackXnb(string xnbPath, string outputDir, out string generatedSourceFile) | |
| { | |
| generatedSourceFile = null; | |
| try | |
| { | |
| using (var fs = new FileStream(xnbPath, FileMode.Open, FileAccess.Read, FileShare.Read)) | |
| using (var r = new BinaryReader(fs)) | |
| { | |
| if (fs.Length < 10) return false; | |
| if (!r.ReadAndCompareString("XNB")) return false; | |
| char platform = (char)r.ReadByte(); | |
| bool isBigEndian = (platform == 'x'); | |
| int version = r.ReadByte(); | |
| if (version != 5) return false; | |
| byte flags = r.ReadByte(); | |
| bool isCompressed = (flags & 0x80) != 0; | |
| int fileSize = ReadInt32(r, isBigEndian); | |
| int decompSize = isCompressed ? ReadInt32(r, isBigEndian) : fileSize; | |
| if (decompSize <= 0 || decompSize > 0x10000000) return CreateFontFallback(xnbPath, outputDir, out generatedSourceFile); | |
| Stream targetStream = null; | |
| if (isCompressed) | |
| { | |
| var ms = new MemoryStream(decompSize); | |
| lzx.Decompress(r, fileSize - 14, ms, decompSize); | |
| ms.Position = 0; | |
| targetStream = ms; | |
| } | |
| else | |
| { | |
| byte[] rawBytes = r.ReadBytes(fileSize - 10); | |
| targetStream = new MemoryStream(rawBytes); | |
| } | |
| using (targetStream) | |
| using (var br = new BinaryReader(targetStream)) | |
| { | |
| return ParseAsset(br, Path.GetFileNameWithoutExtension(xnbPath), outputDir, isBigEndian, out generatedSourceFile); | |
| } | |
| } | |
| } | |
| catch | |
| { | |
| // НЕ создаём SpriteFont для непонятных файлов! | |
| generatedSourceFile = null; | |
| return false; // Пусть файл скопируется как бинарник | |
| } | |
| } | |
| private static bool CreateFontFallback(string xnbPath, string outputDir, out string outPath) | |
| { | |
| string name = Path.GetFileNameWithoutExtension(xnbPath); | |
| outPath = Path.Combine(outputDir, name + ".spritefont"); | |
| if (File.Exists(outPath)) return true; | |
| string font = name.IndexOf("Andy", StringComparison.OrdinalIgnoreCase) >= 0 ? "Andy" : "Arial"; | |
| int size = 12; | |
| if (name.IndexOf("big", StringComparison.OrdinalIgnoreCase) >= 0) size = 18; | |
| if (name.IndexOf("stack", StringComparison.OrdinalIgnoreCase) >= 0) size = 14; | |
| string xml = string.Format( | |
| @"<?xml version=""1.0"" encoding=""utf-8""?> | |
| <XnaContent xmlns:Graphics=""Microsoft.Xna.Framework.Content.Pipeline.Graphics""> | |
| <Asset Type=""Graphics:FontDescription""> | |
| <FontName>{0}</FontName> | |
| <Size>{1}</Size> | |
| <Spacing>0</Spacing> | |
| <UseKerning>true</UseKerning> | |
| <Style>Regular</Style> | |
| <CharacterRegions> | |
| <CharacterRegion><Start> </Start><End>~</End></CharacterRegion> | |
| <CharacterRegion><Start>А</Start><End>я</End></CharacterRegion> | |
| </CharacterRegions> | |
| </Asset> | |
| </XnaContent>", font, size); | |
| File.WriteAllText(outPath, xml, Encoding.UTF8); | |
| return true; | |
| } | |
| private static bool ParseAsset(BinaryReader r, string name, string outDir, bool isBigEndian, out string outPath) | |
| { | |
| outPath = null; | |
| int typeCount = r.Read7BitEncodedInt(); | |
| if (typeCount <= 0 || typeCount > 128) return CreateFontFallback(Path.Combine(outDir, name + ".xnb"), outDir, out outPath); | |
| string primaryType = r.Read7BitEncodedString(); | |
| ReadInt32(r, isBigEndian); | |
| int comma = primaryType.IndexOf(','); | |
| if (comma != -1) primaryType = primaryType.Substring(0, comma).Trim(); | |
| for (int i = 1; i < typeCount; i++) | |
| { | |
| r.Read7BitEncodedString(); | |
| ReadInt32(r, isBigEndian); | |
| } | |
| r.Read7BitEncodedInt(); | |
| int primaryIndex = r.Read7BitEncodedInt(); | |
| if (primaryIndex != 1) return false; | |
| if (primaryType.IndexOf("Texture2DReader", StringComparison.OrdinalIgnoreCase) >= 0) | |
| { | |
| int format = ReadInt32(r, isBigEndian); | |
| int width = ReadInt32(r, isBigEndian); | |
| int height = ReadInt32(r, isBigEndian); | |
| int mipCount = ReadInt32(r, isBigEndian); | |
| int dataSize = ReadInt32(r, isBigEndian); | |
| if (width <= 0 || height <= 0 || dataSize <= 0 || dataSize > r.BaseStream.Length) | |
| return false; | |
| byte[] raw = r.ReadBytes(dataSize); | |
| if (format == 4) raw = DxtUtil.DecompressDxt1(raw, width, height); | |
| else if (format == 5) raw = DxtUtil.DecompressDxt3(raw, width, height); | |
| else if (format == 6) raw = DxtUtil.DecompressDxt5(raw, width, height); | |
| else if (format != 0) return false; | |
| for (int i = 0; i < width * height * 4 && i + 3 < raw.Length; i += 4) | |
| { | |
| byte temp = raw[i]; | |
| raw[i] = raw[i + 2]; | |
| raw[i + 2] = temp; | |
| } | |
| outPath = Path.Combine(outDir, name + ".png"); | |
| using (var bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb)) | |
| { | |
| BitmapData bd = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); | |
| Marshal.Copy(raw, 0, bd.Scan0, Math.Min(raw.Length, bd.Stride * height)); | |
| bmp.UnlockBits(bd); | |
| bmp.Save(outPath, ImageFormat.Png); | |
| } | |
| return true; | |
| } | |
| if (primaryType.IndexOf("SoundEffectReader", StringComparison.OrdinalIgnoreCase) >= 0) | |
| { | |
| int formatSize = ReadInt32(r, isBigEndian); | |
| int formatTag = ReadInt16(r, isBigEndian); | |
| int channels = ReadInt16(r, isBigEndian) & 0xFFFF; | |
| int samplesPerSec = ReadInt32(r, isBigEndian); | |
| int avgBytesPerSec = ReadInt32(r, isBigEndian); | |
| int blockAlign = ReadInt16(r, isBigEndian) & 0xFFFF; | |
| int bitsPerSample = ReadInt16(r, isBigEndian) & 0xFFFF; | |
| if (formatSize > 16) | |
| { | |
| int extraSize = ReadInt16(r, isBigEndian); | |
| if (extraSize > 0) r.ReadBytes(extraSize); | |
| } | |
| int dataSize = ReadInt32(r, isBigEndian); | |
| if (dataSize <= 0 || dataSize > r.BaseStream.Length) return false; | |
| byte[] waveData = r.ReadBytes(dataSize); | |
| outPath = Path.Combine(outDir, name + ".wav"); | |
| using (var fs = new FileStream(outPath, FileMode.Create, FileAccess.Write)) | |
| using (var bw = new BinaryWriter(fs)) | |
| { | |
| bw.Write(Encoding.ASCII.GetBytes("RIFF")); | |
| bw.Write(dataSize + 36); | |
| bw.Write(Encoding.ASCII.GetBytes("WAVEfmt ")); | |
| bw.Write(16); | |
| bw.Write((short)1); | |
| bw.Write((short)channels); | |
| bw.Write(samplesPerSec); | |
| bw.Write(avgBytesPerSec); | |
| bw.Write((short)blockAlign); | |
| bw.Write((short)bitsPerSample); | |
| bw.Write(Encoding.ASCII.GetBytes("data")); | |
| bw.Write(dataSize); | |
| bw.Write(waveData); | |
| } | |
| return true; | |
| } | |
| if (primaryType.IndexOf("SpriteFontReader", StringComparison.OrdinalIgnoreCase) >= 0 || | |
| primaryType.IndexOf("DynamicSpriteFontReader", StringComparison.OrdinalIgnoreCase) >= 0) | |
| { | |
| return CreateFontFallback(Path.Combine(outDir, name + ".xnb"), outDir, out outPath); | |
| } | |
| // НЕ Font, НЕ Texture, НЕ Sound, НЕ Effect → пропускаем как бинарник | |
| // Возвращаем false, чтобы вызывающий код скопировал файл как есть | |
| outPath = null; | |
| return false; | |
| } | |
| private static short ReadInt16(BinaryReader r, bool isBE) | |
| { | |
| if (!isBE) return r.ReadInt16(); | |
| byte[] b = r.ReadBytes(2); | |
| return (short)((b[0] << 8) | b[1]); | |
| } | |
| private static int ReadInt32(BinaryReader r, bool isBE) | |
| { | |
| if (!isBE) return r.ReadInt32(); | |
| byte[] b = r.ReadBytes(4); | |
| return (b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3]; | |
| } | |
| } | |
| public enum Endianness { Little, Big } | |
| public class EndianReader : BinaryReader | |
| { | |
| private readonly Endianness _endian; | |
| public EndianReader(Stream input) : this(input, Endianness.Little) { } | |
| public EndianReader(Stream input, Endianness endian) : base(input, Encoding.UTF8) | |
| { | |
| _endian = endian; | |
| } | |
| public override short ReadInt16() { return ReadInt16(_endian); } | |
| public short ReadInt16(Endianness endian) | |
| { | |
| byte[] b = EndianBytes(2, endian); | |
| return BitConverter.ToInt16(b, 0); | |
| } | |
| public override ushort ReadUInt16() { return ReadUInt16(_endian); } | |
| public ushort ReadUInt16(Endianness endian) | |
| { | |
| byte[] b = EndianBytes(2, endian); | |
| return BitConverter.ToUInt16(b, 0); | |
| } | |
| public override int ReadInt32() { return ReadInt32(_endian); } | |
| public int ReadInt32(Endianness endian) | |
| { | |
| byte[] b = EndianBytes(4, endian); | |
| return BitConverter.ToInt32(b, 0); | |
| } | |
| public override uint ReadUInt32() { return ReadUInt32(_endian); } | |
| public uint ReadUInt32(Endianness endian) | |
| { | |
| byte[] b = EndianBytes(4, endian); | |
| return BitConverter.ToUInt32(b, 0); | |
| } | |
| private byte[] EndianBytes(int count, Endianness endian) | |
| { | |
| byte[] bytes = ReadBytes(count); | |
| if ((endian == Endianness.Little && !BitConverter.IsLittleEndian) || | |
| (endian == Endianness.Big && BitConverter.IsLittleEndian)) | |
| { | |
| Array.Reverse(bytes); | |
| } | |
| return bytes; | |
| } | |
| } | |
| public static class XsbReader | |
| { | |
| public static string[] ExtractCueNames(string xsbPath) | |
| { | |
| try | |
| { | |
| using (var fs = new FileStream(xsbPath, FileMode.Open, FileAccess.Read, FileShare.Read)) | |
| using (var reader = new EndianReader(fs)) | |
| { | |
| byte[] sig = reader.ReadBytes(4); | |
| if (sig.Length < 4) return null; | |
| Endianness endian = Endianness.Little; | |
| if (sig[0] == 'K' && sig[1] == 'B' && sig[2] == 'D' && sig[3] == 'S') | |
| { | |
| endian = Endianness.Big; | |
| } | |
| else if (!(sig[0] == 'S' && sig[1] == 'D' && sig[2] == 'B' && sig[3] == 'K')) | |
| { | |
| return null; | |
| } | |
| reader.ReadUInt16(endian); // Version | |
| reader.ReadUInt16(endian); // Header Version | |
| reader.ReadUInt16(endian); // CRC | |
| reader.ReadUInt32(endian); // LowBuild | |
| reader.ReadUInt32(endian); // HighBuild | |
| reader.ReadByte(); // Platform ID | |
| ushort cueSet1Count = reader.ReadUInt16(endian); | |
| ushort cueSet2Count = reader.ReadUInt16(endian); | |
| reader.ReadUInt16(endian); | |
| reader.ReadUInt16(endian); | |
| reader.ReadByte(); | |
| reader.ReadUInt16(endian); | |
| uint cueNamesLen = reader.ReadUInt32(endian); | |
| // Ровно 8 полей Int32 (32 байта) до CueNamesTableOffset | |
| reader.BaseStream.Seek(32, SeekOrigin.Current); | |
| int cueNamesTableOffset = reader.ReadInt32(endian); | |
| if (cueNamesLen == 0 || cueNamesTableOffset <= 0 || cueNamesTableOffset >= fs.Length) | |
| return null; | |
| int totalCues = cueSet1Count + cueSet2Count; | |
| if (totalCues <= 0 || totalCues > 8192) return null; | |
| int[] nameOffsets = new int[totalCues]; | |
| reader.BaseStream.Seek(cueNamesTableOffset, SeekOrigin.Begin); | |
| for (int i = 0; i < totalCues; i++) | |
| { | |
| nameOffsets[i] = reader.ReadInt32(endian); | |
| reader.ReadInt16(endian); | |
| } | |
| char[] invalidChars = Path.GetInvalidFileNameChars(); | |
| string[] names = new string[totalCues]; | |
| for (int i = 0; i < totalCues; i++) | |
| { | |
| if (nameOffsets[i] <= 0 || nameOffsets[i] >= fs.Length) continue; | |
| reader.BaseStream.Seek(nameOffsets[i], SeekOrigin.Begin); | |
| var sb = new StringBuilder(); | |
| while (reader.BaseStream.Position < reader.BaseStream.Length) | |
| { | |
| char c = (char)reader.ReadByte(); | |
| if (c == '\0') break; | |
| if (Array.IndexOf(invalidChars, c) < 0) sb.Append(c); | |
| } | |
| names[i] = sb.ToString().Trim(); | |
| } | |
| return names; | |
| } | |
| } | |
| catch | |
| { | |
| return null; | |
| } | |
| } | |
| } | |
| [ComImport] | |
| [Guid("8BA5FB08-5195-40E2-AC58-0D989C3A0102")] | |
| [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] | |
| internal interface ID3DBlob | |
| { | |
| [PreserveSig] | |
| IntPtr GetBufferPointer(); | |
| [PreserveSig] | |
| IntPtr GetBufferSize(); | |
| } | |
| internal static class EffectExtractor | |
| { | |
| [DllImport("d3dcompiler_47.dll", EntryPoint = "D3DDisassemble", CharSet = CharSet.Ansi)] | |
| private static extern int D3DDisassemble47(IntPtr pSrcData, IntPtr srcDataSize, uint flags, string szComments, out ID3DBlob ppDisassembly); | |
| [DllImport("d3dcompiler_43.dll", EntryPoint = "D3DDisassemble", CharSet = CharSet.Ansi)] | |
| private static extern int D3DDisassemble43(IntPtr pSrcData, IntPtr srcDataSize, uint flags, string szComments, out ID3DBlob ppDisassembly); | |
| public static bool TryUnpackEffect(string xnbPath, string outputDir, out string generatedFxFile) | |
| { | |
| generatedFxFile = null; | |
| try | |
| { | |
| byte[] bytecode = null; | |
| using (var fs = new FileStream(xnbPath, FileMode.Open, FileAccess.Read, FileShare.Read)) | |
| using (var r = new BinaryReader(fs)) | |
| { | |
| if (fs.Length < 10 || !r.ReadAndCompareString("XNB")) return false; | |
| char platform = (char)r.ReadByte(); | |
| bool isBigEndian = (platform == 'x'); | |
| int ver = r.ReadByte(); | |
| if (ver != 5) return false; | |
| byte flags = r.ReadByte(); | |
| bool isCompressed = (flags & 0x80) != 0; | |
| int fileSize = isBigEndian ? ReadInt32BE(r) : r.ReadInt32(); | |
| int decompSize = isCompressed ? (isBigEndian ? ReadInt32BE(r) : r.ReadInt32()) : fileSize; | |
| if (decompSize <= 0 || decompSize > 0x10000000) return false; | |
| Stream ms = null; | |
| if (isCompressed) | |
| { | |
| var mem = new MemoryStream(decompSize); | |
| var lzx = new LzxDecoder(); | |
| lzx.Decompress(r, fileSize - 14, mem, decompSize); | |
| mem.Position = 0; | |
| ms = mem; | |
| } | |
| else | |
| { | |
| ms = new MemoryStream(r.ReadBytes(fileSize - 10)); | |
| } | |
| using (ms) | |
| using (var br = new BinaryReader(ms)) | |
| { | |
| int typeCount = br.Read7BitEncodedInt(); | |
| if (typeCount <= 0 || typeCount > 64) return false; | |
| string primary = br.Read7BitEncodedString(); | |
| if (isBigEndian) ReadInt32BE(br); else br.ReadInt32(); | |
| // Строгая проверка: обрабатываем ТОЛЬКО шейдеры | |
| if (primary.IndexOf("EffectReader", StringComparison.OrdinalIgnoreCase) < 0) | |
| return false; | |
| for (int i = 1; i < typeCount; i++) | |
| { | |
| br.Read7BitEncodedString(); | |
| if (isBigEndian) ReadInt32BE(br); else br.ReadInt32(); | |
| } | |
| br.Read7BitEncodedInt(); // shared | |
| br.Read7BitEncodedInt(); // primary index | |
| int codeLen = isBigEndian ? ReadInt32BE(br) : br.ReadInt32(); | |
| if (codeLen > 0 && codeLen <= (ms.Length - ms.Position)) | |
| { | |
| bytecode = br.ReadBytes(codeLen); | |
| } | |
| } | |
| } | |
| if (bytecode == null || bytecode.Length == 0) return false; | |
| string shaderName = Path.GetFileNameWithoutExtension(xnbPath); | |
| generatedFxFile = Path.Combine(outputDir, shaderName + ".fx"); | |
| string disassembledAsm = TryDisassembleBytecode(bytecode); | |
| string hlslSource = GenerateFallbackHlsl(shaderName, disassembledAsm); | |
| File.WriteAllText(generatedFxFile, hlslSource, Encoding.UTF8); | |
| return true; | |
| } | |
| catch | |
| { | |
| return false; | |
| } | |
| } | |
| private static int ReadInt32BE(BinaryReader br) | |
| { | |
| byte[] b = br.ReadBytes(4); | |
| return (b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3]; | |
| } | |
| private static string TryDisassembleBytecode(byte[] bytecode) | |
| { | |
| IntPtr ptr = Marshal.AllocHGlobal(bytecode.Length); | |
| Marshal.Copy(bytecode, 0, ptr, bytecode.Length); | |
| ID3DBlob blob = null; | |
| try | |
| { | |
| int hr = -1; | |
| try { hr = D3DDisassemble47(ptr, (IntPtr)bytecode.Length, 0, null, out blob); } catch { } | |
| if (hr != 0) | |
| { | |
| try { hr = D3DDisassemble43(ptr, (IntPtr)bytecode.Length, 0, null, out blob); } catch { } | |
| } | |
| if (hr == 0 && blob != null) | |
| { | |
| IntPtr buffer = blob.GetBufferPointer(); | |
| if (buffer != IntPtr.Zero) | |
| { | |
| return Marshal.PtrToStringAnsi(buffer); | |
| } | |
| } | |
| } | |
| catch { } | |
| finally | |
| { | |
| Marshal.FreeHGlobal(ptr); | |
| if (blob != null) Marshal.ReleaseComObject(blob); | |
| } | |
| return null; | |
| } | |
| private static string GenerateFallbackHlsl(string shaderName, string disassemblyComments) | |
| { | |
| var sb = new StringBuilder(); | |
| sb.AppendLine("// -------------------------------------------------------------"); | |
| sb.AppendFormat("// Auto-Generated XNA 4.0 Effect: {0}\n", shaderName); | |
| sb.AppendLine("// -------------------------------------------------------------"); | |
| if (!string.IsNullOrEmpty(disassemblyComments)) | |
| { | |
| sb.AppendLine("/* Direct3D Disassembly Preview:"); | |
| sb.AppendLine(disassemblyComments); | |
| sb.AppendLine("*/\n"); | |
| } | |
| sb.AppendLine("sampler2D TextureSampler : register(s0);"); | |
| sb.AppendLine("float4 uColor = float4(1, 1, 1, 1);"); | |
| sb.AppendLine("float2 uScreenResolution = float2(1920, 1080);"); | |
| sb.AppendLine("float uOpacity = 1.0;"); | |
| sb.AppendLine(); | |
| sb.AppendLine("float4 PixelShaderFunction(float2 coords : TEXCOORD0, float4 color : COLOR0) : COLOR0"); | |
| sb.AppendLine("{"); | |
| sb.AppendLine(" float4 baseColor = tex2D(TextureSampler, coords);"); | |
| sb.AppendLine(" return baseColor * color * uColor * uOpacity;"); | |
| sb.AppendLine("}"); | |
| sb.AppendLine(); | |
| sb.AppendLine("technique Technique1"); | |
| sb.AppendLine("{"); | |
| sb.AppendLine(" pass Pass1"); | |
| sb.AppendLine(" {"); | |
| sb.AppendLine(" PixelShader = compile ps_2_0 PixelShaderFunction();"); | |
| sb.AppendLine(" }"); | |
| sb.AppendLine("}"); | |
| return sb.ToString(); | |
| } | |
| } | |
| internal static class XapGenerator | |
| { | |
| public static string GenerateXap(string xapFilePath, string projectName, | |
| string xwbFileName, List<string> waveFilePaths, string contentProjectDir, | |
| string xgsFilePath) | |
| { | |
| var sb = new StringBuilder(); | |
| string xgsRel = string.IsNullOrEmpty(xgsFilePath) | |
| ? "TerrariaMusic.xgs" | |
| : GetRelativePath(Path.GetDirectoryName(xapFilePath), xgsFilePath); | |
| string headerFile = Path.GetFileNameWithoutExtension(xgsRel) + ".h"; | |
| // === HEADER === | |
| sb.AppendLine("Signature = XACT3;"); | |
| sb.AppendLine("Version = 18;"); | |
| sb.AppendLine("Content Version = 46;"); | |
| sb.AppendLine("Release = February 2010;"); | |
| sb.AppendLine(); | |
| // === OPTIONS === | |
| sb.AppendLine("Options"); | |
| sb.AppendLine("{"); | |
| sb.AppendLine(" Verbose Report = 0;"); | |
| sb.AppendLine(" Generate C/C++ Headers = 1;"); | |
| sb.AppendLine("}"); | |
| sb.AppendLine(); | |
| // === GLOBAL SETTINGS === | |
| sb.AppendLine("Global Settings"); | |
| sb.AppendLine("{"); | |
| sb.AppendLine(string.Format(" Xbox File = {0};", xgsRel)); | |
| sb.AppendLine(string.Format(" Windows File = {0};", xgsRel)); | |
| sb.AppendLine(string.Format(" Header File = {0};", headerFile)); | |
| sb.AppendLine(" Exclude Category Names = 0;"); | |
| sb.AppendLine(" Exclude Variable Names = 0;"); | |
| sb.AppendLine(" Last Modified Low = 0;"); | |
| sb.AppendLine(" Last Modified High = 0;"); | |
| sb.AppendLine(); | |
| // Categories | |
| WriteCategory(sb, "Global", 0, null); | |
| WriteCategory(sb, "Default", 0, "Global"); | |
| WriteCategory(sb, "Music", 1, "Global"); | |
| // Variables — ЗНАЧЕНИЯ ДОЛЖНЫ БЫТЬ ТОЧНЫМИ | |
| WriteVariable(sb, "OrientationAngle", 0, 0, 0, 0, 0, 0, "0.000000", "0.000000", "-180.000000", "180.000000"); | |
| WriteVariable(sb, "DopplerPitchScalar", 0, 0, 0, 0, 0, 0, "1.000000", "1.000000", "0.000000", "4.000000"); | |
| WriteVariable(sb, "SpeedOfSound", 0, 1, 0, 0, 0, 0, "343.500000", "343.500000", "0.000000", "1000000.000000"); | |
| WriteVariable(sb, "ReleaseTime", 0, 0, 1, 1, 1, 1, "0.000000", "0.000000", "0.000000", "15000.000000"); | |
| WriteVariable(sb, "AttackTime", 0, 0, 1, 1, 1, 1, "0.000000", "0.000000", "0.000000", "15000.000000"); | |
| WriteVariable(sb, "NumCueInstances", 0, 0, 1, 1, 1, 1, "0.000000", "0.000000", "0.000000", "1024.000000"); | |
| WriteVariable(sb, "Distance", 0, 0, 0, 0, 0, 0, "0.000000", "0.000000", "0.000000", "1000000.000000"); | |
| // Compression Preset | |
| sb.AppendLine(" Compression Preset"); | |
| sb.AppendLine(" {"); | |
| sb.AppendLine(" Name = Compression Preset;"); | |
| sb.AppendLine(" Xbox Format Tag = 357;"); | |
| sb.AppendLine(" Target Sample Rate = 48000;"); | |
| sb.AppendLine(" XMA Quality = 100;"); | |
| sb.AppendLine(" Find Best Quality = 0;"); | |
| sb.AppendLine(" High Freq Cut = 0;"); | |
| sb.AppendLine(" Loop = 0;"); | |
| sb.AppendLine(" PC Format Tag = 2;"); | |
| sb.AppendLine(" Samples Per Block = 128;"); | |
| sb.AppendLine(" }"); | |
| sb.AppendLine("}"); | |
| sb.AppendLine(); | |
| // === WAVE BANK === | |
| string waveBankName = Path.GetFileNameWithoutExtension(xwbFileName); | |
| sb.AppendLine("Wave Bank"); | |
| sb.AppendLine("{"); | |
| sb.AppendLine(string.Format(" Name = {0};", waveBankName)); | |
| sb.AppendLine(string.Format(" Xbox File = Xbox\\{0}.xwb;", waveBankName)); | |
| sb.AppendLine(string.Format(" Windows File = Win\\{0}.xwb;", waveBankName)); | |
| sb.AppendLine(" Xbox Bank Path Edited = 0;"); | |
| sb.AppendLine(" Windows Bank Path Edited = 0;"); | |
| sb.AppendLine(" Seek Tables = 1;"); | |
| sb.AppendLine(" Compression Preset Name = <none>;"); | |
| sb.AppendLine(" Bank Last Revised Low = 0;"); | |
| sb.AppendLine(" Bank Last Revised High = 0;"); | |
| sb.AppendLine(); | |
| for (int i = 0; i < waveFilePaths.Count; i++) | |
| { | |
| string waveName = Path.GetFileNameWithoutExtension(waveFilePaths[i]); | |
| string relWavePath = GetRelativePath(Path.GetDirectoryName(xapFilePath), waveFilePaths[i]); | |
| sb.AppendLine(" Wave"); | |
| sb.AppendLine(" {"); | |
| sb.AppendLine(string.Format(" Name = {0};", waveName)); | |
| sb.AppendLine(string.Format(" File = {0};", relWavePath)); | |
| sb.AppendLine(" Build Settings Last Modified Low = 0;"); | |
| sb.AppendLine(" Build Settings Last Modified High = 0;"); | |
| sb.AppendLine(); // ПУСТАЯ СТРОКА перед Cache! | |
| sb.AppendLine(" Cache"); | |
| sb.AppendLine(" {"); | |
| sb.AppendLine(" Format Tag = 0;"); | |
| sb.AppendLine(" Channels = 2;"); | |
| sb.AppendLine(" Sampling Rate = 44100;"); | |
| sb.AppendLine(" Bits Per Sample = 1;"); | |
| sb.AppendLine(" Play Region Offset = 78;"); | |
| sb.AppendLine(" Play Region Length = 10000000;"); | |
| sb.AppendLine(" Loop Region Offset = 0;"); | |
| sb.AppendLine(" Loop Region Length = 0;"); | |
| sb.AppendLine(" File Type = 1;"); | |
| sb.AppendLine(" Last Modified Low = 0;"); | |
| sb.AppendLine(" Last Modified High = 0;"); | |
| sb.AppendLine(" }"); | |
| sb.AppendLine(" }"); | |
| sb.AppendLine(); | |
| } | |
| sb.AppendLine("}"); | |
| sb.AppendLine(); | |
| // === SOUND BANK === | |
| sb.AppendLine("Sound Bank"); | |
| sb.AppendLine("{"); | |
| sb.AppendLine(string.Format(" Name = {0};", waveBankName)); | |
| sb.AppendLine(string.Format(" Xbox File = Xbox\\{0}.xsb;", waveBankName)); | |
| sb.AppendLine(string.Format(" Windows File = Win\\{0}.xsb;", waveBankName)); | |
| sb.AppendLine(" Xbox Bank Path Edited = 0;"); | |
| sb.AppendLine(" Windows Bank Path Edited = 0;"); | |
| sb.AppendLine(" Bank Last Modified Low = 0;"); | |
| sb.AppendLine(" Bank Last Modified High = 0;"); | |
| sb.AppendLine(" Header Last Modified High = 0;"); | |
| sb.AppendLine(" Header Last Modified Low = 0;"); | |
| sb.AppendLine(); | |
| for (int i = 0; i < waveFilePaths.Count; i++) | |
| { | |
| string trackName = Path.GetFileNameWithoutExtension(waveFilePaths[i]); | |
| // Sound | |
| sb.AppendLine(" Sound"); | |
| sb.AppendLine(" {"); | |
| sb.AppendLine(string.Format(" Name = {0};", trackName)); | |
| sb.AppendLine(" Volume = -1200;"); | |
| sb.AppendLine(" Pitch = 0;"); | |
| sb.AppendLine(" Priority = 0;"); | |
| sb.AppendLine(); | |
| sb.AppendLine(" Category Entry"); | |
| sb.AppendLine(" {"); | |
| sb.AppendLine(" Name = Music;"); | |
| sb.AppendLine(" }"); | |
| sb.AppendLine(); | |
| sb.AppendLine(" Track"); | |
| sb.AppendLine(" {"); | |
| sb.AppendLine(" Volume = 0;"); | |
| sb.AppendLine(" Use Filter = 0;"); | |
| sb.AppendLine(); | |
| sb.AppendLine(" Play Wave Event"); | |
| sb.AppendLine(" {"); | |
| sb.AppendLine(" Break Loop = 0;"); | |
| sb.AppendLine(" Use Speaker Position = 0;"); | |
| sb.AppendLine(" Use Center Speaker = 1;"); | |
| sb.AppendLine(" New Speaker Position On Loop = 1;"); | |
| sb.AppendLine(" Speaker Position Angle = 0.000000;"); | |
| sb.AppendLine(" Speaker Position Arc = 0.000000;"); | |
| sb.AppendLine(); | |
| sb.AppendLine(" Event Header"); | |
| sb.AppendLine(" {"); | |
| sb.AppendLine(" Timestamp = 0;"); | |
| sb.AppendLine(" Relative = 0;"); | |
| sb.AppendLine(" Random Recurrence = 0;"); | |
| sb.AppendLine(" Random Offset = 0;"); | |
| sb.AppendLine(" }"); | |
| sb.AppendLine(); | |
| sb.AppendLine(" Wave Entry"); | |
| sb.AppendLine(" {"); | |
| sb.AppendLine(string.Format(" Bank Name = {0};", waveBankName)); | |
| sb.AppendLine(" Bank Index = 0;"); | |
| sb.AppendLine(string.Format(" Entry Name = {0};", trackName)); | |
| sb.AppendLine(string.Format(" Entry Index = {0};", i)); | |
| sb.AppendLine(" Weight = 255;"); | |
| sb.AppendLine(" Weight Min = 0;"); | |
| sb.AppendLine(" }"); | |
| sb.AppendLine(" }"); | |
| sb.AppendLine(" }"); | |
| sb.AppendLine(" }"); | |
| sb.AppendLine(); | |
| // Cue | |
| sb.AppendLine(" Cue"); | |
| sb.AppendLine(" {"); | |
| sb.AppendLine(string.Format(" Name = {0};", trackName)); | |
| sb.AppendLine(); | |
| sb.AppendLine(" Variation"); | |
| sb.AppendLine(" {"); | |
| sb.AppendLine(" Variation Type = 3;"); | |
| sb.AppendLine(" Variation Table Type = 1;"); | |
| sb.AppendLine(" New Variation on Loop = 0;"); | |
| sb.AppendLine(" }"); | |
| sb.AppendLine(); | |
| sb.AppendLine(" Sound Entry"); | |
| sb.AppendLine(" {"); | |
| sb.AppendLine(string.Format(" Name = {0};", trackName)); | |
| sb.AppendLine(" Index = 0;"); | |
| sb.AppendLine(" Weight Min = 0;"); | |
| sb.AppendLine(" Weight Max = 255;"); | |
| sb.AppendLine(" }"); | |
| sb.AppendLine(" }"); | |
| sb.AppendLine(); | |
| } | |
| sb.AppendLine("}"); | |
| File.WriteAllText(xapFilePath, sb.ToString(), Encoding.UTF8); | |
| return xapFilePath; | |
| } | |
| private static void WriteCategory(StringBuilder sb, string name, int bgMusic, string parent) | |
| { | |
| sb.AppendLine(" Category"); | |
| sb.AppendLine(" {"); | |
| sb.AppendLine(string.Format(" Name = {0};", name)); | |
| sb.AppendLine(" Public = 1;"); | |
| sb.AppendLine(string.Format(" Background Music = {0};", bgMusic)); | |
| sb.AppendLine(" Volume = 0;"); | |
| sb.AppendLine(); // ПУСТАЯ СТРОКА | |
| sb.AppendLine(" Category Entry"); | |
| sb.AppendLine(" {"); | |
| if (parent != null) | |
| sb.AppendLine(string.Format(" Name = {0};", parent)); | |
| sb.AppendLine(" }"); | |
| sb.AppendLine(); // ПУСТАЯ СТРОКА | |
| sb.AppendLine(" Instance Limit"); | |
| sb.AppendLine(" {"); | |
| sb.AppendLine(" Max Instances = 255;"); | |
| sb.AppendLine(" Behavior = 0;"); | |
| sb.AppendLine(); // ПУСТАЯ СТРОКА | |
| sb.AppendLine(" Crossfade"); | |
| sb.AppendLine(" {"); | |
| sb.AppendLine(" Fade In = 0;"); | |
| sb.AppendLine(" Fade Out = 0;"); | |
| sb.AppendLine(" Crossfade Type = 0;"); | |
| sb.AppendLine(" }"); | |
| sb.AppendLine(" }"); | |
| sb.AppendLine(" }"); | |
| sb.AppendLine(); | |
| } | |
| private static void WriteVariable(StringBuilder sb, string name, | |
| int isPublic, int isGlobal, int isInternal, int isExternal, | |
| int isReadOnly, int isTime, | |
| string value, string initValue, string min, string max) | |
| { | |
| sb.AppendLine(" Variable"); | |
| sb.AppendLine(" {"); | |
| sb.AppendLine(string.Format(" Name = {0};", name)); | |
| sb.AppendLine(string.Format(" Public = {0};", isPublic)); | |
| sb.AppendLine(string.Format(" Global = {0};", isGlobal)); | |
| sb.AppendLine(string.Format(" Internal = {0};", isInternal)); | |
| sb.AppendLine(string.Format(" External = {0};", isExternal)); | |
| sb.AppendLine(" Monitored = 1;"); | |
| sb.AppendLine(" Reserved = 1;"); | |
| sb.AppendLine(string.Format(" Read Only = {0};", isReadOnly)); | |
| sb.AppendLine(string.Format(" Time = {0};", isTime)); | |
| sb.AppendLine(string.Format(" Value = {0};", value)); | |
| sb.AppendLine(string.Format(" Initial Value = {0};", initValue)); | |
| sb.AppendLine(string.Format(" Min = {0};", min)); | |
| sb.AppendLine(string.Format(" Max = {0};", max)); | |
| sb.AppendLine(" }"); | |
| sb.AppendLine(); | |
| } | |
| private static string GetRelativePath(string relativeTo, string path) | |
| { | |
| if (string.IsNullOrEmpty(relativeTo)) return path; | |
| var fromUri = new Uri(relativeTo.EndsWith("\\") ? relativeTo : relativeTo + "\\"); | |
| var toUri = new Uri(path); | |
| Uri relativeUri = fromUri.MakeRelativeUri(toUri); | |
| return Uri.UnescapeDataString(relativeUri.ToString()).Replace('/', '\\'); | |
| } | |
| } | |
| internal static class StfsExtractor | |
| { | |
| private const int MagicPirs = 0x50495253; // "PIRS" | |
| private const int MagicLive = 0x4C495645; // "LIVE" | |
| private const int MagicCon = 0x434F4E20; // "CON " | |
| public static bool IsStfsPackage(string filePath) | |
| { | |
| if (!File.Exists(filePath)) return false; | |
| try | |
| { | |
| using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) | |
| using (var br = new BinaryReader(fs)) | |
| { | |
| if (fs.Length < 0xC000) return false; | |
| byte[] sig = br.ReadBytes(4); | |
| int magic = (sig[0] << 24) | (sig[1] << 16) | (sig[2] << 8) | sig[3]; | |
| return magic == MagicPirs || magic == MagicLive || magic == MagicCon; | |
| } | |
| } | |
| catch { return false; } | |
| } | |
| public static string GetPackageTitle(string packagePath) | |
| { | |
| try | |
| { | |
| using (var fs = new FileStream(packagePath, FileMode.Open, FileAccess.Read, FileShare.Read)) | |
| using (var br = new BinaryReader(fs)) | |
| { | |
| // Смещение 0x410 содержит название игры в UTF-16 Big-Endian | |
| br.BaseStream.Seek(0x410, SeekOrigin.Begin); | |
| byte[] data = br.ReadBytes(256); | |
| var sb = new StringBuilder(); | |
| for (int i = 0; i < data.Length; i += 2) | |
| { | |
| ushort ch = (ushort)((data[i] << 8) | data[i + 1]); | |
| if (ch == 0) break; | |
| if (!char.IsControl((char)ch)) sb.Append((char)ch); | |
| } | |
| string title = sb.ToString().Trim(); | |
| if (!string.IsNullOrEmpty(title)) | |
| { | |
| foreach (char c in Path.GetInvalidFileNameChars()) title = title.Replace(c, '_'); | |
| return title.Trim(); | |
| } | |
| } | |
| } | |
| catch { } | |
| return null; | |
| } | |
| public static string ExtractPackage(string packagePath, string outputFolder) | |
| { | |
| Logger.Log(LogLevel.Info, string.Format("Extracting STFS package: {0}", Path.GetFileName(packagePath))); | |
| using (var fs = new FileStream(packagePath, FileMode.Open, FileAccess.Read, FileShare.Read)) | |
| using (var br = new BinaryReader(fs)) | |
| { | |
| byte[] sig = br.ReadBytes(4); | |
| int magic = (sig[0] << 24) | (sig[1] << 16) | (sig[2] << 8) | sig[3]; | |
| br.BaseStream.Seek(49200L, SeekOrigin.Begin); | |
| byte[] b4 = br.ReadBytes(4); | |
| int check = (b4[0] << 24) | (b4[1] << 16) | (b4[2] << 8) | b4[3]; | |
| long pirsOffset = 8192L; | |
| long pirsStart = 53248L; | |
| if (magic == MagicCon) | |
| { | |
| pirsOffset = 8192L; | |
| pirsStart = 49152L; | |
| } | |
| else if (check == 65535) | |
| { | |
| pirsOffset = 4096L; | |
| pirsStart = 45056L + pirsOffset; | |
| } | |
| var entries = new List<StfsEntry>(); | |
| int index = 0; | |
| while (true) | |
| { | |
| long entryPos = pirsStart + (index * 64); | |
| if (entryPos + 64 > fs.Length) break; | |
| br.BaseStream.Seek(entryPos, SeekOrigin.Begin); | |
| byte[] nameBytes = br.ReadBytes(38); | |
| string name = Encoding.ASCII.GetString(nameBytes).Trim('\0', ' '); | |
| if (string.IsNullOrEmpty(name)) break; | |
| br.BaseStream.Seek(entryPos + 46, SeekOrigin.Begin); | |
| int cluster = br.ReadInt32() >> 8; | |
| byte[] pBytes = br.ReadBytes(2); | |
| ushort parent = (ushort)((pBytes[0] << 8) | pBytes[1]); | |
| byte[] sBytes = br.ReadBytes(4); | |
| int size = (sBytes[0] << 24) | (sBytes[1] << 16) | (sBytes[2] << 8) | sBytes[3]; | |
| entries.Add(new StfsEntry | |
| { | |
| Name = name, | |
| Cluster = cluster, | |
| Parent = parent, | |
| Size = size, | |
| IsFolder = (cluster == 0 && size == 0) | |
| }); | |
| index++; | |
| } | |
| Logger.Log(LogLevel.Info, string.Format("Package contains {0} items. Unpacking...", entries.Count)); | |
| for (int i = 0; i < entries.Count; i++) | |
| { | |
| var entry = entries[i]; | |
| string fullPath = ResolveEntryPath(entries, i, outputFolder); | |
| if (entry.IsFolder) | |
| { | |
| if (!Directory.Exists(fullPath)) Directory.CreateDirectory(fullPath); | |
| } | |
| else | |
| { | |
| string dir = Path.GetDirectoryName(fullPath); | |
| if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); | |
| ExtractFileClusters(br, entry.Cluster, entry.Size, pirsStart, pirsOffset, fullPath); | |
| } | |
| Tui.DrawProgress(i + 1, entries.Count, entry.Name); | |
| } | |
| } | |
| Logger.Log(LogLevel.Success, "STFS package extraction complete."); | |
| return outputFolder; | |
| } | |
| private struct StfsEntry | |
| { | |
| public string Name; | |
| public int Cluster; | |
| public ushort Parent; | |
| public int Size; | |
| public bool IsFolder; | |
| } | |
| private static string ResolveEntryPath(List<StfsEntry> entries, int index, string baseDir) | |
| { | |
| var entry = entries[index]; | |
| string path = entry.Name; | |
| ushort parent = entry.Parent; | |
| while (parent != 0xFFFF && parent < entries.Count) | |
| { | |
| path = Path.Combine(entries[parent].Name, path); | |
| parent = entries[parent].Parent; | |
| } | |
| return Path.Combine(baseDir, path); | |
| } | |
| private static void ExtractFileClusters(BinaryReader br, int cluster, int size, long pirsStart, long pirsOffset, string outFile) | |
| { | |
| using (var outFs = new FileStream(outFile, FileMode.Create, FileAccess.Write, FileShare.None)) | |
| using (var bw = new BinaryWriter(outFs)) | |
| { | |
| long blocks = size >> 12; | |
| long remainder = size & 0xFFF; | |
| for (long b = 0; b < blocks; b++) | |
| { | |
| long offset = GetClusterOffset(cluster + b, pirsStart, pirsOffset); | |
| br.BaseStream.Seek(offset, SeekOrigin.Begin); | |
| bw.Write(br.ReadBytes(4096)); | |
| } | |
| if (remainder > 0) | |
| { | |
| long offset = GetClusterOffset(cluster + blocks, pirsStart, pirsOffset); | |
| br.BaseStream.Seek(offset, SeekOrigin.Begin); | |
| bw.Write(br.ReadBytes((int)remainder)); | |
| } | |
| } | |
| } | |
| private static long GetClusterOffset(long cluster, long pirsStart, long pirsOffset) | |
| { | |
| long offset = pirsStart + (cluster * 4096); | |
| long n2 = cluster / 170; | |
| long n3 = n2 / 170; | |
| if (n2 > 0) offset += (n2 + 1) * pirsOffset; | |
| if (n3 > 0) offset += (n3 + 1) * pirsOffset; | |
| return offset; | |
| } | |
| } | |
| internal static class XexAssemblyScanner | |
| { | |
| private static readonly byte[] RetailKey2 = new byte[16] | |
| { | |
| 0x20, 0xB1, 0x85, 0xA5, 0x9D, 0x28, 0xFD, 0xC3, 0x40, 0x58, 0x3F, 0xBB, 0x08, 0x96, 0xBF, 0x91 | |
| }; | |
| private static readonly byte[] DevkitKey = new byte[16] | |
| { | |
| 0xA8, 0xB0, 0x05, 0x12, 0xED, 0xE3, 0x63, 0x8D, 0xC6, 0x58, 0xB3, 0x10, 0x1F, 0x9F, 0x50, 0xD1 | |
| }; | |
| private static readonly string[] StubBlacklist = new string[] | |
| { | |
| "stubbin", "defaulthost", "runtimehost", "hostloader", "default" | |
| }; | |
| public static string LocateOrExtractGameAssembly(string rootDir, ref string projectName, bool isNameLocked, string customAsmPath) | |
| { | |
| UnpackRuntimeLibraries(rootDir); | |
| // 1. Проверка пользовательского пути | |
| if (!string.IsNullOrEmpty(customAsmPath)) | |
| { | |
| string target = customAsmPath; | |
| if (!Path.IsPathRooted(target)) | |
| { | |
| string candidate = Path.Combine(rootDir, target); | |
| if (File.Exists(candidate)) target = candidate; | |
| else | |
| { | |
| string[] found = Directory.GetFiles(rootDir, target, SearchOption.AllDirectories); | |
| if (found.Length > 0) target = found[0]; | |
| } | |
| } | |
| if (File.Exists(target)) | |
| { | |
| string extracted = ProcessTargetFile(target, rootDir); | |
| if (!string.IsNullOrEmpty(extracted)) | |
| { | |
| if (!isNameLocked) projectName = GetCleanProjectName(extracted); | |
| return extracted; | |
| } | |
| } | |
| } | |
| // 2. Поиск уже распакованного файла ASSEMBLY | |
| string[] rawAssemblies = Directory.GetFiles(rootDir, "*ASSEMBLY*", SearchOption.AllDirectories); | |
| for (int i = 0; i < rawAssemblies.Length; i++) | |
| { | |
| if (IsDotNetAssembly(rawAssemblies[i])) | |
| { | |
| Logger.Log(LogLevel.Success, string.Format("Found uncompressed ASSEMBLY: {0}", Path.GetFileName(rawAssemblies[i]))); | |
| string outExe = Path.Combine(rootDir, projectName + ".exe"); | |
| File.Copy(rawAssemblies[i], outExe, true); | |
| return outExe; | |
| } | |
| } | |
| // 3. Целевой поиск game.exe.xex | |
| string[] gameXexs = Directory.GetFiles(rootDir, "*game*.xex", SearchOption.AllDirectories); | |
| for (int i = 0; i < gameXexs.Length; i++) | |
| { | |
| string res = ExtractAssemblyFromXex(gameXexs[i], rootDir); | |
| if (!string.IsNullOrEmpty(res)) | |
| { | |
| return res; | |
| } | |
| } | |
| // 4. Поиск среди остальных обычных .exe | |
| string[] exes = Directory.GetFiles(rootDir, "*.exe", SearchOption.AllDirectories); | |
| for (int i = 0; i < exes.Length; i++) | |
| { | |
| string name = Path.GetFileNameWithoutExtension(exes[i]).ToLowerInvariant(); | |
| if (IsStubName(name)) continue; | |
| if (IsDotNetAssembly(exes[i])) | |
| { | |
| if (!isNameLocked) projectName = GetCleanProjectName(exes[i]); | |
| return exes[i]; | |
| } | |
| } | |
| // 5. Поиск по всем оставшимся .xex | |
| string[] allXex = Directory.GetFiles(rootDir, "*.xex", SearchOption.AllDirectories); | |
| for (int i = 0; i < allXex.Length; i++) | |
| { | |
| string name = Path.GetFileNameWithoutExtension(allXex[i]).ToLowerInvariant(); | |
| if (IsStubName(name) || name.EndsWith(".dll")) continue; | |
| string res = ExtractAssemblyFromXex(allXex[i], rootDir); | |
| if (!string.IsNullOrEmpty(res)) return res; | |
| } | |
| return null; | |
| } | |
| private static bool IsStubName(string name) | |
| { | |
| if (string.IsNullOrEmpty(name)) return true; | |
| string lower = name.ToLowerInvariant(); | |
| for (int i = 0; i < StubBlacklist.Length; i++) | |
| { | |
| if (lower == StubBlacklist[i] || lower.StartsWith("stubbin")) return true; | |
| } | |
| return false; | |
| } | |
| private static string GetCleanProjectName(string filePath) | |
| { | |
| string name = Path.GetFileNameWithoutExtension(filePath); | |
| if (name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) | |
| name = Path.GetFileNameWithoutExtension(name); | |
| return name; | |
| } | |
| private static string ProcessTargetFile(string file, string rootDir) | |
| { | |
| if (Path.GetExtension(file).Equals(".xex", StringComparison.OrdinalIgnoreCase)) | |
| { | |
| return ExtractAssemblyFromXex(file, rootDir); | |
| } | |
| if (IsDotNetAssembly(file)) | |
| { | |
| return file; | |
| } | |
| byte[] raw = File.ReadAllBytes(file); | |
| byte[] carved = CarveManagedPe(raw); | |
| if (carved != null) | |
| { | |
| string outPath = Path.Combine(rootDir, Path.GetFileNameWithoutExtension(file) + ".exe"); | |
| File.WriteAllBytes(outPath, carved); | |
| return outPath; | |
| } | |
| return null; | |
| } | |
| private static string FindXeXTool(string searchDir) | |
| { | |
| var candidates = new List<string> | |
| { | |
| Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "xextool.exe"), | |
| Path.Combine(Environment.CurrentDirectory, "xextool.exe"), | |
| Path.Combine(searchDir, "xextool.exe") | |
| }; | |
| string parent = Path.GetDirectoryName(searchDir); | |
| if (!string.IsNullOrEmpty(parent)) | |
| candidates.Add(Path.Combine(parent, "xextool.exe")); | |
| for (int i = 0; i < candidates.Count; i++) | |
| { | |
| if (File.Exists(candidates[i])) return candidates[i]; | |
| } | |
| string pathEnv = Environment.GetEnvironmentVariable("PATH"); | |
| if (!string.IsNullOrEmpty(pathEnv)) | |
| { | |
| string[] dirs = pathEnv.Split(Path.PathSeparator); | |
| for (int i = 0; i < dirs.Length; i++) | |
| { | |
| try | |
| { | |
| string p = Path.Combine(dirs[i].Trim(), "xextool.exe"); | |
| if (File.Exists(p)) return p; | |
| } | |
| catch { } | |
| } | |
| } | |
| return null; | |
| } | |
| private static byte[] DecompressLzxPayload(Stream stream, int dataOffset, int firstBlockSize, byte[] sessionKey, int uncompressedSize) | |
| { | |
| try | |
| { | |
| using (var decMs = new MemoryStream(uncompressedSize)) | |
| { | |
| stream.Seek(dataOffset, SeekOrigin.Begin); | |
| byte[] encBytes = new byte[firstBlockSize]; | |
| stream.Read(encBytes, 0, firstBlockSize); | |
| byte[] decBlock = (sessionKey != null) ? DecryptAesPayload(encBytes, sessionKey) : encBytes; | |
| if (decBlock == null || decBlock.Length < 24) return null; | |
| var lzx = new LzxDecoder(); | |
| using (var brBlock = new BinaryReader(new MemoryStream(decBlock))) | |
| { | |
| int nextBlockSize = ReadInt32BE(brBlock); | |
| byte[] nextHash = brBlock.ReadBytes(20); | |
| while (brBlock.BaseStream.Position + 2 <= brBlock.BaseStream.Length && decMs.Length < uncompressedSize) | |
| { | |
| ushort chunkSize = ReadUInt16BE(brBlock); | |
| if (chunkSize == 0 || brBlock.BaseStream.Position + chunkSize > brBlock.BaseStream.Length) break; | |
| int toDecompress = Math.Min(32768, uncompressedSize - (int)decMs.Length); | |
| lzx.Decompress(brBlock, chunkSize, decMs, toDecompress); | |
| } | |
| } | |
| if (decMs.Length > 0) return decMs.ToArray(); | |
| } | |
| } | |
| catch { } | |
| return null; | |
| } | |
| private static byte[] DecryptAesKey(byte[] encryptedKey, byte[] masterKey) | |
| { | |
| try | |
| { | |
| using (var aes = new RijndaelManaged { Key = masterKey, Mode = CipherMode.CBC, Padding = PaddingMode.None, IV = new byte[16] }) | |
| using (var dec = aes.CreateDecryptor()) | |
| { | |
| return dec.TransformFinalBlock(encryptedKey, 0, 16); | |
| } | |
| } | |
| catch { return null; } | |
| } | |
| private static byte[] DecryptAesPayload(byte[] payload, byte[] key) | |
| { | |
| try | |
| { | |
| using (var aes = new RijndaelManaged { Key = key, Mode = CipherMode.CBC, Padding = PaddingMode.None, IV = new byte[16] }) | |
| using (var dec = aes.CreateDecryptor()) | |
| { | |
| return dec.TransformFinalBlock(payload, 0, payload.Length); | |
| } | |
| } | |
| catch { return null; } | |
| } | |
| private static void UnpackRuntimeLibraries(string rootDir) | |
| { | |
| string runtimeDir = Path.Combine(rootDir, "Runtime"); | |
| if (!Directory.Exists(runtimeDir)) return; | |
| string[] xexLibs = Directory.GetFiles(runtimeDir, "*.dll.xex", SearchOption.AllDirectories); | |
| if (xexLibs.Length == 0) return; | |
| Logger.Log(LogLevel.Info, string.Format("Unpacking {0} Xbox 360 runtime dependencies...", xexLibs.Length)); | |
| var mxfMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) | |
| { | |
| { "MXF.dll", "Microsoft.Xna.Framework.dll" }, | |
| { "MXF.Game.dll", "Microsoft.Xna.Framework.Game.dll" }, | |
| { "MXF.Graphics.dll", "Microsoft.Xna.Framework.Graphics.dll" }, | |
| { "MXF.GamerServices.dll", "Microsoft.Xna.Framework.GamerServices.dll" }, | |
| { "MXF.Xact.dll", "Microsoft.Xna.Framework.Xact.dll" }, | |
| { "MXF.Net.dll", "Microsoft.Xna.Framework.Net.dll" }, | |
| { "MXF.Storage.dll", "Microsoft.Xna.Framework.Storage.dll" }, | |
| { "MXF.Video.dll", "Microsoft.Xna.Framework.Video.dll" }, | |
| { "MXF.Avatar.dll", "Microsoft.Xna.Framework.Avatar.dll" }, | |
| { "MXF.Xdk.dll", "Microsoft.Xna.Framework.Xdk.dll" } | |
| }; | |
| for (int i = 0; i < xexLibs.Length; i++) | |
| { | |
| string xexLib = xexLibs[i]; | |
| string baseDll = xexLib.Substring(0, xexLib.Length - 4); | |
| if (!File.Exists(baseDll)) | |
| { | |
| byte[] data = DecompressXexAndExtractAssembly(xexLib); | |
| if (data != null) | |
| { | |
| byte[] pe = CarveManagedPe(data); | |
| File.WriteAllBytes(baseDll, pe ?? data); | |
| } | |
| } | |
| string fileName = Path.GetFileName(baseDll); | |
| if (File.Exists(baseDll) && mxfMap.ContainsKey(fileName)) | |
| { | |
| string standardDllPath = Path.Combine(Path.GetDirectoryName(baseDll), mxfMap[fileName]); | |
| if (!File.Exists(standardDllPath)) | |
| { | |
| File.Copy(baseDll, standardDllPath, true); | |
| } | |
| } | |
| } | |
| } | |
| public static string ExtractAssemblyFromXex(string xexPath, string rootDir) | |
| { | |
| string baseDir = Path.GetDirectoryName(xexPath); | |
| // 1. Попытка через внешнюю утилиту xextool.exe (если доступна) | |
| string xextool = FindXeXTool(baseDir); | |
| if (!string.IsNullOrEmpty(xextool)) | |
| { | |
| try | |
| { | |
| var psiRes = new ProcessStartInfo | |
| { | |
| FileName = xextool, | |
| Arguments = string.Format("-r a \"{0}\"", xexPath), | |
| WorkingDirectory = baseDir, | |
| CreateNoWindow = true, | |
| UseShellExecute = false | |
| }; | |
| using (var p = Process.Start(psiRes)) p.WaitForExit(); | |
| string dumpExe = Path.Combine(baseDir, Path.GetFileNameWithoutExtension(xexPath) + "_unpacked.exe"); | |
| var psiBase = new ProcessStartInfo | |
| { | |
| FileName = xextool, | |
| Arguments = string.Format("-d -u -e \"{0}\" \"{1}\"", dumpExe, xexPath), | |
| WorkingDirectory = baseDir, | |
| CreateNoWindow = true, | |
| UseShellExecute = false | |
| }; | |
| using (var p = Process.Start(psiBase)) p.WaitForExit(); | |
| } | |
| catch { } | |
| string[] files = Directory.GetFiles(baseDir, "*.*", SearchOption.AllDirectories); | |
| for (int i = 0; i < files.Length; i++) | |
| { | |
| string f = files[i]; | |
| if (Path.GetExtension(f).Equals(".xex", StringComparison.OrdinalIgnoreCase)) continue; | |
| if (Path.GetFileName(f).IndexOf("ASSEMBLY", StringComparison.OrdinalIgnoreCase) >= 0 || | |
| f.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) || | |
| f.EndsWith(".mz", StringComparison.OrdinalIgnoreCase)) | |
| { | |
| if (IsDotNetAssembly(f)) | |
| { | |
| string finalPath = Path.Combine(rootDir, "Game.exe"); | |
| File.Copy(f, finalPath, true); | |
| Logger.Log(LogLevel.Success, string.Format("xextool extracted valid assembly: {0}", Path.GetFileName(f))); | |
| return finalPath; | |
| } | |
| } | |
| } | |
| } | |
| // 2. Встроенный декриптор XEX2 (алгоритм XeXtractor) | |
| try | |
| { | |
| byte[] assemblyBytes = DecompressXexAndExtractAssembly(xexPath); | |
| if (assemblyBytes != null && assemblyBytes.Length > 0) | |
| { | |
| string finalExe = Path.Combine(rootDir, "Game.exe"); | |
| File.WriteAllBytes(finalExe, assemblyBytes); | |
| if (IsDotNetAssembly(finalExe)) | |
| { | |
| Logger.Log(LogLevel.Success, string.Format("Extracted valid .NET ASSEMBLY.MZ ({0} KB) -> Game.exe", assemblyBytes.Length / 1024)); | |
| return finalExe; | |
| } | |
| else | |
| { | |
| // Попытка карвинга PE, если внутри ресурса был контейнер | |
| byte[] carved = CarveManagedPe(assemblyBytes); | |
| if (carved != null) | |
| { | |
| File.WriteAllBytes(finalExe, carved); | |
| Logger.Log(LogLevel.Success, string.Format("Carved valid .NET PE ({0} KB) -> Game.exe", carved.Length / 1024)); | |
| return finalExe; | |
| } | |
| } | |
| } | |
| } | |
| catch (Exception ex) | |
| { | |
| Logger.Log(LogLevel.Trace, string.Format("XEX decode error: {0}", ex.Message)); | |
| } | |
| return null; | |
| } | |
| private struct RawBlock | |
| { | |
| public int DataSize; | |
| public int ZeroSize; | |
| } | |
| private static byte[] DecompressXexAndExtractAssembly(string xexPath) | |
| { | |
| byte[] xexBytes = File.ReadAllBytes(xexPath); | |
| if (xexBytes.Length < 0x200) return null; | |
| if (xexBytes[0] != 'X' || xexBytes[1] != 'E' || xexBytes[2] != 'X' || xexBytes[3] != '2') | |
| return null; | |
| int dataOffset = ReadInt32BE(xexBytes, 8); | |
| int secInfoOffset = ReadInt32BE(xexBytes, 16); | |
| int optHeaderCount = ReadInt32BE(xexBytes, 20); | |
| if (secInfoOffset <= 0 || secInfoOffset + 380 > xexBytes.Length) return null; | |
| int imageSize = ReadInt32BE(xexBytes, secInfoOffset + 4); | |
| int loadAddress = ReadInt32BE(xexBytes, secInfoOffset + 272); | |
| byte[] encryptedAesKey = new byte[16]; | |
| Array.Copy(xexBytes, secInfoOffset + 336, encryptedAesKey, 0, 16); | |
| int resInfoOffset = 0; | |
| int baseFmtOffset = 0; | |
| int curOpt = 24; | |
| for (int i = 0; i < optHeaderCount && curOpt + 8 <= xexBytes.Length; i++) | |
| { | |
| uint key = ReadUInt32BE(xexBytes, curOpt); | |
| uint val = ReadUInt32BE(xexBytes, curOpt + 4); | |
| if (key == 0x000002FF) resInfoOffset = (int)val; | |
| if (key == 0x000003FF) baseFmtOffset = (int)val; | |
| curOpt += 8; | |
| } | |
| if (baseFmtOffset <= 0 || baseFmtOffset + 8 > xexBytes.Length) return null; | |
| int infoSize = ReadInt32BE(xexBytes, baseFmtOffset); | |
| short encryptionType = ReadInt16BE(xexBytes, baseFmtOffset + 4); | |
| short compressionType = ReadInt16BE(xexBytes, baseFmtOffset + 6); | |
| Logger.Log(LogLevel.Info, string.Format("Inspecting {0} (Enc={1}, Comp={2}, ImageSize={3} KB)...", | |
| Path.GetFileName(xexPath), | |
| encryptionType == 1 ? "Encrypted" : "None", | |
| compressionType == 2 ? "LZX" : (compressionType == 1 ? "Raw" : "Delta"), | |
| imageSize / 1024)); | |
| // Дешифрование сессионного ключа AES | |
| byte[] sessionKey = null; | |
| if (encryptionType == 1) | |
| { | |
| sessionKey = DecryptAesKey(encryptedAesKey, RetailKey2); | |
| if (sessionKey == null) sessionKey = DecryptAesKey(encryptedAesKey, DevkitKey); | |
| if (sessionKey == null) return null; | |
| } | |
| byte[] uncompressedBaseFile = null; | |
| // 1. Raw-формат блоков (DataSize + ZeroSize) | |
| if (compressionType == 1) | |
| { | |
| int blockCount = (infoSize - 8) / 8; | |
| var blocks = new RawBlock[blockCount]; | |
| int bOffset = baseFmtOffset + 8; | |
| for (int b = 0; b < blockCount; b++) | |
| { | |
| blocks[b].DataSize = ReadInt32BE(xexBytes, bOffset); | |
| blocks[b].ZeroSize = ReadInt32BE(xexBytes, bOffset + 4); | |
| bOffset += 8; | |
| } | |
| int rawEncLen = xexBytes.Length - dataOffset; | |
| int cryptBlocks = (rawEncLen / 16) * 16; | |
| byte[] rawEnc = new byte[cryptBlocks]; | |
| Array.Copy(xexBytes, dataOffset, rawEnc, 0, cryptBlocks); | |
| byte[] decryptedPayload = (encryptionType == 1) ? DecryptAesPayload(rawEnc, sessionKey) : rawEnc; | |
| if (decryptedPayload == null) return null; | |
| using (var baseMs = new MemoryStream(imageSize)) | |
| { | |
| int payloadPos = 0; | |
| for (int b = 0; b < blockCount; b++) | |
| { | |
| int dSize = blocks[b].DataSize; | |
| int zSize = blocks[b].ZeroSize; | |
| if (dSize > 0) | |
| { | |
| int take = Math.Min(dSize, decryptedPayload.Length - payloadPos); | |
| if (take > 0) | |
| { | |
| baseMs.Write(decryptedPayload, payloadPos, take); | |
| payloadPos += take; | |
| } | |
| } | |
| if (zSize > 0) | |
| { | |
| byte[] zeros = new byte[zSize]; | |
| baseMs.Write(zeros, 0, zSize); | |
| } | |
| } | |
| baseMs.SetLength(imageSize); | |
| uncompressedBaseFile = baseMs.ToArray(); | |
| } | |
| } | |
| // 2. LZX-сжатый формат | |
| else if (compressionType == 2) | |
| { | |
| int firstBlockSize = ReadInt32BE(xexBytes, baseFmtOffset + 12); | |
| using (var ms = new MemoryStream(xexBytes)) | |
| { | |
| uncompressedBaseFile = DecompressLzxPayload(ms, dataOffset, firstBlockSize, encryptionType == 1 ? sessionKey : null, imageSize); | |
| } | |
| } | |
| if (uncompressedBaseFile == null || uncompressedBaseFile.Length == 0) return null; | |
| // Чтение таблицы ResourceInfo напрямую из массива xexBytes | |
| if (resInfoOffset > 0 && resInfoOffset + 4 < xexBytes.Length) | |
| { | |
| int resTableSize = ReadInt32BE(xexBytes, resInfoOffset); | |
| int resCount = (resTableSize - 4) / 16; | |
| int rPos = resInfoOffset + 4; | |
| for (int r = 0; r < resCount && rPos + 16 <= xexBytes.Length; r++) | |
| { | |
| string resName = Encoding.ASCII.GetString(xexBytes, rPos, 8).Trim('\0', ' '); | |
| int resAddress = ReadInt32BE(xexBytes, rPos + 8); | |
| int resLength = ReadInt32BE(xexBytes, rPos + 12); | |
| rPos += 16; | |
| int offsetInFile = resAddress - loadAddress; | |
| if (offsetInFile >= 0 && offsetInFile + resLength <= uncompressedBaseFile.Length) | |
| { | |
| byte[] resData = new byte[resLength]; | |
| Array.Copy(uncompressedBaseFile, offsetInFile, resData, 0, resLength); | |
| if (resName.IndexOf("ASSEMBLY", StringComparison.OrdinalIgnoreCase) >= 0 || | |
| (resData.Length > 2 && resData[0] == 0x4D && resData[1] == 0x5A)) | |
| { | |
| Logger.Log(LogLevel.Success, string.Format("Found resource '{0}' with valid MZ header ({1} KB).", resName, resLength / 1024)); | |
| return resData; | |
| } | |
| } | |
| } | |
| } | |
| return CarveManagedPe(uncompressedBaseFile); | |
| } | |
| private static int ReadInt32BE(byte[] data, int offset) | |
| { | |
| return (data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3]; | |
| } | |
| private static uint ReadUInt32BE(byte[] data, int offset) | |
| { | |
| return (uint)((data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3]); | |
| } | |
| private static short ReadInt16BE(byte[] data, int offset) | |
| { | |
| return (short)((data[offset] << 8) | data[offset + 1]); | |
| } | |
| public static byte[] CarveManagedPe(byte[] data) | |
| { | |
| if (data == null || data.Length < 0x200) return null; | |
| for (int i = 0; i < data.Length - 0x200; i++) | |
| { | |
| if (data[i] == 0x4D && data[i + 1] == 0x5A) // MZ | |
| { | |
| if (i + 0x3C + 4 > data.Length) continue; | |
| int e_lfanew = BitConverter.ToInt32(data, i + 0x3C); | |
| if (e_lfanew <= 0 || e_lfanew > 0x1000 || i + e_lfanew + 0x100 > data.Length) continue; | |
| if (data[i + e_lfanew] == 0x50 && data[i + e_lfanew + 1] == 0x45 && | |
| data[i + e_lfanew + 2] == 0 && data[i + e_lfanew + 3] == 0) | |
| { | |
| ushort numSections = BitConverter.ToUInt16(data, i + e_lfanew + 6); | |
| ushort optHeaderSize = BitConverter.ToUInt16(data, i + e_lfanew + 20); | |
| ushort magic = BitConverter.ToUInt16(data, i + e_lfanew + 24); | |
| if (magic != 0x10B && magic != 0x20B) continue; | |
| int clrDirOffset = (magic == 0x20B) | |
| ? (i + e_lfanew + 24 + 224) | |
| : (i + e_lfanew + 24 + 208); | |
| if (clrDirOffset + 8 > data.Length) continue; | |
| uint clrRva = BitConverter.ToUInt32(data, clrDirOffset); | |
| if (clrRva == 0) continue; | |
| int sectionHeadersStart = i + e_lfanew + 24 + optHeaderSize; | |
| int maxRawOffset = 0; | |
| for (int s = 0; s < numSections; s++) | |
| { | |
| int secHeader = sectionHeadersStart + (s * 40); | |
| if (secHeader + 40 > data.Length) break; | |
| int rawSize = BitConverter.ToInt32(data, secHeader + 16); | |
| int rawPtr = BitConverter.ToInt32(data, secHeader + 20); | |
| if (rawPtr + rawSize > maxRawOffset) | |
| { | |
| maxRawOffset = rawPtr + rawSize; | |
| } | |
| } | |
| if (maxRawOffset > 0 && i + maxRawOffset <= data.Length) | |
| { | |
| byte[] pe = new byte[maxRawOffset]; | |
| Array.Copy(data, i, pe, 0, maxRawOffset); | |
| return pe; | |
| } | |
| } | |
| } | |
| } | |
| return null; | |
| } | |
| public static bool IsDotNetAssembly(string path) | |
| { | |
| try | |
| { | |
| using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) | |
| using (var br = new BinaryReader(fs)) | |
| { | |
| if (fs.Length < 0x200) return false; | |
| if (br.ReadUInt16() != 0x5A4D) return false; | |
| fs.Seek(0x3C, SeekOrigin.Begin); | |
| int peOffset = br.ReadInt32(); | |
| if (peOffset <= 0 || peOffset + 0x100 > fs.Length) return false; | |
| fs.Seek(peOffset, SeekOrigin.Begin); | |
| if (br.ReadUInt32() != 0x00004550) return false; | |
| fs.Seek(peOffset + 24, SeekOrigin.Begin); | |
| ushort magic = br.ReadUInt16(); | |
| int clrDirOffset = (magic == 0x20B) ? (peOffset + 24 + 224) : (peOffset + 24 + 208); | |
| if (clrDirOffset + 8 > fs.Length) return false; | |
| fs.Seek(clrDirOffset, SeekOrigin.Begin); | |
| uint clrRva = br.ReadUInt32(); | |
| return clrRva != 0; | |
| } | |
| } | |
| catch { return false; } | |
| } | |
| private static int ReadInt32BE(BinaryReader br) | |
| { | |
| byte[] b = br.ReadBytes(4); | |
| return (b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3]; | |
| } | |
| private static ushort ReadUInt16BE(BinaryReader br) | |
| { | |
| byte[] b = br.ReadBytes(2); | |
| return (ushort)((b[0] << 8) | b[1]); | |
| } | |
| private static uint ReadUInt32BE(BinaryReader br) | |
| { | |
| byte[] b = br.ReadBytes(4); | |
| return (uint)((b[0] << 24) | (b[1] << 16) | (b[2] << 8) | b[3]); | |
| } | |
| private static short ReadInt16BE(BinaryReader br) | |
| { | |
| byte[] b = br.ReadBytes(2); | |
| return (short)((b[0] << 8) | b[1]); | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment