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;
}
}
+109
View File
@@ -0,0 +1,109 @@
using System;
using System.IO;
namespace R0PatchGenerator
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("========================================");
Console.WriteLine(" R0对战平台 增量包生成器");
Console.WriteLine("========================================");
Console.WriteLine();
if (args.Length < 3)
{
PrintUsage();
return;
}
string oldVersionPath = args[0];
string newVersionPath = args[1];
string outputPath = args[2];
// 可选参数:版本号
string fromVersion = args.Length > 3 ? args[3] : "unknown";
string toVersion = args.Length > 4 ? args[4] : "unknown";
Console.WriteLine("源版本目录: " + oldVersionPath);
Console.WriteLine("目标版本目录: " + newVersionPath);
Console.WriteLine("输出路径: " + outputPath);
Console.WriteLine("从版本: " + fromVersion);
Console.WriteLine("到版本: " + toVersion);
Console.WriteLine();
if (!Directory.Exists(oldVersionPath))
{
Console.WriteLine("错误: 源版本目录不存在!");
return;
}
if (!Directory.Exists(newVersionPath))
{
Console.WriteLine("错误: 目标版本目录不存在!");
return;
}
try
{
PatchGenerator generator = new PatchGenerator();
bool success = generator.GeneratePatch(oldVersionPath, newVersionPath, outputPath, fromVersion, toVersion);
if (success)
{
Console.WriteLine();
Console.WriteLine("增量包生成成功!");
Console.WriteLine("输出文件: " + outputPath);
FileInfo fi = new FileInfo(outputPath);
Console.WriteLine("文件大小: " + FormatFileSize(fi.Length));
}
else
{
Console.WriteLine();
Console.WriteLine("增量包生成失败!");
}
}
catch (Exception ex)
{
Console.WriteLine("错误: " + ex.Message);
Console.WriteLine(ex.StackTrace);
}
}
static void PrintUsage()
{
Console.WriteLine("用法:");
Console.WriteLine(" R0PatchGenerator.exe <旧版本目录> <新版本目录> <输出文件.zip> [从版本] [到版本]");
Console.WriteLine();
Console.WriteLine("示例:");
Console.WriteLine(" R0PatchGenerator.exe \"C:\\versions\\1.0.0\" \"C:\\versions\\1.0.1\" \"patch_1.0.0_to_1.0.1.zip\" 1.0.0 1.0.1");
Console.WriteLine();
Console.WriteLine("说明:");
Console.WriteLine(" - 旧版本目录: 包含旧版本完整文件的目录");
Console.WriteLine(" - 新版本目录: 包含新版本完整文件的目录");
Console.WriteLine(" - 输出文件: 生成的增量包文件路径(.zip格式)");
Console.WriteLine(" - 从版本/到版本: 可选,用于生成版本信息");
Console.WriteLine();
Console.WriteLine("增量包内容:");
Console.WriteLine(" - patch.json: 更新清单文件");
Console.WriteLine(" - patches/: 差异文件目录(使用HDiffPatch格式)");
Console.WriteLine(" - new/: 新增文件目录");
Console.WriteLine();
Console.WriteLine("注意: 需要将 hdiffz.exe 放在同一目录下");
}
static 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);
}
}
}
@@ -0,0 +1,19 @@
using System.Reflection;
using System.Runtime.InteropServices;
[assembly: AssemblyTitle("R0PatchGenerator")]
[assembly: AssemblyDescription("R0对战平台增量包生成器")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("R0")]
[assembly: AssemblyProduct("R0PatchGenerator")]
[assembly: AssemblyCopyright("Copyright © R0 2024")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
[assembly: ComVisible(false)]
[assembly: Guid("b2c3d4e5-f6a7-8901-bcde-f23456789012")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
+42
View File
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Release</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{B2C3D4E5-F6A7-8901-BCDE-F23456789012}</ProjectGuid>
<OutputType>Exe</OutputType>
<RootNamespace>R0PatchGenerator</RootNamespace>
<AssemblyName>R0PatchGenerator</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
<ApplicationIcon>..\icon.ico</ApplicationIcon>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.IO.Compression.FileSystem" />
</ItemGroup>
<ItemGroup>
<Compile Include="Program.cs" />
<Compile Include="PatchGenerator.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
</ItemGroup>
<ItemGroup>
<Content Include="..\icon.ico" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>