Skip to content

Commit 60f616f

Browse files
committed
Merge branch 'c#代码' into packer重构
2 parents 2bb5b53 + 3ea4a55 commit 60f616f

46 files changed

Lines changed: 1866 additions & 47 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# CFPA-specifics
1+
# CFPA-specifics
22
Minecraft-Mod-Language-Package-*.zip
33
*.md5
44
build/
@@ -389,3 +389,5 @@ MigrationBackup/
389389
# Fody - auto-generated XML schema
390390
FodyWeavers.xsd
391391

392+
/.reasonix/truncated-results
393+
/.reasonix/truncated-results

Minecraft-Mod-Language-Package.sln

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Uploader", "src\Uploader\Up
99
EndProject
1010
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Language.Core", "src\Language.Core\Language.Core.csproj", "{D59EE21C-875E-4B31-8A97-813DC4C2DB68}"
1111
EndProject
12+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Packer.Core", "src\Packer.Core\Packer.Core.csproj", "{0ACFBD7A-2AE9-D5B9-1B5A-3D8A6B4E7A50}"
13+
EndProject
14+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Packer.Core.Tests", "src\Packer.Core.Tests\Packer.Core.Tests.csproj", "{C1362B4E-DC54-1918-71D7-10198B04FA50}"
15+
EndProject
1216
Global
1317
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1418
Debug|Any CPU = Debug|Any CPU
@@ -27,6 +31,14 @@ Global
2731
{D59EE21C-875E-4B31-8A97-813DC4C2DB68}.Debug|Any CPU.Build.0 = Debug|Any CPU
2832
{D59EE21C-875E-4B31-8A97-813DC4C2DB68}.Release|Any CPU.ActiveCfg = Release|Any CPU
2933
{D59EE21C-875E-4B31-8A97-813DC4C2DB68}.Release|Any CPU.Build.0 = Release|Any CPU
34+
{0ACFBD7A-2AE9-D5B9-1B5A-3D8A6B4E7A50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
35+
{0ACFBD7A-2AE9-D5B9-1B5A-3D8A6B4E7A50}.Debug|Any CPU.Build.0 = Debug|Any CPU
36+
{0ACFBD7A-2AE9-D5B9-1B5A-3D8A6B4E7A50}.Release|Any CPU.ActiveCfg = Release|Any CPU
37+
{0ACFBD7A-2AE9-D5B9-1B5A-3D8A6B4E7A50}.Release|Any CPU.Build.0 = Release|Any CPU
38+
{C1362B4E-DC54-1918-71D7-10198B04FA50}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
39+
{C1362B4E-DC54-1918-71D7-10198B04FA50}.Debug|Any CPU.Build.0 = Debug|Any CPU
40+
{C1362B4E-DC54-1918-71D7-10198B04FA50}.Release|Any CPU.ActiveCfg = Release|Any CPU
41+
{C1362B4E-DC54-1918-71D7-10198B04FA50}.Release|Any CPU.Build.0 = Release|Any CPU
3042
EndGlobalSection
3143
GlobalSection(SolutionProperties) = preSolution
3244
HideSolutionNode = FALSE

diag_mc.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Target dir exists: True
2+
Has font/: True
3+
Policy types: [DirectPolicy, IndirectPolicy]
4+
Providers: 0
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
using Packer.Core.Model;
2+
using Packer.Core.Model.Configuration;
3+
using Packer.Core.Model.ResourceFile;
4+
using System.Text.Json;
5+
6+
namespace Packer.Core.Tests.ConfigurationTests;
7+
8+
/// <summary>测试 1/2/3:配置与策略的解析与组合行为</summary>
9+
public class DeserializationTests
10+
{
11+
static readonly JsonSerializerOptions Opts = new()
12+
{
13+
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
14+
TypeInfoResolver = new DefaultJsonTypeInfoResolver()
15+
};
16+
17+
// ── 测试 1:浮动配置和打包策略在命名空间文件夹里时可以解析 ──
18+
19+
[Fact]
20+
public void NamespaceFolder_Loads_LocalConfig_And_Policy()
21+
{
22+
using var tmp = new TempDir();
23+
// 造一个命名空间目录,带 local-config.json 和 packer-policy.json
24+
tmp.Write("local-config.json", """{ "inclusionDomains": ["font"] }""");
25+
tmp.Write("packer-policy.json", """[{ "type": "singleton", "source": "a.json", "relativePath": "lang/zh_cn.json" }]""");
26+
27+
var ns = new AssetsNamespaceResource(tmp.Path, "myns", "mymod", "1.21");
28+
29+
// 验证 local-config 被加载
30+
Assert.Contains("font", ns.LocalConfig.InclusionDomains);
31+
32+
// 验证策略被加载(不会是 Shared)
33+
Assert.NotSame(PackerPolicy.Shared, ns.PackerPolicies);
34+
35+
// 验证策略类型正确
36+
var policy = Assert.Single(ns.PackerPolicies);
37+
Assert.IsType<SingletonPolicy>(policy);
38+
}
39+
40+
// ── 测试 2:全局配置和浮动配置组合正确 ──
41+
42+
[Fact]
43+
public void GlobalConfig_Merges_With_LocalConfig()
44+
{
45+
var global = new FloatingConfig(
46+
["font"], [], [], ["README.md"],
47+
new Dictionary<string, string> { ["旧"] = "新" },
48+
new Dictionary<string, string>());
49+
50+
var local = new FloatingConfig(
51+
["gui"], [], [], ["packer-policy.json"],
52+
new Dictionary<string, string>(),
53+
new Dictionary<string, string>());
54+
55+
var merged = global.Merge(local);
56+
57+
// domain 取并集
58+
Assert.Contains("font", merged.InclusionDomains);
59+
Assert.Contains("gui", merged.InclusionDomains);
60+
61+
// exclusionPaths 取并集
62+
Assert.Contains("README.md", merged.ExclusionPaths);
63+
Assert.Contains("packer-policy.json", merged.ExclusionPaths);
64+
65+
// 全局的 replacement 保留
66+
Assert.Equal("新", merged.CharacterReplacement["旧"]);
67+
}
68+
69+
// ── 测试 3:没有浮动配置和打包策略时结果正确 ──
70+
71+
[Fact]
72+
public void NoLocalConfig_Returns_Shared()
73+
{
74+
using var tmp = new TempDir();
75+
// 空目录,没有 local-config.json
76+
var ns = new AssetsNamespaceResource(tmp.Path, "ns", "mod", "1.21");
77+
78+
Assert.Same(FloatingConfig.Shared, ns.LocalConfig);
79+
Assert.Same(PackerPolicy.Shared, ns.PackerPolicies);
80+
}
81+
82+
// ── 测试 8:TryAdd 和 ModifyOnly ──
83+
84+
[Fact]
85+
public void JsonFile_Merge_TryAdd_FirstWins()
86+
{
87+
var a = new JsonFile(new() { ["a"] = "1", ["b"] = "2" }, "x.json");
88+
var b = new JsonFile(new() { ["a"] = "x", ["c"] = "3" }, "x.json");
89+
90+
var r = a.Merge(b);
91+
Assert.Equal("1", r.Entries["a"]); // 第一个赢
92+
Assert.Equal("2", r.Entries["b"]);
93+
Assert.Equal("3", r.Entries["c"]);
94+
}
95+
96+
[Fact]
97+
public void JsonFile_Merge_ModifyOnly_OverwritesExisting()
98+
{
99+
var a = new JsonFile(new() { ["a"] = "1", ["b"] = "2" }, "x.json");
100+
var b = new JsonFile(new() { ["a"] = "x", ["c"] = "3" }, "x.json")
101+
{
102+
PolicyItem = new DirectPolicy { ModifyOnly = true }
103+
};
104+
105+
var r = a.Merge(b);
106+
Assert.Equal("x", r.Entries["a"]); // 覆盖已有
107+
Assert.Equal("2", r.Entries["b"]); // 不动
108+
Assert.False(r.Entries.ContainsKey("c")); // 不添加新键
109+
}
110+
111+
[Fact]
112+
public void LangFile_Merge_TryAdd_FirstWins()
113+
{
114+
var a = new LangFile(new() { ["k1"] = "v1", ["k2"] = "v2" }, "x.lang");
115+
var b = new LangFile(new() { ["k1"] = "x", ["k3"] = "v3" }, "x.lang");
116+
117+
var r = a.Merge(b);
118+
Assert.Equal("v1", r.Entries["k1"]);
119+
Assert.Equal("v3", r.Entries["k3"]);
120+
}
121+
122+
// ── 测试 11:全局配置的过滤 ──
123+
// 此测试需要在真实目录中放置文件,用 AssetsNamespaceResource + DirectPolicy
124+
// 验证 inclusionDomains / exclusionDomains / exclusionPaths 生效
125+
126+
[Fact]
127+
public void GlobalFilter_InclusionDomain_ForceIncludes()
128+
{
129+
using var dir = new TempDir();
130+
dir.Write("font/extra.txt", "should be included via domain");
131+
dir.Write("sounds/ambient.ogg", "should NOT be included");
132+
133+
var ns = new AssetsNamespaceResource(dir.Path, "testns", "testmod", "1.21");
134+
var config = new FloatingConfig(
135+
InclusionDomains: ["font"], // font 之下无条件进包
136+
ExclusionDomains: [],
137+
InclusionPaths: [],
138+
ExclusionPaths: ["packer-policy.json", "local-config.json"],
139+
CharacterReplacement: new Dictionary<string, string>(),
140+
DestinationReplacement: new Dictionary<string, string>());
141+
142+
// DirectPolicy 默认共享
143+
var providers = new DirectPolicy().CreateProviders(ns, config).ToList();
144+
145+
Assert.Contains(providers, p => p.Destination.EndsWith("font/extra.txt"));
146+
Assert.DoesNotContain(providers, p => p.Destination.EndsWith("sounds/ambient.ogg"));
147+
}
148+
}
149+
150+
/// <summary>临时目录辅助类</summary>
151+
public class TempDir : IDisposable
152+
{
153+
public string Path { get; } = System.IO.Path.Combine(System.IO.Path.GetTempPath(), Guid.NewGuid().ToString());
154+
155+
public TempDir()
156+
{
157+
Directory.CreateDirectory(Path);
158+
}
159+
160+
/// <summary>在临时目录下创建一个文件(自动创建父目录)</summary>
161+
public void Write(string relativePath, string content)
162+
{
163+
var full = System.IO.Path.Combine(Path, relativePath);
164+
Directory.CreateDirectory(System.IO.Path.GetDirectoryName(full)!);
165+
File.WriteAllText(full, content);
166+
}
167+
168+
public void Dispose()
169+
{
170+
try { Directory.Delete(Path, recursive: true); }
171+
catch { /* 清理失败忽略 */ }
172+
}
173+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
global using Packer.Core.Model;
2+
global using Packer.Core.Model.Abstract;
3+
global using Packer.Core.Model.Configuration;
4+
global using Packer.Core.Model.ModProvider;
5+
global using Packer.Core.Model.ResourceFile;
6+
global using System.Text.Json;
7+
global using System.Text.Json.Serialization.Metadata;
8+
global using Packer.Core.Tests.ConfigurationTests;
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using Microsoft.Extensions.FileProviders;
2+
using Microsoft.Extensions.Primitives;
3+
4+
namespace Packer.Core.Tests;
5+
6+
/// <summary>
7+
/// 内存虚拟文件系统,实现 <see cref="IFileProvider"/>。
8+
/// 用于不依赖真实磁盘的单元测试。
9+
/// </summary>
10+
public class InMemoryFileProvider : IFileProvider
11+
{
12+
private readonly Dictionary<string, byte[]> _files = new(StringComparer.OrdinalIgnoreCase);
13+
private readonly HashSet<string> _dirs = new(StringComparer.OrdinalIgnoreCase);
14+
15+
/// <summary>添加一个虚拟文件</summary>
16+
public void AddFile(string path, string content)
17+
{
18+
path = Normalize(path);
19+
_files[path] = System.Text.Encoding.UTF8.GetBytes(content);
20+
// 注册所有父目录
21+
var parts = path.Split('/');
22+
for (int i = 1; i < parts.Length; i++)
23+
{
24+
var dir = string.Join("/", parts, 0, i);
25+
_dirs.Add(dir);
26+
}
27+
}
28+
29+
public IFileInfo GetFileInfo(string subpath)
30+
{
31+
subpath = Normalize(subpath);
32+
if (_files.TryGetValue(subpath, out var data))
33+
return new InMemoryFile(Path.GetFileName(subpath), data, false);
34+
return new InMemoryFile(Path.GetFileName(subpath), null, true);
35+
}
36+
37+
public IDirectoryContents GetDirectoryContents(string subpath)
38+
{
39+
subpath = Normalize(subpath);
40+
var prefix = string.IsNullOrEmpty(subpath) ? "" : subpath + "/";
41+
var files = _files.Keys
42+
.Where(k => k.StartsWith(prefix) && k.LastIndexOf('/') == prefix.Length - 1)
43+
.Select(k => new InMemoryFile(Path.GetFileName(k), _files[k], false));
44+
var dirs = _dirs
45+
.Where(d => d.StartsWith(prefix) && d.Length > prefix.Length && !d[prefix.Length..].Contains('/'))
46+
.Select(d => new InMemoryFile(d[(prefix.Length)..], null, false) { IsDirectory = true });
47+
return new InMemoryDirectoryContents(files.Concat(dirs));
48+
}
49+
50+
public IChangeToken Watch(string filter) => NullChangeToken.Singleton;
51+
52+
private static string Normalize(string path)
53+
=> path.Replace('\\', '/').TrimStart('/');
54+
}
55+
56+
internal class InMemoryFile : IFileInfo
57+
{
58+
private readonly byte[]? _data;
59+
public bool Exists => _data is not null;
60+
public long Length => _data?.Length ?? -1;
61+
public string? PhysicalPath => null;
62+
public string Name { get; }
63+
public DateTimeOffset LastModified => DateTimeOffset.MinValue;
64+
public bool IsDirectory { get; init; }
65+
66+
public InMemoryFile(string name, byte[]? data, bool notExists)
67+
{
68+
Name = name;
69+
_data = notExists ? null : data;
70+
}
71+
72+
public Stream CreateReadStream()
73+
=> _data is not null ? new MemoryStream(_data) : Stream.Null;
74+
}
75+
76+
internal class InMemoryDirectoryContents(IEnumerable<IFileInfo> contents) : IDirectoryContents
77+
{
78+
public bool Exists => true;
79+
public IEnumerator<IFileInfo> GetEnumerator() => contents.GetEnumerator();
80+
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
81+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
using Packer.Core.Model.ResourceFile;
2+
using System.Text.Json;
3+
4+
namespace Packer.Core.Tests.MergeTests;
5+
6+
/// <summary>测试 10:CompositionEntry 笛卡尔积</summary>
7+
public class CompositionEntryTests
8+
{
9+
static CompositionEntry ParseEntry(string json)
10+
{
11+
using var doc = JsonDocument.Parse(json);
12+
var root = doc.RootElement;
13+
var templates = new Dictionary<string, string>();
14+
foreach (var t in root.GetProperty("templates").EnumerateObject())
15+
templates[t.Name] = t.Value.GetString()!;
16+
17+
var parameters = new List<Dictionary<string, string>>();
18+
foreach (var p in root.GetProperty("parameters").EnumerateArray())
19+
{
20+
var dict = new Dictionary<string, string>();
21+
foreach (var kv in p.EnumerateObject())
22+
dict[kv.Name] = kv.Value.GetString()!;
23+
parameters.Add(dict);
24+
}
25+
return new CompositionEntry(templates, parameters);
26+
}
27+
28+
[Fact]
29+
public void SingleParam_SingleTemplate()
30+
{
31+
var entry = ParseEntry("""
32+
{ "templates": { "item.{0}.name": "{0}" }, "parameters": [{ "apple": "苹果", "banana": "香蕉" }] }
33+
""");
34+
var r = entry.BuildDictionary();
35+
Assert.Equal(2, r.Count);
36+
Assert.Equal("苹果", r["item.apple.name"]);
37+
}
38+
39+
[Fact]
40+
public void TwoParams_CartesianProduct()
41+
{
42+
var entry = ParseEntry("""
43+
{ "templates": { "{0}.{1}": "{1}.{0}" }, "parameters": [{ "a": "1", "b": "2" }, { "x": "!", "y": "?" }] }
44+
""");
45+
var r = entry.BuildDictionary();
46+
Assert.Equal(4, r.Count);
47+
Assert.Equal("!.1", r["a.x"]);
48+
}
49+
50+
[Fact]
51+
public void ThreeParams_FullProduct()
52+
{
53+
var entry = ParseEntry("""
54+
{"templates":{"{0}{1}{2}":"{0}{1}{2}"},"parameters":[{"A":"a"},{"1":"1"},{"@":"@"}]}
55+
""");
56+
var r = entry.BuildDictionary();
57+
Assert.Single(r);
58+
Assert.Equal("a1@", r["A1@"]);
59+
}
60+
61+
[Fact]
62+
public void TwoTemplates_FourEntriesEach()
63+
{
64+
var entry = ParseEntry("""
65+
{"templates":{"{0}.name":"{1}","{0}.desc":"{1}描述"},"parameters":[{"sword":"剑","pickaxe":"镐"},{"iron":"铁"}]}
66+
""");
67+
var r = entry.BuildDictionary();
68+
Assert.Equal(4, r.Count); // 2×1 参数 × 2 模板
69+
}
70+
}

0 commit comments

Comments
 (0)