using System; using System.IO; namespace XWorld.PublishTool { // 把构建产物落成发布目录 + 清单 + (可选)签名。 // privateKeyPem 传 null = 不签名:files.txt.sig 落空文件(两端验签均为"未配置则跳过")。 public sealed class PublishLayout { // 服务器树:outDir 即 /// 的内容(普通文件名,不做 md5 命名) public void PublishServer(string srcDir, string outDir, GameSpec spec, string privateKeyPem) { Directory.CreateDirectory(outDir); CopyTo(srcDir, spec.ServerAssembly, outDir); CopyTo(srcDir, spec.CoreDll, outDir); // game.json 先写,确保其 md5 纳入清单 File.WriteAllText(Path.Combine(outDir, "game.json"), GameJsonWriter.WriteServer(spec)); var fm = new FilesManifest(); foreach (var name in new[] { spec.ServerAssembly, spec.CoreDll, "game.json" }) { byte[] b = File.ReadAllBytes(Path.Combine(outDir, name)); fm.Add(name, Md5Util.OfBytes(b), b.Length); } WriteManifestAndSig(outDir, fm, privateKeyPem); } // 客户端树:outDir 即 CDN/minigame//// 的内容。 // spec.CodeAb / spec.Assets 均为完整 AB 文件名(code.unity3d / res.unity3d),统一 AB:无散 .bytes。 public void PublishClient(string srcDir, string outDir, GameSpec spec, string privateKeyPem) { if (string.IsNullOrEmpty(spec.CodeAb)) throw new InvalidOperationException("spec.CodeAb 为空:客户端发布需要代码 AB(统一 AB 格式)"); Directory.CreateDirectory(outDir); var fm = new FilesManifest(); CopyMd5Named(srcDir, spec.CodeAb, outDir, fm); foreach (var ab in spec.Assets) CopyMd5Named(srcDir, ab, outDir, fm); File.WriteAllText(Path.Combine(outDir, "game.json"), GameJsonWriter.WriteClient(spec)); WriteManifestAndSig(outDir, fm, privateKeyPem); } private static void WriteManifestAndSig(string outDir, FilesManifest fm, string privateKeyPem) { File.WriteAllText(Path.Combine(outDir, "files.txt"), fm.Render()); byte[] sig = privateKeyPem == null ? Array.Empty() : ManifestSigner.Sign(fm.Digest(), privateKeyPem); File.WriteAllBytes(Path.Combine(outDir, "files.txt.sig"), sig); } private static void CopyTo(string srcDir, string name, string dstDir) => File.Copy(Path.Combine(srcDir, name), Path.Combine(dstDir, name), true); // 落盘 md5 命名(name_md5.ext,与 MiniGameDownloader.Md5Name 推导一致),files.txt 登记逻辑名 private static void CopyMd5Named(string srcDir, string name, string dstDir, FilesManifest fm) { byte[] data = File.ReadAllBytes(Path.Combine(srcDir, name)); string md5 = Md5Util.OfBytes(data); File.WriteAllBytes(Path.Combine(dstDir, Md5Name(name, md5)), data); fm.Add(name, md5, data.Length); } // name.ext -> name_md5.ext(无扩展名则 name_md5) private static string Md5Name(string filename, string md5) { int pos = filename.LastIndexOf('.'); return pos >= 0 ? $"{filename.Substring(0, pos)}_{md5}{filename.Substring(pos)}" : $"{filename}_{md5}"; } } }