using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; using System.Security.Cryptography; using System.Text; namespace R0PatchGenerator { public class PatchGenerator { private string hdiffzPath; public PatchGenerator() { // 查找 hdiffz.exe string exeDir = Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location); hdiffzPath = Path.Combine(exeDir, "hdiffz.exe"); if (!File.Exists(hdiffzPath)) { // 尝试在当前目录查找 hdiffzPath = "hdiffz.exe"; } } public bool GeneratePatch(string oldPath, string newPath, string outputZip, string fromVersion, string toVersion) { // 创建临时目录 string tempDir = Path.Combine(Path.GetTempPath(), "r0patch_" + Guid.NewGuid().ToString("N")); string patchesDir = Path.Combine(tempDir, "patches"); string newFilesDir = Path.Combine(tempDir, "new"); Directory.CreateDirectory(tempDir); Directory.CreateDirectory(patchesDir); Directory.CreateDirectory(newFilesDir); try { // 获取所有文件 Dictionary oldFiles = GetAllFiles(oldPath); Dictionary newFiles = GetAllFiles(newPath); Console.WriteLine("旧版本文件数: " + oldFiles.Count); Console.WriteLine("新版本文件数: " + newFiles.Count); Console.WriteLine(); // 创建清单 PatchManifest manifest = new PatchManifest(); manifest.from_version = fromVersion; manifest.to_version = toVersion; manifest.created_at = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ"); int processedCount = 0; int totalFiles = newFiles.Count; foreach (var kvp in newFiles) { string relativePath = kvp.Key; FileInfo newFile = kvp.Value; processedCount++; Console.Write("[" + processedCount + "/" + totalFiles + "] " + relativePath + " ... "); if (oldFiles.ContainsKey(relativePath)) { // 文件存在于两个版本 FileInfo oldFile = oldFiles[relativePath]; string oldHash = CalculateMD5(oldFile.FullName); string newHash = CalculateMD5(newFile.FullName); if (oldHash == newHash) { // 文件未变化 Console.WriteLine("未变化"); manifest.unchanged.Add(relativePath); } else { // 文件已修改,生成差异 string patchFileName = relativePath.Replace("\\", "_").Replace("/", "_") + ".hdiff"; string patchFilePath = Path.Combine(patchesDir, patchFileName); // 确保目录存在 Directory.CreateDirectory(Path.GetDirectoryName(patchFilePath)); bool patchSuccess = CreateHDiff(oldFile.FullName, newFile.FullName, patchFilePath); if (patchSuccess && File.Exists(patchFilePath)) { FileInfo patchInfo = new FileInfo(patchFilePath); FileInfo originalInfo = new FileInfo(newFile.FullName); // 如果差异文件比原文件还大,直接用新文件 if (patchInfo.Length >= originalInfo.Length * 0.9) { Console.WriteLine("替换 (差异过大)"); File.Delete(patchFilePath); // 作为新文件处理 string newFilePath = Path.Combine(newFilesDir, relativePath); Directory.CreateDirectory(Path.GetDirectoryName(newFilePath)); File.Copy(newFile.FullName, newFilePath, true); PatchFileInfo pfi = new PatchFileInfo(); pfi.path = relativePath; pfi.hash = newHash; pfi.size = newFile.Length; manifest.new_files.Add(pfi); } else { Console.WriteLine("差异 (" + FormatFileSize(patchInfo.Length) + ")"); PatchDiffInfo pdi = new PatchDiffInfo(); pdi.path = relativePath; pdi.patch_file = "patches/" + patchFileName; pdi.old_hash = oldHash; pdi.new_hash = newHash; pdi.new_size = newFile.Length; manifest.modified.Add(pdi); } } else { // 差异生成失败,使用完整文件 Console.WriteLine("替换 (差异失败)"); string newFilePath = Path.Combine(newFilesDir, relativePath); Directory.CreateDirectory(Path.GetDirectoryName(newFilePath)); File.Copy(newFile.FullName, newFilePath, true); PatchFileInfo pfi = new PatchFileInfo(); pfi.path = relativePath; pfi.hash = newHash; pfi.size = newFile.Length; manifest.new_files.Add(pfi); } } // 从旧文件列表移除 oldFiles.Remove(relativePath); } else { // 新增文件 Console.WriteLine("新增"); string newFilePath = Path.Combine(newFilesDir, relativePath); Directory.CreateDirectory(Path.GetDirectoryName(newFilePath)); File.Copy(newFile.FullName, newFilePath, true); PatchFileInfo pfi = new PatchFileInfo(); pfi.path = relativePath; pfi.hash = CalculateMD5(newFile.FullName); pfi.size = newFile.Length; manifest.new_files.Add(pfi); } } // 剩余的旧文件需要删除 foreach (var kvp in oldFiles) { Console.WriteLine("删除: " + kvp.Key); manifest.deleted.Add(kvp.Key); } Console.WriteLine(); Console.WriteLine("统计:"); Console.WriteLine(" - 未变化: " + manifest.unchanged.Count); Console.WriteLine(" - 已修改: " + manifest.modified.Count); Console.WriteLine(" - 新增: " + manifest.new_files.Count); Console.WriteLine(" - 删除: " + manifest.deleted.Count); // 生成清单JSON string manifestJson = GenerateManifestJson(manifest); File.WriteAllText(Path.Combine(tempDir, "patch.json"), manifestJson, Encoding.UTF8); // 打包成ZIP if (File.Exists(outputZip)) File.Delete(outputZip); ZipFile.CreateFromDirectory(tempDir, outputZip, CompressionLevel.Optimal, false); return true; } finally { // 清理临时目录 try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { } } } private Dictionary GetAllFiles(string rootPath) { Dictionary files = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (string filePath in Directory.GetFiles(rootPath, "*", SearchOption.AllDirectories)) { string relativePath = filePath.Substring(rootPath.Length).TrimStart('\\', '/'); files[relativePath] = new FileInfo(filePath); } return files; } private string CalculateMD5(string filePath) { using (var md5 = MD5.Create()) using (var stream = File.OpenRead(filePath)) { byte[] hash = md5.ComputeHash(stream); StringBuilder sb = new StringBuilder(); foreach (byte b in hash) sb.Append(b.ToString("x2")); return sb.ToString(); } } private bool CreateHDiff(string oldFile, string newFile, string patchFile) { if (!File.Exists(hdiffzPath)) { Console.WriteLine("警告: hdiffz.exe 未找到,将使用完整文件替换"); return false; } try { ProcessStartInfo psi = new ProcessStartInfo(); psi.FileName = hdiffzPath; // -m: 使用内存匹配(更快),-c-zlib: 使用zlib压缩 psi.Arguments = String.Format("-m -c-zlib \"{0}\" \"{1}\" \"{2}\"", oldFile, newFile, patchFile); psi.UseShellExecute = false; psi.RedirectStandardOutput = true; psi.RedirectStandardError = true; psi.CreateNoWindow = true; using (Process process = Process.Start(psi)) { process.WaitForExit(60000); // 最多等待60秒 return process.ExitCode == 0; } } catch (Exception ex) { Console.WriteLine("HDiff错误: " + ex.Message); return false; } } private string GenerateManifestJson(PatchManifest manifest) { StringBuilder sb = new StringBuilder(); sb.AppendLine("{"); sb.AppendLine(" \"from_version\": \"" + EscapeJson(manifest.from_version) + "\","); sb.AppendLine(" \"to_version\": \"" + EscapeJson(manifest.to_version) + "\","); sb.AppendLine(" \"created_at\": \"" + EscapeJson(manifest.created_at) + "\","); // unchanged sb.AppendLine(" \"unchanged\": ["); for (int i = 0; i < manifest.unchanged.Count; i++) { sb.Append(" \"" + EscapeJson(manifest.unchanged[i]) + "\""); if (i < manifest.unchanged.Count - 1) sb.Append(","); sb.AppendLine(); } sb.AppendLine(" ],"); // modified sb.AppendLine(" \"modified\": ["); for (int i = 0; i < manifest.modified.Count; i++) { PatchDiffInfo pdi = manifest.modified[i]; sb.AppendLine(" {"); sb.AppendLine(" \"path\": \"" + EscapeJson(pdi.path) + "\","); sb.AppendLine(" \"patch_file\": \"" + EscapeJson(pdi.patch_file) + "\","); sb.AppendLine(" \"old_hash\": \"" + pdi.old_hash + "\","); sb.AppendLine(" \"new_hash\": \"" + pdi.new_hash + "\","); sb.AppendLine(" \"new_size\": " + pdi.new_size); sb.Append(" }"); if (i < manifest.modified.Count - 1) sb.Append(","); sb.AppendLine(); } sb.AppendLine(" ],"); // new_files sb.AppendLine(" \"new_files\": ["); for (int i = 0; i < manifest.new_files.Count; i++) { PatchFileInfo pfi = manifest.new_files[i]; sb.AppendLine(" {"); sb.AppendLine(" \"path\": \"" + EscapeJson(pfi.path) + "\","); sb.AppendLine(" \"hash\": \"" + pfi.hash + "\","); sb.AppendLine(" \"size\": " + pfi.size); sb.Append(" }"); if (i < manifest.new_files.Count - 1) sb.Append(","); sb.AppendLine(); } sb.AppendLine(" ],"); // deleted sb.AppendLine(" \"deleted\": ["); for (int i = 0; i < manifest.deleted.Count; i++) { sb.Append(" \"" + EscapeJson(manifest.deleted[i]) + "\""); if (i < manifest.deleted.Count - 1) sb.Append(","); sb.AppendLine(); } sb.AppendLine(" ]"); sb.AppendLine("}"); return sb.ToString(); } private string EscapeJson(string s) { if (s == null) return ""; return s.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r"); } private string FormatFileSize(long bytes) { if (bytes >= 1073741824) return String.Format("{0:F2} GB", bytes / 1073741824.0); if (bytes >= 1048576) return String.Format("{0:F2} MB", bytes / 1048576.0); if (bytes >= 1024) return String.Format("{0:F2} KB", bytes / 1024.0); return String.Format("{0} B", bytes); } } // 清单数据结构 public class PatchManifest { public string from_version = ""; public string to_version = ""; public string created_at = ""; public List unchanged = new List(); public List modified = new List(); public List new_files = new List(); public List deleted = new List(); } public class PatchDiffInfo { public string path = ""; public string patch_file = ""; public string old_hash = ""; public string new_hash = ""; public long new_size = 0; } public class PatchFileInfo { public string path = ""; public string hash = ""; public long size = 0; } }