Skip to content

Instantly share code, notes, and snippets.

@DartPower
Last active September 17, 2026 17:28
Show Gist options
  • Select an option

  • Save DartPower/ce0f01855dbef84198fbd867ba89e09a to your computer and use it in GitHub Desktop.

Select an option

Save DartPower/ce0f01855dbef84198fbd867ba89e09a to your computer and use it in GitHub Desktop.
XNA Ressurector - Project restore
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>&#32;</Start>
<End>&#126;</End>
</CharacterRegion>
<CharacterRegion>
<Start>&#1040;</Start>
<End>&#1103;</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>&#32;</Start><End>&#126;</End></CharacterRegion>
<CharacterRegion><Start>&#1040;</Start><End>&#1103;</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