Initial commit: R0Installer source, patch generator, API backend and build scripts

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-27 18:03:44 +08:00
commit 6124d6060c
33 changed files with 8389 additions and 0 deletions
+368
View File
@@ -0,0 +1,368 @@
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<string, FileInfo> oldFiles = GetAllFiles(oldPath);
Dictionary<string, FileInfo> 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<string, FileInfo> GetAllFiles(string rootPath)
{
Dictionary<string, FileInfo> files = new Dictionary<string, FileInfo>(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<string> unchanged = new List<string>();
public List<PatchDiffInfo> modified = new List<PatchDiffInfo>();
public List<PatchFileInfo> new_files = new List<PatchFileInfo>();
public List<string> deleted = new List<string>();
}
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;
}
}