From fb27436ff2404fd90fc24b9fd1053cb24a97aeac Mon Sep 17 00:00:00 2001 From: mrbbbaixue Date: Tue, 10 Dec 2024 11:24:49 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E5=9F=BA=E6=9C=AC=E5=85=83=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=EF=BC=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Metadata/apps/apps.xml | 12 + .../ra3battlenet/changelogs/changelogs.xml | 17 + .../apps/ra3battlenet/manifests/1.5.1.0.xml | 23 + .../apps/ra3battlenet/manifests/1.5.2.0.xml | 23 + .../apps/ra3battlenet/manifests/manifests.xml | 13 + Metadata/apps/ra3battlenet/posts/posts.xml | 15 + Metadata/apps/ra3battlenet/ra3battlenet.xml | 52 +++ Metadata/client/client.xml | 22 - Metadata/coronalauncher/coronalauncher.xml | 16 - Metadata/coronalauncher/depot-application.xml | 20 - Metadata/example.xml | 4 - Metadata/metadata.xml | 11 +- .../mods/corona/changelogs/changelogs.xml | 17 + Metadata/mods/corona/corona.xml | 87 ++++ Metadata/mods/corona/manifests/3.228.xml | 29 ++ Metadata/mods/corona/manifests/manifests.xml | 12 + Metadata/mods/corona/posts/posts.xml | 14 + Metadata/mods/mods.xml | 11 + README.md | 53 ++- Ra3.BattleNet.Metadata/Metadata.cs | 401 ++++++++++++++++++ Ra3.BattleNet.Metadata/Program.cs | 111 +++-- .../Ra3.BattleNet.Metadata.csproj | 2 +- build.sh | 2 +- 23 files changed, 863 insertions(+), 104 deletions(-) create mode 100644 Metadata/apps/apps.xml create mode 100644 Metadata/apps/ra3battlenet/changelogs/changelogs.xml create mode 100644 Metadata/apps/ra3battlenet/manifests/1.5.1.0.xml create mode 100644 Metadata/apps/ra3battlenet/manifests/1.5.2.0.xml create mode 100644 Metadata/apps/ra3battlenet/manifests/manifests.xml create mode 100644 Metadata/apps/ra3battlenet/posts/posts.xml create mode 100644 Metadata/apps/ra3battlenet/ra3battlenet.xml delete mode 100644 Metadata/client/client.xml delete mode 100644 Metadata/coronalauncher/coronalauncher.xml delete mode 100644 Metadata/coronalauncher/depot-application.xml delete mode 100644 Metadata/example.xml create mode 100644 Metadata/mods/corona/changelogs/changelogs.xml create mode 100644 Metadata/mods/corona/corona.xml create mode 100644 Metadata/mods/corona/manifests/3.228.xml create mode 100644 Metadata/mods/corona/manifests/manifests.xml create mode 100644 Metadata/mods/corona/posts/posts.xml create mode 100644 Metadata/mods/mods.xml create mode 100644 Ra3.BattleNet.Metadata/Metadata.cs diff --git a/Metadata/apps/apps.xml b/Metadata/apps/apps.xml new file mode 100644 index 0000000..fd75695 --- /dev/null +++ b/Metadata/apps/apps.xml @@ -0,0 +1,12 @@ + + + + + ${ENV:CF_PAGES_COMMIT_SHA} + + + + + + + \ No newline at end of file diff --git a/Metadata/apps/ra3battlenet/changelogs/changelogs.xml b/Metadata/apps/ra3battlenet/changelogs/changelogs.xml new file mode 100644 index 0000000..0385751 --- /dev/null +++ b/Metadata/apps/ra3battlenet/changelogs/changelogs.xml @@ -0,0 +1,17 @@ + + + + + ${ENV:CF_PAGES_COMMIT_SHA} + + + + + + + + + + + + \ No newline at end of file diff --git a/Metadata/apps/ra3battlenet/manifests/1.5.1.0.xml b/Metadata/apps/ra3battlenet/manifests/1.5.1.0.xml new file mode 100644 index 0000000..d28fe56 --- /dev/null +++ b/Metadata/apps/ra3battlenet/manifests/1.5.1.0.xml @@ -0,0 +1,23 @@ + + + + + ${ENV:CF_PAGES_COMMIT_SHA} + + + + + + + + NativeDll.dll + /contents/ + APPLICATION; PROGRAM; + + + + /WorldBuilder/ + NewWorldBuilder-v20 + + + \ No newline at end of file diff --git a/Metadata/apps/ra3battlenet/manifests/1.5.2.0.xml b/Metadata/apps/ra3battlenet/manifests/1.5.2.0.xml new file mode 100644 index 0000000..1d2147f --- /dev/null +++ b/Metadata/apps/ra3battlenet/manifests/1.5.2.0.xml @@ -0,0 +1,23 @@ + + + + + ${ENV:CF_PAGES_COMMIT_SHA} + + + + + + + + NativeDll.dll + /contents/ + APPLICATION; PROGRAM; + + + + /WorldBuilder/ + NewWorldBuilder-v20 + + + \ No newline at end of file diff --git a/Metadata/apps/ra3battlenet/manifests/manifests.xml b/Metadata/apps/ra3battlenet/manifests/manifests.xml new file mode 100644 index 0000000..de95b39 --- /dev/null +++ b/Metadata/apps/ra3battlenet/manifests/manifests.xml @@ -0,0 +1,13 @@ + + + + + ${ENV:CF_PAGES_COMMIT_SHA} + + + + + + + + \ No newline at end of file diff --git a/Metadata/apps/ra3battlenet/posts/posts.xml b/Metadata/apps/ra3battlenet/posts/posts.xml new file mode 100644 index 0000000..4a7ef03 --- /dev/null +++ b/Metadata/apps/ra3battlenet/posts/posts.xml @@ -0,0 +1,15 @@ + + + + + ${ENV:CF_PAGES_COMMIT_SHA} + + + + + + + + + + \ No newline at end of file diff --git a/Metadata/apps/ra3battlenet/ra3battlenet.xml b/Metadata/apps/ra3battlenet/ra3battlenet.xml new file mode 100644 index 0000000..1114e06 --- /dev/null +++ b/Metadata/apps/ra3battlenet/ra3battlenet.xml @@ -0,0 +1,52 @@ + + + + + ${ENV:CF_PAGES_COMMIT_SHA} + + + + + + + + + + + + 1.5.2.0 + + + + 2025-03-29 + + changelog-zh-1.5.2.0 + changelog-en-1.5.2.0 + + manifest-1.5.2.0 + + + 2025-01-28 + + changelog-zh-1.5.1.0 + changelog-en-1.5.1.0 + + manifest-1.5.1.0 + + + + + + + 版本更新 1.5.2.0 + Update 1.5.2.0 + + + news-zh-1.5.2.0 + news-en-1.5.2.0 + + + + + + \ No newline at end of file diff --git a/Metadata/client/client.xml b/Metadata/client/client.xml deleted file mode 100644 index 6cd65e6..0000000 --- a/Metadata/client/client.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - 1.5.0.0 - {META:CoronaLauncher:LauncherVersion} - 1.5.0.0 - http://file.rmrts.com/srv/feed.xml - https://file.rmrts.com/srv/ra3battlenet/launcher/RA3BattleNet_Setup_{Version}.exe - https://bakneko.com/ra3battlenet/RA3BattleNet_Downloader_{Version}.exe - - changelog-en.txt - changelog-cn.txt - - - \ No newline at end of file diff --git a/Metadata/coronalauncher/coronalauncher.xml b/Metadata/coronalauncher/coronalauncher.xml deleted file mode 100644 index 699667f..0000000 --- a/Metadata/coronalauncher/coronalauncher.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - Application - 3.10.8811.300 - 3.215 - 3.215 - - \ No newline at end of file diff --git a/Metadata/coronalauncher/depot-application.xml b/Metadata/coronalauncher/depot-application.xml deleted file mode 100644 index dd3542a..0000000 --- a/Metadata/coronalauncher/depot-application.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - RA3CoronaDevelopers - - - - - 3.10.8811.300 - 2024-10-01 - - changelog-zh-3.10.8811.300.md - changelog-en-3.10.8811.300.md - - hash-application-3.10.8811.300.xml - - - - \ No newline at end of file diff --git a/Metadata/example.xml b/Metadata/example.xml deleted file mode 100644 index e48b2ae..0000000 --- a/Metadata/example.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/Metadata/metadata.xml b/Metadata/metadata.xml index 59bfd65..0787c57 100644 --- a/Metadata/metadata.xml +++ b/Metadata/metadata.xml @@ -1,11 +1,12 @@ - + + ${ENV:CF_PAGES_COMMIT_SHA} - - - + + + - \ No newline at end of file + \ No newline at end of file diff --git a/Metadata/mods/corona/changelogs/changelogs.xml b/Metadata/mods/corona/changelogs/changelogs.xml new file mode 100644 index 0000000..0385751 --- /dev/null +++ b/Metadata/mods/corona/changelogs/changelogs.xml @@ -0,0 +1,17 @@ + + + + + ${ENV:CF_PAGES_COMMIT_SHA} + + + + + + + + + + + + \ No newline at end of file diff --git a/Metadata/mods/corona/corona.xml b/Metadata/mods/corona/corona.xml new file mode 100644 index 0000000..83e86b6 --- /dev/null +++ b/Metadata/mods/corona/corona.xml @@ -0,0 +1,87 @@ + + + + + ${ENV:CF_PAGES_COMMIT_SHA} + + + + + + + + + + + + + + + + + + + + + 3.229 + corona-icon-64px + + + + + + + + 2025-04-01 + + changelog-zh-3229 + changelog-en-3229 + + manifest-3229 + + + 2025-03-28 + + changelog-zh-3228 + changelog-en-3228 + + manifest-3228 + + + + + + + + 版本更新 3.229 + Update 3.229 + + + news-zh-3229 + news-en-3229 + + + + + + \ No newline at end of file diff --git a/Metadata/mods/corona/manifests/3.228.xml b/Metadata/mods/corona/manifests/3.228.xml new file mode 100644 index 0000000..974f2dc --- /dev/null +++ b/Metadata/mods/corona/manifests/3.228.xml @@ -0,0 +1,29 @@ + + + + + ${ENV:CF_PAGES_COMMIT_SHA} + + + + + + + + corona_3.228.lyi + / + MOD; ENCRYPTED_FILE; CAN_PATCH; + + + + + + coronaBGM_3.228.lyi + / + MOD; ENCRYPTED_FILE; CAN_PATCH; + + + + + + \ No newline at end of file diff --git a/Metadata/mods/corona/manifests/manifests.xml b/Metadata/mods/corona/manifests/manifests.xml new file mode 100644 index 0000000..c5e61d8 --- /dev/null +++ b/Metadata/mods/corona/manifests/manifests.xml @@ -0,0 +1,12 @@ + + + + + ${ENV:CF_PAGES_COMMIT_SHA} + + + + + + + \ No newline at end of file diff --git a/Metadata/mods/corona/posts/posts.xml b/Metadata/mods/corona/posts/posts.xml new file mode 100644 index 0000000..bd255c2 --- /dev/null +++ b/Metadata/mods/corona/posts/posts.xml @@ -0,0 +1,14 @@ + + + + + ${ENV:CF_PAGES_COMMIT_SHA} + + + + + + + + + \ No newline at end of file diff --git a/Metadata/mods/mods.xml b/Metadata/mods/mods.xml new file mode 100644 index 0000000..2acea40 --- /dev/null +++ b/Metadata/mods/mods.xml @@ -0,0 +1,11 @@ + + + + + ${ENV:CF_PAGES_COMMIT_SHA} + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 53db71a..5cd0063 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,61 @@ # 红警3战网元数据文件 -此仓库存储的是战网客户端需要的客户端、插件、模组、地图的描述信息。 +此仓库存储的是战网客户端需要的客户端、数据包、模组、地图的描述信息。 【其他说明待编写】 -## 开发注记 +## 模块说明 -### {} 内联变量规定: +``` xml + + + + + + + + + + + + +``` + +大部分XML文件都包含Includes字段,本质上Metadata文件可以合并为一个大的XML。但是为了方便编辑和维护进行拆分 +Type分为 public 和 private,public指的是可以被展平(暴露给上级引用者,别的文件可以访问),private代表私有引用仅在此文件中可用,别的文件无法访问 + +### 元数据需求 + +Metadata 需要存储的数据有: + +1. 客户端本身的版本信息和元数据信息,包括:客户端版本号,数据包最新版本号,客户端更新列表,数据包更新列表,多语言翻译文件等 +2. Mod下载功能(预计50+Mod)需要包含:Mod列表,每个Mod都需要包含ModID,Mod版本号,Mod介绍(图文混合,可能是Markdown),Mod更新列表(更新列表要支持多个文件,并且要支持对不同文件定义哈希值和下载方式),新闻列表,新闻信息(图文混合) +3. 地图下载功能:预留 + +于是分为:Application, Markdown, Image, + + +存储信息的主要格式为XML,因为数据会引用包含图片素材和HTML/MarkDown素材,所以可能会有文件和XML放在一起。以ID的方法引用,每个松散数据都需要验证哈希(在引用时同时声明MD5,但此MD5需要进行计算) + +于是,XML数据需要支持变量,比如 ${MD5:} 是对指定ID的松散文件进行哈希值计算,并在Build时替换 +${MD5::}指的是对当前文件进行哈希计算 +需要编写XSD文件,方便对XML进行验证 + +### XML的 ${} 内联变量规定: ENV: 系统环境变量(CF Pages) -MD5: 对文件进行md5 ,MD5:this 指的是计算当前标签的content的文件路径的md5 +MD5: 对文件进行md5 ,${MD5::} 指的是计算当前标签文件路径的md5 -META: 绝对路径引用,冒号是此处规定的成员运算符 +或者 ${MD5:xxxx.txt} 代表对指定文件进行哈希 -this指当前标签的content +META: 绝对路径引用,冒号是此处规定的成员运算符 其他:相对路径引用(当前module或者depot下) **举例:** -{META:CoronaLauncher:LauncherVersion}代表 +${META:CoronaLauncher:LauncherVersion}代表 -{ENV:WINVER} 代表获取系统的winver变量值 +${ENV:WINVER} 代表获取系统的winver变量值 -{Version} (client.xml中可以找到相同例子),代表同一个Module,也就是Client下的Defines的Version变量值,即1.5.0.0 +${this:Version} (client.xml中可以找到相同例子),代表同一个Module,也就是Client下的Defines的Version变量值,即1.5.0.0 diff --git a/Ra3.BattleNet.Metadata/Metadata.cs b/Ra3.BattleNet.Metadata/Metadata.cs new file mode 100644 index 0000000..c4e0d50 --- /dev/null +++ b/Ra3.BattleNet.Metadata/Metadata.cs @@ -0,0 +1,401 @@ +using System.Security.Cryptography; +using System.Xml; +using System.Xml.Linq; +using System.Xml.Schema; + +namespace Ra3.BattleNet.Metadata +{ + public class Metadata + { + private readonly Dictionary _variables = new(); + private readonly List _children = new(); + private readonly Dictionary _defines = new(); + private readonly List _defineChildren = new(); + private readonly Dictionary _envVars = new(); + private readonly Dictionary _fileHashes = new(); + private string _currentFilePath = string.Empty; + + public string Name { get; private set; } = string.Empty; + public IReadOnlyDictionary Variables => _variables; + public IReadOnlyList Children => _children; + public IReadOnlyDictionary Defines => _defines; + public IReadOnlyList DefineChildren => _defineChildren; + + public Metadata() + { + foreach (System.Collections.DictionaryEntry env in Environment.GetEnvironmentVariables()) + { + _envVars[env.Key.ToString()] = env.Value.ToString(); + } + } + + /// + /// 从文件加载并验证XML元数据 + /// + /// XML文件路径 + /// 解析后的Metadata对象 + /// 当XML格式无效时抛出 + /// 当文件不存在时抛出 + public static Metadata LoadFromFile(string path) + { + if (string.IsNullOrWhiteSpace(path)) + throw new ArgumentException("文件路径不能为空", nameof(path)); + + string fullPath = Path.IsPathRooted(path) + ? path + : Path.Combine(Environment.CurrentDirectory, path); + + if (!File.Exists(fullPath)) + throw new FileNotFoundException($"找不到文件: {fullPath}"); + + // 配置XML验证设置 + var settings = new XmlReaderSettings + { + DtdProcessing = DtdProcessing.Prohibit, + XmlResolver = null, + ValidationFlags = XmlSchemaValidationFlags.None, + IgnoreWhitespace = true + }; + + try + { + using var reader = XmlReader.Create(fullPath, settings); + var doc = XDocument.Load(reader); + + if (doc.Root == null || doc.Root.Name != "Metadata") + throw new XmlException("无效的XML结构: 缺少根节点或根节点名称不是'Metadata'"); + + var metadata = new Metadata + { + _currentFilePath = fullPath + }; + + metadata.ParseElement(doc.Root, fullPath); + return metadata; + } + catch (XmlException ex) + { + throw new XmlException($"XML解析失败: {fullPath}", ex); + } + } + + /// + /// 编译当前元数据及其所有引用 + /// + /// 编译后的XDocument + /// 当XML结构无效时抛出 + public XDocument Compile() + { + try + { + var doc = new XDocument(new XElement(Name)); + var root = doc.Root ?? throw new InvalidOperationException("无效的XML结构: 缺少根节点"); + + // 复制属性 + foreach (var var in _variables) + { + root.SetAttributeValue(var.Key, var.Value); + } + + // 处理子元素 + foreach (var child in _children) + { + try + { + root.Add(child.ToXElement()); + } + catch (Exception ex) + { + throw new InvalidOperationException($"处理子元素失败: {child.Name}", ex); + } + } + + // 处理Includes和变量替换 + ProcessIncludes(root); + ReplaceVariables(root); + + return doc; + } + catch (Exception ex) + { + throw new InvalidOperationException($"编译元数据失败: {Name}", ex); + } + } + + private XElement ToXElement() + { + var element = new XElement(Name); + foreach (var var in _variables) + { + element.SetAttributeValue(var.Key, var.Value); + } + foreach (var child in _children) + { + element.Add(child.ToXElement()); + } + return element; + } + + private void ParseElement(XElement? element, string currentFilePath) + { + if (element == null) return; + + Name = element.Name.LocalName; + + foreach (var attr in element.Attributes()) + { + _variables[attr.Name.LocalName] = attr.Value; + } + + foreach (var child in element.Elements()) + { + if (child.Name.LocalName == "Include" || child.Name.LocalName == "Module") + { + var includePath = child.Attribute("Path")?.Value ?? child.Attribute("Source")?.Value; + if (!string.IsNullOrEmpty(includePath)) + { + string normalizedPath = includePath.Replace('/', '\\'); + string fullIncludePath = Path.Combine(Path.GetDirectoryName(currentFilePath) ?? string.Empty, normalizedPath); + var included = LoadFromFile(fullIncludePath); + _children.Add(included); + } + } + else if (child.Name.LocalName == "Defines") + { + foreach (var define in child.Elements()) + { + if (define.HasElements) + { + var defineChild = new Metadata(); + defineChild.ParseElement(define, currentFilePath); + _defineChildren.Add(defineChild); + } + else + { + _defines[define.Name.LocalName] = define.Value; + } + } + } + else + { + var childMetadata = new Metadata(); + childMetadata.ParseElement(child, currentFilePath); + _children.Add(childMetadata); + } + } + } + + private void ProcessIncludes(XElement element) + { + foreach (var include in element.Elements("Include")) + { + var source = include.Attribute("Source")?.Value; + if (string.IsNullOrEmpty(source)) continue; + + var fullPath = Path.Combine(Path.GetDirectoryName(_currentFilePath) ?? string.Empty, source.Replace('/', '\\')); + if (!File.Exists(fullPath)) continue; + + var includedDoc = XDocument.Load(fullPath); + var includedRoot = includedDoc.Root; + if (includedRoot == null) continue; + + ProcessIncludes(includedRoot); + include.ReplaceWith(includedRoot.Elements()); + } + } + + /// + /// 递归替换XML元素中的变量 + /// + /// 要处理的XML元素 + private void ReplaceVariables(XElement element) + { + try + { + foreach (var attr in element.Attributes()) + { + try + { + attr.Value = ResolveVariables(attr.Value, element); + } + catch (Exception ex) + { + throw new InvalidOperationException($"解析属性变量失败: {attr.Name}", ex); + } + } + + if (!element.HasElements && !string.IsNullOrEmpty(element.Value)) + { + try + { + element.Value = ResolveVariables(element.Value, element); + } + catch (Exception ex) + { + throw new InvalidOperationException($"解析元素值变量失败: {element.Name}", ex); + } + } + + foreach (var child in element.Elements()) + { + ReplaceVariables(child); + } + } + catch (Exception ex) + { + throw new InvalidOperationException($"替换变量失败: {element.Name}", ex); + } + } + + private string ResolveVariables(string input, XElement context) + { + return System.Text.RegularExpressions.Regex.Replace(input, @"\$\{(.*?)\}", match => + { + var expr = match.Groups[1].Value; + var parts = expr.Split(':'); + if (parts.Length < 1) return match.Value; + + switch (parts[0]) + { + case "ENV": + return parts.Length > 1 && _envVars.TryGetValue(parts[1], out var envValue) + ? envValue + : match.Value; + case "MD5": + var fileToHash = parts.Length > 1 ? parts[1] : ""; + if (string.IsNullOrEmpty(fileToHash)) + { + return ComputeFileHash(_currentFilePath); + } + return ComputeFileHash(Path.Combine(Path.GetDirectoryName(_currentFilePath) ?? string.Empty, fileToHash)); + case "META": + if (parts.Length < 2) return match.Value; + try + { + var metaPath = string.Join(":", parts.Skip(1)); + var target = FindMetaReference(context.Document?.Root, metaPath); + return target?.Value ?? $"META_NOT_FOUND:{metaPath}"; + } + catch + { + return $"META_ERROR:{string.Join(":", parts.Skip(1))}"; + } + case "this": + if (parts.Length < 2) return match.Value; + try + { + var currentModule = GetCurrentModule(context); + var target = FindInDefines(currentModule, parts[1]); + return target ?? $"THIS_NOT_FOUND:{parts[1]}"; + } + catch + { + return $"THIS_ERROR:{parts[1]}"; + } + default: + return match.Value; + } + }); + } + + private string ComputeFileHash(string filePath) + { + if (_fileHashes.TryGetValue(filePath, out var hash)) + { + return hash; + } + + try + { + using var md5 = MD5.Create(); + using var stream = File.OpenRead(filePath); + var hashBytes = md5.ComputeHash(stream); + hash = BitConverter.ToString(hashBytes).Replace("-", "").ToLowerInvariant(); + _fileHashes[filePath] = hash; + return hash; + } + catch + { + return $"HASH_ERROR_{filePath}"; + } + } + + private XElement? FindMetaReference(XElement? root, string path) + { + if (root == null) return null; + + var parts = path.Split(':'); + XElement? current = root; + + foreach (var part in parts) + { + current = current.Elements().FirstOrDefault(e => e.Name.LocalName == part); + if (current == null) return null; + } + + return current; + } + + private XElement? GetCurrentModule(XElement element) + { + var current = element; + while (current != null) + { + if (current.Name.LocalName == "Module") return current; + current = current.Parent; + } + return null; + } + + private string? FindInDefines(XElement? module, string key) + { + if (module == null) return null; + + var defines = module.Elements("Defines").FirstOrDefault(); + if (defines == null) return null; + + var define = defines.Elements().FirstOrDefault(e => e.Name.LocalName == key); + return define?.Value; + } + + public string? Get(string key, string? defaultValue = null) + { + if (_variables.TryGetValue(key, out var value)) + return value; + + if (_defines.TryGetValue(key, out value)) + return value; + + return defaultValue; + } + + public Metadata? Find(string path) + { + var parts = path.Split(':'); + if (parts.Length == 0) + return null; + + Metadata current = this; + foreach (var part in parts) + { + current = current._children.FirstOrDefault(c => c.Name == part); + if (current == null) + return null; + } + return current; + } + + public string? GetDefine(string path) + { + var lastColon = path.LastIndexOf(':'); + if (lastColon == -1) + return _defines.TryGetValue(path, out var value) ? value : null; + + var parentPath = path.Substring(0, lastColon); + var key = path.Substring(lastColon + 1); + + var parent = Find(parentPath); + return parent?.Get(key); + } + } +} diff --git a/Ra3.BattleNet.Metadata/Program.cs b/Ra3.BattleNet.Metadata/Program.cs index 238f1db..4c7d49c 100644 --- a/Ra3.BattleNet.Metadata/Program.cs +++ b/Ra3.BattleNet.Metadata/Program.cs @@ -1,63 +1,122 @@ -namespace Ra3.BattleNet.Metadata +using System; +using System.IO; + +namespace Ra3.BattleNet.Metadata { internal class Program { - public static string srcMetadataFolder = "./Metadata"; - public static string dstOutputFolder = "./Output"; - static void Main(string[] args) + private const string DefaultSrcFolder = "./Metadata"; + private const string DefaultDstFolder = "./Output"; + + public static string srcMetadataFolder = DefaultSrcFolder; + public static string dstOutputFolder = DefaultDstFolder; + + private static string GetArgValue(string arg, string defaultValue) { + var parts = arg.Split('='); + return parts.Length > 1 ? parts[1] : defaultValue; + } - // Read parameters from build script. + static void Main(string[] args) + { + // 读取构建脚本中的参数 foreach (string arg in args) { if (arg.StartsWith("--src=")) { - srcMetadataFolder = arg.Split('=')[1]; + srcMetadataFolder = GetArgValue(arg, DefaultSrcFolder); } if (arg.StartsWith("--dst=")) { - dstOutputFolder = arg.Split('=')[1]; + dstOutputFolder = GetArgValue(arg, DefaultDstFolder); } } + // 如果目标文件夹不存在,则创建它 if (!Directory.Exists(dstOutputFolder)) { - Directory.CreateDirectory(dstOutputFolder); + Directory.CreateDirectory(dstOutputFolder ?? DefaultDstFolder); } - Console.WriteLine($"workingDirectory: {Environment.CurrentDirectory}"); - Console.WriteLine($"srcMetadataFolder: {srcMetadataFolder}"); - Console.WriteLine($"dstOutputFolder: {dstOutputFolder}"); - + Console.WriteLine($"工作目录: {Environment.CurrentDirectory}"); + Console.WriteLine($"元数据源目录: {srcMetadataFolder}"); + Console.WriteLine($"输出目录: {dstOutputFolder}"); - // Copy to output dir try { - // 获取源目录下所有的文件 - string[] files = Directory.GetFiles(srcMetadataFolder, "*.*", SearchOption.AllDirectories); + // 1. 加载并编译metadata.xml + string metadataPath = Path.Combine(srcMetadataFolder, "metadata.xml"); + var metadata = Metadata.LoadFromFile(metadataPath); + var compiledDoc = metadata.Compile(); + + // 2. 保存编译后的文件 + string compiledPath = Path.Combine(dstOutputFolder ?? DefaultDstFolder, "metadata.compiled.xml"); + compiledDoc.Save(compiledPath); + Console.WriteLine($"已生成编译后的元数据文件: {compiledPath}"); + // 3. 复制所有其他文件到输出目录 + string[] files = Directory.GetFiles(srcMetadataFolder, "*.*", SearchOption.AllDirectories); foreach (string file in files) { - // 构建目标文件的完整路径 - string targetFilePath = Path.Combine(dstOutputFolder, file[(srcMetadataFolder.Length + 1)..]); + if (file.EndsWith("metadata.xml")) continue; // 已处理 + + string targetFilePath = Path.Combine(dstOutputFolder ?? DefaultDstFolder, + file[(srcMetadataFolder.Length + 1)..]); - // 确保目标文件所在的目录存在 - string targetFileDirectory = Path.GetDirectoryName(targetFilePath); - if (!Directory.Exists(targetFileDirectory)) + string targetDir = Path.GetDirectoryName(targetFilePath) ?? throw new InvalidOperationException("无法确定目标目录"); + if (!Directory.Exists(targetDir)) { - Directory.CreateDirectory(targetFileDirectory); + Directory.CreateDirectory(targetDir); } - // 复制文件 - File.Copy(file, targetFilePath, true); // 第三个参数为true表示如果目标位置已有同名文件,则覆盖 - Console.WriteLine($"Copy {file} to {targetFilePath}"); + File.Copy(file, targetFilePath, true); + Console.WriteLine($"复制 {file} 到 {targetFilePath}"); } - Console.WriteLine("文件复制完成!"); + Console.WriteLine("文件处理完成!"); + + // 4. 验证编译后的文件 + try + { + var verifiedMetadata = Metadata.LoadFromFile(compiledPath); + Console.WriteLine($"成功验证编译后的元数据"); + Console.WriteLine($"根节点: {verifiedMetadata.Name}"); + Console.WriteLine($"包含 {verifiedMetadata.Children.Count} 个子节点"); + + // 示例查询 + string commit = verifiedMetadata.Get("Commit") ?? "Unknown"; + Console.WriteLine($"Commit: {commit}"); + + // 打印所有模块信息 + PrintModuleInfo(verifiedMetadata, 0); + } + catch (Exception ex) + { + Console.WriteLine($"验证编译后的元数据时发生错误: {ex.Message}"); + } } catch (Exception ex) { - Console.WriteLine($"发生错误: {ex.Message}"); + Console.WriteLine($"处理过程中发生错误: {ex.Message}"); + } + } + + private static void PrintModuleInfo(Metadata metadata, int indentLevel) + { + string indent = new string(' ', indentLevel * 2); + string moduleName = metadata.Get("Name") ?? "Unknown"; + Console.WriteLine($"{indent}模块: {moduleName}"); + + // 打印变量 + foreach (var var in metadata.Variables) + { + Console.WriteLine($"{indent} {var.Key}: {var.Value}"); + } + + // 递归打印子模块 + foreach (var child in metadata.Children) + { + PrintModuleInfo(child, indentLevel + 1); } } } diff --git a/Ra3.BattleNet.Metadata/Ra3.BattleNet.Metadata.csproj b/Ra3.BattleNet.Metadata/Ra3.BattleNet.Metadata.csproj index 2150e37..91b464a 100644 --- a/Ra3.BattleNet.Metadata/Ra3.BattleNet.Metadata.csproj +++ b/Ra3.BattleNet.Metadata/Ra3.BattleNet.Metadata.csproj @@ -1,4 +1,4 @@ - + Exe diff --git a/build.sh b/build.sh index cf6b917..696ebf9 100644 --- a/build.sh +++ b/build.sh @@ -2,4 +2,4 @@ curl -sSL https://dot.net/v1/dotnet-install.sh > dotnet-install.sh chmod +x dotnet-install.sh ./dotnet-install.sh -c 8.0 -InstallDir ./dotnet ./dotnet/dotnet --version -./dotnet/dotnet run --project ./Ra3.BattleNet.Metadata --src="./Metadata" --dst="./output" +./dotnet/dotnet run --project ./Ra3.BattleNet.Metadata --src="./Metadata" --dst="./Output" From 8a2e510a7b43cd3afd417788418b2ccb144948d5 Mon Sep 17 00:00:00 2001 From: mrbbbaixue Date: Tue, 22 Apr 2025 20:28:30 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E6=94=B9=EF=BC=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Ra3.BattleNet.Metadata/Metadata.cs | 74 ++++++++++++++++++++++++++++-- Ra3.BattleNet.Metadata/Program.cs | 20 ++++---- 2 files changed, 80 insertions(+), 14 deletions(-) diff --git a/Ra3.BattleNet.Metadata/Metadata.cs b/Ra3.BattleNet.Metadata/Metadata.cs index c4e0d50..7ff35e5 100644 --- a/Ra3.BattleNet.Metadata/Metadata.cs +++ b/Ra3.BattleNet.Metadata/Metadata.cs @@ -122,6 +122,59 @@ public XDocument Compile() } } + /// + /// 在原文件中替换变量并验证资源 + /// + /// 要处理的XML文件路径 + /// 当XML结构无效或资源验证失败时抛出 + /// + /// 在原文件中替换变量并验证资源 + /// + /// 要处理的XML文件路径 + /// 当XML结构无效或资源验证失败时抛出 + public void ReplaceVariablesInFile(string filePath) + { + try + { + // 加载原始XML文件 + var doc = XDocument.Load(filePath); + var root = doc.Root ?? throw new InvalidOperationException("无效的XML结构: 缺少根节点"); + + // 验证所有Include/Module资源 + ValidateResources(root, Path.GetDirectoryName(filePath)); + + // 替换变量 + ReplaceVariables(root); + + // 保存回原文件 + doc.Save(filePath); + } + catch (Exception ex) + { + throw new InvalidOperationException($"替换变量失败: {filePath}", ex); + } + } + + private void ValidateResources(XElement element, string basePath) + { + foreach (var include in element.Elements("Include").Concat(element.Elements("Module"))) + { + var path = include.Attribute("Path")?.Value ?? include.Attribute("Source")?.Value; + if (string.IsNullOrEmpty(path)) continue; + + var fullPath = Path.Combine(basePath, path.Replace('/', '\\')); + if (!File.Exists(fullPath)) + { + throw new FileNotFoundException($"引用的资源文件不存在: {fullPath} (来自元素: {include.Name.LocalName})"); + } + } + + foreach (var child in element.Elements()) + { + ValidateResources(child, basePath); + } + } + private XElement ToXElement() { var element = new XElement(Name); @@ -155,7 +208,12 @@ private void ParseElement(XElement? element, string currentFilePath) if (!string.IsNullOrEmpty(includePath)) { string normalizedPath = includePath.Replace('/', '\\'); - string fullIncludePath = Path.Combine(Path.GetDirectoryName(currentFilePath) ?? string.Empty, normalizedPath); + var dir = Path.GetDirectoryName(currentFilePath); + if (string.IsNullOrEmpty(dir)) + { + throw new InvalidOperationException($"无法确定文件目录: {currentFilePath}"); + } + string fullIncludePath = Path.Combine(dir, normalizedPath); var included = LoadFromFile(fullIncludePath); _children.Add(included); } @@ -192,7 +250,12 @@ private void ProcessIncludes(XElement element) var source = include.Attribute("Source")?.Value; if (string.IsNullOrEmpty(source)) continue; - var fullPath = Path.Combine(Path.GetDirectoryName(_currentFilePath) ?? string.Empty, source.Replace('/', '\\')); + var dir = Path.GetDirectoryName(_currentFilePath); + if (string.IsNullOrEmpty(dir)) + { + throw new InvalidOperationException($"无法确定文件目录: {_currentFilePath}"); + } + var fullPath = Path.Combine(dir, source.Replace('/', '\\')); if (!File.Exists(fullPath)) continue; var includedDoc = XDocument.Load(fullPath); @@ -267,7 +330,12 @@ private string ResolveVariables(string input, XElement context) { return ComputeFileHash(_currentFilePath); } - return ComputeFileHash(Path.Combine(Path.GetDirectoryName(_currentFilePath) ?? string.Empty, fileToHash)); + var dir = Path.GetDirectoryName(_currentFilePath); + if (string.IsNullOrEmpty(dir)) + { + return $"HASH_ERROR_DIR_NOT_FOUND:{_currentFilePath}"; + } + return ComputeFileHash(Path.Combine(dir, fileToHash)); case "META": if (parts.Length < 2) return match.Value; try diff --git a/Ra3.BattleNet.Metadata/Program.cs b/Ra3.BattleNet.Metadata/Program.cs index 4c7d49c..c04b936 100644 --- a/Ra3.BattleNet.Metadata/Program.cs +++ b/Ra3.BattleNet.Metadata/Program.cs @@ -44,15 +44,13 @@ static void Main(string[] args) try { - // 1. 加载并编译metadata.xml + // 1. 加载并处理metadata.xml string metadataPath = Path.Combine(srcMetadataFolder, "metadata.xml"); var metadata = Metadata.LoadFromFile(metadataPath); - var compiledDoc = metadata.Compile(); - - // 2. 保存编译后的文件 - string compiledPath = Path.Combine(dstOutputFolder ?? DefaultDstFolder, "metadata.compiled.xml"); - compiledDoc.Save(compiledPath); - Console.WriteLine($"已生成编译后的元数据文件: {compiledPath}"); + + // 2. 在原文件中替换变量并验证资源 + metadata.ReplaceVariablesInFile(metadataPath); + Console.WriteLine($"已处理元数据文件: {metadataPath}"); // 3. 复制所有其他文件到输出目录 string[] files = Directory.GetFiles(srcMetadataFolder, "*.*", SearchOption.AllDirectories); @@ -75,11 +73,11 @@ static void Main(string[] args) Console.WriteLine("文件处理完成!"); - // 4. 验证编译后的文件 + // 4. 验证处理后的文件 try { - var verifiedMetadata = Metadata.LoadFromFile(compiledPath); - Console.WriteLine($"成功验证编译后的元数据"); + var verifiedMetadata = Metadata.LoadFromFile(metadataPath); + Console.WriteLine($"成功验证处理后的元数据"); Console.WriteLine($"根节点: {verifiedMetadata.Name}"); Console.WriteLine($"包含 {verifiedMetadata.Children.Count} 个子节点"); @@ -92,7 +90,7 @@ static void Main(string[] args) } catch (Exception ex) { - Console.WriteLine($"验证编译后的元数据时发生错误: {ex.Message}"); + Console.WriteLine($"验证处理后的元数据时发生错误: {ex.Message}"); } } catch (Exception ex)