diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/CombineResultMaterialIdMapFactory.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/CombineResultMaterialIdMapFactory.cs
new file mode 100644
index 0000000..120b7e9
--- /dev/null
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/CombineResultMaterialIdMapFactory.cs
@@ -0,0 +1,64 @@
+// Copyright (c) DGP Studio. All rights reserved.
+// Licensed under the MIT license.
+
+using Snap.Hutao.Model.Intrinsic;
+using Snap.Hutao.Model.Metadata;
+using Snap.Hutao.Model.Primitive;
+using System.Collections.Immutable;
+
+namespace Snap.Hutao.Service.Metadata;
+
+///
+/// Combine 元数据中多条配方可共享同一产物 Id(例如元素断片既有 3×碎屑合成,也有用其他元素石与尘的转化)。
+/// 构建 MaterialId→Combine 映射时必须优先保留「单材料×3→产物×1」的合成台主链,否则后项覆盖前项会导致养成统计无法向下展开原料。
+///
+internal static class CombineResultMaterialIdMapFactory
+{
+ public static ImmutableDictionary ToImmutableDictionary(ImmutableArray combines)
+ {
+ Dictionary map = [];
+
+ foreach (Combine current in combines)
+ {
+ MaterialId key = current.Result.Id;
+ if (!map.TryGetValue(key, out Combine? existing))
+ {
+ map[key] = current;
+ continue;
+ }
+
+ if (ComparePreference(current, existing) > 0)
+ {
+ map[key] = current;
+ }
+ }
+
+ return map.ToImmutableDictionary();
+ }
+
+ /// 正数表示 应取代已有项。
+ private static int ComparePreference(Combine incoming, Combine existing)
+ {
+ return Score(incoming).CompareTo(Score(existing));
+ }
+
+ private static int Score(Combine c)
+ {
+ if (c.Materials.Length is 1 && c.Materials[0].Count is 3 && c.Result.Count is 1)
+ {
+ return c.RecipeType switch
+ {
+ RecipeType.RECIPE_TYPE_COMBINE => 300,
+ RecipeType.RECIPE_TYPE_CONVERT => 200,
+ _ => 150,
+ };
+ }
+
+ if (c.RecipeType is RecipeType.RECIPE_TYPE_COMBINE)
+ {
+ return 100;
+ }
+
+ return 0;
+ }
+}
diff --git a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataServiceImmutableDictionaryExtension.cs b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataServiceImmutableDictionaryExtension.cs
index 0b93ecf..f7ced14 100644
--- a/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataServiceImmutableDictionaryExtension.cs
+++ b/src/Snap.Hutao/Snap.Hutao/Service/Metadata/MetadataServiceImmutableDictionaryExtension.cs
@@ -216,12 +216,17 @@ internal static class MetadataServiceImmutableDictionaryExtension
token);
}
- public ValueTask> GetResultMaterialIdToCombineMapAsync(CancellationToken token = default)
+ public async ValueTask> GetResultMaterialIdToCombineMapAsync(CancellationToken token = default)
{
- return metadataService.FromCacheAsDictionaryAsync(
- MetadataFileStrategies.Combine,
- c => c.Result.Id,
- token);
+ string cacheKey = $"{nameof(MetadataService)}.Cache.{MetadataFileStrategies.Combine.Name}.Map.{nameof(MaterialId)}.{nameof(Combine)}.PreferThreeToOne";
+ ImmutableDictionary? result = await metadataService.MemoryCache.GetOrCreateAsync(cacheKey, async entry =>
+ {
+ ImmutableArray array = await metadataService.FromCacheOrFileAsync(MetadataFileStrategies.Combine, token).ConfigureAwait(false);
+ return CombineResultMaterialIdMapFactory.ToImmutableDictionary(array);
+ }).ConfigureAwait(false);
+
+ ArgumentNullException.ThrowIfNull(result);
+ return result;
}
private ValueTask> FromCacheAsDictionaryAsync(MetadataFileStrategy strategy, CancellationToken token)