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..7ff35e5
--- /dev/null
+++ b/Ra3.BattleNet.Metadata/Metadata.cs
@@ -0,0 +1,469 @@
+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);
+ }
+ }
+
+ ///
+ /// 在原文件中替换变量并验证资源
+ ///
+ /// 要处理的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);
+ 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('/', '\\');
+ 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);
+ }
+ }
+ 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 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);
+ 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);
+ }
+ 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
+ {
+ 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..c04b936 100644
--- a/Ra3.BattleNet.Metadata/Program.cs
+++ b/Ra3.BattleNet.Metadata/Program.cs
@@ -1,63 +1,120 @@
-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);
+
+ // 2. 在原文件中替换变量并验证资源
+ metadata.ReplaceVariablesInFile(metadataPath);
+ Console.WriteLine($"已处理元数据文件: {metadataPath}");
+ // 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(metadataPath);
+ 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"