wip deepseek
This commit is contained in:
@@ -540,12 +540,7 @@ namespace AnotherReplayReader.Utils
|
||||
|
||||
// Check 3: UnitId used as builder vs claim
|
||||
var isBuilderInReplay = factIndex.BuilderUnitIds.Contains(unitId);
|
||||
var claimLooksLikeBuilder = claim.Claim.IndexOf("MCV", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| claim.Claim.IndexOf("基地车", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| claim.Claim.IndexOf("Nanocore", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| claim.Claim.IndexOf("纳米核心", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| claim.Claim.IndexOf("builder", StringComparison.OrdinalIgnoreCase) >= 0
|
||||
|| claim.Claim.IndexOf("建造者", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
var claimLooksLikeBuilder = ClaimLooksLikeBuilder(claim.Claim);
|
||||
if (claimLooksLikeBuilder && !isBuilderInReplay)
|
||||
{
|
||||
issues.Add(new AIValidationIssue(
|
||||
@@ -651,5 +646,31 @@ namespace AnotherReplayReader.Utils
|
||||
}
|
||||
return result.ToImmutableArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a claim text describes a builder unit (MCV, Nanocore, etc.).
|
||||
/// Uses structured knowledge (knowledge_units.json) when available, falls
|
||||
/// back to heuristic string matching for backward compatibility.
|
||||
/// </summary>
|
||||
private static bool ClaimLooksLikeBuilder(string claimText)
|
||||
{
|
||||
// Primary: structured knowledge lookup
|
||||
var structured = StructuredKnowledge.Instance;
|
||||
if (structured is not null)
|
||||
{
|
||||
foreach (var entity in structured.AllEntities)
|
||||
{
|
||||
if (entity.HasTag(KnowledgeTag.Builder) &&
|
||||
claimText.IndexOf(entity.AssetName, StringComparison.OrdinalIgnoreCase) >= 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: heuristic string matching
|
||||
var keywords = new[] { "MCV", "基地车", "Nanocore", "纳米核心", "builder", "建造者" };
|
||||
return keywords.Any(kw => claimText.IndexOf(kw, StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,27 @@ namespace AnotherReplayReader.Utils
|
||||
AiPromptSettings? promptSettings = null)
|
||||
{
|
||||
var defaultPrompt = BuildDefaultSystemPrompt(replay, players);
|
||||
|
||||
// Try loading from knowledge_{mod}.md file (generated by tools/expand_knowledge.py).
|
||||
// If the file exists, use it instead of the built-in string.
|
||||
var modName = replay.Mod.ModName?.ToLowerInvariant();
|
||||
var knowledgeModName = modName switch
|
||||
{
|
||||
"corona" => "corona",
|
||||
_ => "default",
|
||||
};
|
||||
var knowledgePath = Path.Combine(AppContext.BaseDirectory, $"knowledge_{knowledgeModName}.md");
|
||||
if (File.Exists(knowledgePath))
|
||||
{
|
||||
var factionNames = players
|
||||
.Select(kv => ModData.GetFaction(replay.Mod, kv.Value.FactionId).Name)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
var mapId = Path.GetFileNameWithoutExtension(replay.MapPath);
|
||||
var knowledge = KnowledgeSet.ForMod(knowledgeModName, AppContext.BaseDirectory);
|
||||
defaultPrompt = knowledge.RenderAsPrompt(factionNames, mapId);
|
||||
}
|
||||
|
||||
return ComposeSystemPrompt(defaultPrompt, promptSettings);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,582 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace AnotherReplayReader.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// Predefined tag taxonomy for KnowledgeEntry.
|
||||
/// Tags bridge prompt rendering and validation logic.
|
||||
/// </summary>
|
||||
internal static class KnowledgeTag
|
||||
{
|
||||
// ── Capability tags (what a unit can do) ──────────────────────
|
||||
public const string Builder = "builder";
|
||||
public const string Pack = "pack";
|
||||
public const string Unpack = "unpack";
|
||||
public const string Amphibious = "amphibious";
|
||||
public const string Transport = "transport";
|
||||
public const string ReturnToProducer = "returnToProducer";
|
||||
public const string Cloak = "cloak";
|
||||
public const string ToggleWeapon = "toggleWeapon";
|
||||
|
||||
// ── Type tags (what a unit is) ────────────────────────────────
|
||||
public const string Infantry = "infantry";
|
||||
public const string Vehicle = "vehicle";
|
||||
public const string Aircraft = "aircraft";
|
||||
public const string Naval = "naval";
|
||||
public const string Structure = "structure";
|
||||
public const string Hero = "hero";
|
||||
public const string Production = "production";
|
||||
public const string Defense = "defense";
|
||||
public const string Superweapon = "superweapon";
|
||||
|
||||
// ── Combat role tags (what a unit fights) ─────────────────────
|
||||
public const string AntiInfantry = "antiInfantry";
|
||||
public const string AntiVehicle = "antiVehicle";
|
||||
public const string AntiStructure = "antiStructure";
|
||||
public const string AntiAir = "antiAir";
|
||||
public const string AntiNaval = "antiNaval";
|
||||
|
||||
/// <summary>Create a special power reference tag.</summary>
|
||||
public static string SpecialPower(string powerName) => $"specialPower:{powerName}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The scope kind of a knowledge entry within a KnowledgeSet.
|
||||
/// Scope is determined by the entry's position in the hierarchy,
|
||||
/// not stored in the entry itself.
|
||||
/// </summary>
|
||||
internal enum KnowledgeScopeKind
|
||||
{
|
||||
Global,
|
||||
Faction,
|
||||
Map
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Identifies which scope a knowledge entry belongs to.
|
||||
/// </summary>
|
||||
internal sealed record KnowledgeScope(KnowledgeScopeKind Kind, string? Name = null)
|
||||
{
|
||||
public static KnowledgeScope Global { get; } = new(KnowledgeScopeKind.Global);
|
||||
public static KnowledgeScope Faction(string name) => new(KnowledgeScopeKind.Faction, name);
|
||||
public static KnowledgeScope Map(string id) => new(KnowledgeScopeKind.Map, id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The smallest reusable unit of game knowledge.
|
||||
/// Id is unique within a knowledge set; tags enable validation queries;
|
||||
/// text is the markdown description used for prompt rendering.
|
||||
/// </summary>
|
||||
internal sealed record KnowledgeEntry(
|
||||
string Id,
|
||||
ImmutableArray<string> Tags,
|
||||
string Text)
|
||||
{
|
||||
public bool HasTag(string tag) => Tags.Contains(tag, StringComparer.OrdinalIgnoreCase);
|
||||
public bool HasAnyTag(params string[] tags) => tags.Any(HasTag);
|
||||
}
|
||||
|
||||
// ── Structured game entity knowledge ───────────────────────────────
|
||||
|
||||
/// <summary>A special power with its observable name and description.</summary>
|
||||
internal sealed record SpecialPowerInfo(string Name, string Description);
|
||||
|
||||
/// <summary>
|
||||
/// Structured knowledge about a game entity (unit or building).
|
||||
/// Buildings omit <see cref="Tier"/> and <see cref="ProducedBy"/>;
|
||||
/// the <c>structure</c> tag distinguishes them from units.
|
||||
/// </summary>
|
||||
internal sealed record EntityKnowledge(
|
||||
string AssetName,
|
||||
string DisplayName,
|
||||
string Faction,
|
||||
string? Tier,
|
||||
ImmutableArray<string> Tags,
|
||||
ImmutableArray<SpecialPowerInfo> SpecialPowers,
|
||||
ImmutableArray<string> ProducedBy,
|
||||
string Text)
|
||||
{
|
||||
public bool IsBuilding => HasTag(KnowledgeTag.Structure);
|
||||
public bool IsUnit => !IsBuilding;
|
||||
public bool HasTag(string tag) => Tags.Contains(tag, StringComparer.OrdinalIgnoreCase);
|
||||
public bool HasSpecialPower(string name) =>
|
||||
SpecialPowers.Any(sp => string.Equals(sp.Name, name, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// In-memory index of structured game knowledge loaded from knowledge_units.json.
|
||||
/// Used by validation to query unit capabilities deterministically.
|
||||
/// </summary>
|
||||
internal sealed class StructuredKnowledge
|
||||
{
|
||||
public ImmutableDictionary<string, EntityKnowledge> EntitiesByAssetName { get; }
|
||||
public ImmutableArray<EntityKnowledge> AllEntities { get; }
|
||||
public ImmutableDictionary<string, ImmutableArray<EntityKnowledge>> EntitiesByFaction { get; }
|
||||
|
||||
private StructuredKnowledge(
|
||||
ImmutableDictionary<string, EntityKnowledge> byAsset,
|
||||
ImmutableArray<EntityKnowledge> allEntities,
|
||||
ImmutableDictionary<string, ImmutableArray<EntityKnowledge>> byFaction)
|
||||
{
|
||||
EntitiesByAssetName = byAsset;
|
||||
AllEntities = allEntities;
|
||||
EntitiesByFaction = byFaction;
|
||||
}
|
||||
|
||||
// ── Query helpers ────────────────────────────────────────────
|
||||
|
||||
public ImmutableArray<EntityKnowledge> EntitiesWithTag(string tag) =>
|
||||
AllEntities.Where(e => e.HasTag(tag)).ToImmutableArray();
|
||||
|
||||
public ImmutableArray<EntityKnowledge> EntitiesWithSpecialPower(string power) =>
|
||||
AllEntities.Where(e => e.HasSpecialPower(power)).ToImmutableArray();
|
||||
|
||||
public EntityKnowledge? GetEntity(string? assetName) =>
|
||||
assetName is not null && EntitiesByAssetName.TryGetValue(assetName, out var e) ? e : null;
|
||||
|
||||
public bool IsKnownBuilder(string? assetName) =>
|
||||
GetEntity(assetName)?.HasTag(KnowledgeTag.Builder) == true;
|
||||
|
||||
public bool EntityHasSpecialPower(string? assetName, string? powerName) =>
|
||||
assetName is not null && powerName is not null &&
|
||||
GetEntity(assetName)?.HasSpecialPower(powerName) == true;
|
||||
|
||||
// ── Factory ──────────────────────────────────────────────────
|
||||
|
||||
private static readonly Lazy<StructuredKnowledge?> _lazyInstance = new(() => LoadFromFile());
|
||||
|
||||
public static StructuredKnowledge? Instance => _lazyInstance.Value;
|
||||
|
||||
/// <summary>Look up the display name for an asset name.</summary>
|
||||
public string? GetDisplayName(string? assetName) =>
|
||||
GetEntity(assetName)?.DisplayName;
|
||||
|
||||
private static StructuredKnowledge? LoadFromFile()
|
||||
{
|
||||
var path = Path.Combine(AppContext.BaseDirectory, "knowledge_units.json");
|
||||
if (!File.Exists(path)) return null;
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(path, Encoding.UTF8);
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
|
||||
if (!root.TryGetProperty("factions", out var factions))
|
||||
return null;
|
||||
|
||||
var allEntities = new List<EntityKnowledge>();
|
||||
var byFaction = new Dictionary<string, List<EntityKnowledge>>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var faction in factions.EnumerateObject())
|
||||
{
|
||||
var factionName = faction.Name;
|
||||
if (!byFaction.ContainsKey(factionName))
|
||||
byFaction[factionName] = new List<EntityKnowledge>();
|
||||
|
||||
// Buildings
|
||||
if (faction.Value.TryGetProperty("buildings", out var bldgs))
|
||||
{
|
||||
foreach (var b in bldgs.EnumerateArray())
|
||||
{
|
||||
var ek = new EntityKnowledge(
|
||||
GetString(b, "assetName") ?? "unknown",
|
||||
GetString(b, "displayName") ?? "",
|
||||
factionName,
|
||||
Tier: null,
|
||||
GetStringArray(b, "tags"),
|
||||
ParseSpecialPowers(b),
|
||||
ProducedBy: ImmutableArray<string>.Empty,
|
||||
GetString(b, "text") ?? "");
|
||||
allEntities.Add(ek);
|
||||
byFaction[factionName].Add(ek);
|
||||
}
|
||||
}
|
||||
|
||||
// Units
|
||||
if (faction.Value.TryGetProperty("units", out var units))
|
||||
{
|
||||
foreach (var u in units.EnumerateArray())
|
||||
{
|
||||
var assetName = GetString(u, "assetName") ?? "unknown";
|
||||
var ek = new EntityKnowledge(
|
||||
assetName,
|
||||
GetString(u, "displayName") ?? "",
|
||||
factionName,
|
||||
GetString(u, "tier"),
|
||||
GetStringArray(u, "tags"),
|
||||
ParseSpecialPowers(u),
|
||||
GetStringArray(u, "producedBy"),
|
||||
GetString(u, "text") ?? "");
|
||||
allEntities.Add(ek);
|
||||
byFaction[factionName].Add(ek);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new StructuredKnowledge(
|
||||
allEntities.ToImmutableDictionary(e => e.AssetName, e => e, StringComparer.OrdinalIgnoreCase),
|
||||
allEntities.ToImmutableArray(),
|
||||
byFaction.ToImmutableDictionary(
|
||||
kv => kv.Key,
|
||||
kv => kv.Value.ToImmutableArray(),
|
||||
StringComparer.OrdinalIgnoreCase));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[AiKnowledge] Failed to load knowledge_units.json: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? GetString(JsonElement el, string prop) =>
|
||||
el.TryGetProperty(prop, out var v) && v.ValueKind == JsonValueKind.String
|
||||
? v.GetString()
|
||||
: null;
|
||||
|
||||
private static ImmutableArray<string> GetStringArray(JsonElement el, string prop)
|
||||
{
|
||||
if (!el.TryGetProperty(prop, out var arr) || arr.ValueKind != JsonValueKind.Array)
|
||||
return ImmutableArray<string>.Empty;
|
||||
var result = new List<string>();
|
||||
foreach (var item in arr.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind == JsonValueKind.String && item.GetString() is { } s)
|
||||
result.Add(s);
|
||||
}
|
||||
return result.ToImmutableArray();
|
||||
}
|
||||
|
||||
private static ImmutableArray<SpecialPowerInfo> ParseSpecialPowers(JsonElement el)
|
||||
{
|
||||
if (!el.TryGetProperty("specialPowers", out var arr) || arr.ValueKind != JsonValueKind.Array)
|
||||
return ImmutableArray<SpecialPowerInfo>.Empty;
|
||||
var result = new List<SpecialPowerInfo>();
|
||||
foreach (var item in arr.EnumerateArray())
|
||||
{
|
||||
if (item.ValueKind != JsonValueKind.Object) continue;
|
||||
var name = GetString(item, "name") ?? "";
|
||||
var desc = GetString(item, "description") ?? "";
|
||||
if (!string.IsNullOrWhiteSpace(name))
|
||||
result.Add(new SpecialPowerInfo(name, desc));
|
||||
}
|
||||
return result.ToImmutableArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A named collection of game knowledge entries for a specific game version (mod).
|
||||
/// Entries are organized by scope; the set is self-contained and complete for its mod.
|
||||
/// </summary>
|
||||
internal sealed class KnowledgeSet
|
||||
{
|
||||
private readonly ImmutableArray<(KnowledgeScope Scope, KnowledgeEntry Entry)> _entries;
|
||||
|
||||
public KnowledgeSet(IEnumerable<(KnowledgeScope Scope, KnowledgeEntry Entry)> entries)
|
||||
{
|
||||
_entries = entries.ToImmutableArray();
|
||||
}
|
||||
|
||||
// ── Query helpers ────────────────────────────────────────────
|
||||
|
||||
public ImmutableArray<KnowledgeEntry> ByScope(KnowledgeScopeKind kind, string? name = null) =>
|
||||
_entries
|
||||
.Where(e => e.Scope.Kind == kind
|
||||
&& (name is null || string.Equals(e.Scope.Name, name, StringComparison.OrdinalIgnoreCase)))
|
||||
.Select(e => e.Entry)
|
||||
.ToImmutableArray();
|
||||
|
||||
public ImmutableArray<KnowledgeEntry> ByTag(string tag) =>
|
||||
_entries
|
||||
.Where(e => e.Entry.HasTag(tag))
|
||||
.Select(e => e.Entry)
|
||||
.ToImmutableArray();
|
||||
|
||||
public ImmutableArray<KnowledgeEntry> ByAnyTag(params string[] tags) =>
|
||||
_entries
|
||||
.Where(e => e.Entry.HasAnyTag(tags))
|
||||
.Select(e => e.Entry)
|
||||
.ToImmutableArray();
|
||||
|
||||
// ── Prompt rendering ─────────────────────────────────────────
|
||||
|
||||
public string RenderAsPrompt(IReadOnlyList<string> factionNames, string? mapId)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
|
||||
foreach (var entry in ByScope(KnowledgeScopeKind.Global))
|
||||
{
|
||||
sb.AppendLine(entry.Text.Trim());
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
foreach (var faction in factionNames)
|
||||
{
|
||||
foreach (var entry in ByScope(KnowledgeScopeKind.Faction, faction))
|
||||
{
|
||||
sb.AppendLine(entry.Text.Trim());
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
if (mapId is not null)
|
||||
{
|
||||
foreach (var entry in ByScope(KnowledgeScopeKind.Map, mapId))
|
||||
{
|
||||
sb.AppendLine(entry.Text.Trim());
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
return sb.ToString().Replace("\r", "");
|
||||
}
|
||||
|
||||
// ── Built-in factory ─────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Create a built-in KnowledgeSet for a given mod name.
|
||||
/// Loads text from <c>knowledge_{modName}.md</c> and structured entries
|
||||
/// from <c>knowledge_units.json</c>. The flat text's unit/building
|
||||
/// sections are stripped and replaced by structured entries for rendering.
|
||||
/// </summary>
|
||||
public static KnowledgeSet ForMod(string modName, string? baseDirectory = null)
|
||||
{
|
||||
var searchDir = baseDirectory ?? AppContext.BaseDirectory;
|
||||
var entries = new List<(KnowledgeScope Scope, KnowledgeEntry Entry)>();
|
||||
var structured = StructuredKnowledge.Instance;
|
||||
|
||||
// 1. Load flat text from knowledge_{modName}.md
|
||||
var flatPath = Path.Combine(searchDir, $"knowledge_{modName}.md");
|
||||
string? flatText = null;
|
||||
if (File.Exists(flatPath))
|
||||
{
|
||||
flatText = File.ReadAllText(flatPath, Encoding.UTF8);
|
||||
}
|
||||
|
||||
// 2. If structured data is available, strip unit sections from flat text
|
||||
// and add structured entries as faction-scoped KnowledgeEntry.
|
||||
if (structured is not null && flatText is not null)
|
||||
{
|
||||
var cleaned = StripUnitSections(flatText, structured);
|
||||
entries.Add((KnowledgeScope.Global, new KnowledgeEntry(
|
||||
$"knowledge-text-{modName}",
|
||||
ImmutableArray.Create("rule"),
|
||||
cleaned)));
|
||||
|
||||
foreach (var kv in structured.EntitiesByFaction)
|
||||
{
|
||||
var factionName = kv.Key;
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"# {factionName}");
|
||||
|
||||
// Buildings (entities with structure tag)
|
||||
var bldgs = kv.Value
|
||||
.Where(e => e.IsBuilding)
|
||||
.ToImmutableArray();
|
||||
if (!bldgs.IsEmpty)
|
||||
{
|
||||
sb.AppendLine("## 建筑与升级");
|
||||
foreach (var b in bldgs)
|
||||
{
|
||||
sb.Append("- ");
|
||||
sb.Append(b.DisplayName);
|
||||
sb.Append('(');
|
||||
sb.Append(b.AssetName);
|
||||
sb.Append("):");
|
||||
sb.AppendLine(b.Text.Trim());
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
// Units (entities without structure tag) by tier
|
||||
var units = kv.Value
|
||||
.Where(e => e.IsUnit)
|
||||
.GroupBy(u => u.Tier ?? "")
|
||||
.OrderBy(g => TierOrder(g.Key));
|
||||
foreach (var tier in units)
|
||||
{
|
||||
var label = tier.Key switch
|
||||
{
|
||||
"基础" => "基础单位",
|
||||
"T2" => "T2 单位(需要T2升级)",
|
||||
"T3" => "T3 单位(需要T3升级)",
|
||||
"T4" => "T4 单位(需要T4升级)",
|
||||
_ => tier.Key,
|
||||
};
|
||||
sb.AppendLine($"## {label}");
|
||||
foreach (var unit in tier)
|
||||
{
|
||||
sb.Append("- ");
|
||||
sb.Append(unit.DisplayName);
|
||||
sb.Append('(');
|
||||
sb.Append(unit.AssetName);
|
||||
sb.AppendLine(")");
|
||||
|
||||
// Type info from tags
|
||||
var typeTags = unit.Tags
|
||||
.Where(t => t is "vehicle" or "infantry" or "aircraft" or "naval" or "structure" or "hero" or "amphibious")
|
||||
.Select(TagDisplayName);
|
||||
if (typeTags.Any())
|
||||
{
|
||||
sb.Append(" - 类型: ");
|
||||
sb.AppendLine(string.Join("、", typeTags));
|
||||
}
|
||||
|
||||
// Special powers
|
||||
if (!unit.SpecialPowers.IsEmpty)
|
||||
{
|
||||
foreach (var sp in unit.SpecialPowers)
|
||||
{
|
||||
sb.Append(" - 技能: ");
|
||||
sb.Append(sp.Name);
|
||||
if (!string.IsNullOrWhiteSpace(sp.Description))
|
||||
{
|
||||
sb.Append(" — ");
|
||||
sb.Append(sp.Description);
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
// Produced by
|
||||
if (!unit.ProducedBy.IsEmpty)
|
||||
{
|
||||
var producerNames = unit.ProducedBy
|
||||
.Select(name => structured.GetDisplayName(name) ?? name)
|
||||
.ToImmutableArray();
|
||||
sb.Append(" - 生产: ");
|
||||
sb.AppendLine(string.Join("、", producerNames));
|
||||
}
|
||||
|
||||
// Remaining description
|
||||
if (!string.IsNullOrWhiteSpace(unit.Text))
|
||||
{
|
||||
sb.Append(" - 描述: ");
|
||||
sb.AppendLine(unit.Text.Trim());
|
||||
}
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
entries.Add((KnowledgeScope.Faction(factionName), new KnowledgeEntry(
|
||||
$"structured-{factionName}",
|
||||
ImmutableArray.Create("faction", "structured"),
|
||||
sb.ToString().TrimEnd())));
|
||||
}
|
||||
}
|
||||
else if (flatText is not null)
|
||||
{
|
||||
entries.Add((KnowledgeScope.Global, new KnowledgeEntry(
|
||||
$"knowledge-file-{modName}",
|
||||
ImmutableArray.Create("rule"),
|
||||
flatText)));
|
||||
}
|
||||
else
|
||||
{
|
||||
entries.Add((KnowledgeScope.Global, new KnowledgeEntry(
|
||||
"knowledge-unavailable",
|
||||
ImmutableArray.Create("rule"),
|
||||
$"# 注意\n\n游戏知识文件 knowledge_{modName}.md 未找到。\n\n")));
|
||||
}
|
||||
|
||||
return new KnowledgeSet(entries);
|
||||
}
|
||||
|
||||
private static int TierOrder(string tier) => tier switch
|
||||
{
|
||||
"基础" => 0,
|
||||
"T2" => 1,
|
||||
"T3" => 2,
|
||||
"T4" => 3,
|
||||
_ => 99,
|
||||
};
|
||||
|
||||
private static string TagDisplayName(string tag) => tag switch
|
||||
{
|
||||
"vehicle" => "载具",
|
||||
"infantry" => "步兵",
|
||||
"aircraft" => "飞行器",
|
||||
"naval" => "海军",
|
||||
"structure" => "建筑",
|
||||
"hero" => "英雄",
|
||||
"amphibious" => "两栖",
|
||||
_ => tag,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Strip unit and building sections from flat text for factions that
|
||||
/// have structured data, to avoid duplication when rendering.
|
||||
/// </summary>
|
||||
private static string StripUnitSections(string flatText, StructuredKnowledge structured)
|
||||
{
|
||||
var stripMarkers = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var factionName in structured.EntitiesByFaction.Keys)
|
||||
{
|
||||
var (buildingHeader, nextHeader) = factionName switch
|
||||
{
|
||||
"盟军" => ("盟军常用建筑与升级", "盟军常用开局"),
|
||||
"神州" => ("神州常用建筑", "神州常用开局"),
|
||||
_ => ((string?)null, (string?)null),
|
||||
};
|
||||
|
||||
if (buildingHeader is not null && nextHeader is not null)
|
||||
{
|
||||
stripMarkers[buildingHeader] = nextHeader;
|
||||
}
|
||||
}
|
||||
|
||||
if (stripMarkers.Count == 0) return flatText;
|
||||
|
||||
var lines = flatText.Replace("\r", "").Split('\n');
|
||||
var result = new List<string>();
|
||||
var skipping = false;
|
||||
var currentStopMarker = (string?)null;
|
||||
|
||||
foreach (var line in lines)
|
||||
{
|
||||
var trimmed = line.TrimStart();
|
||||
|
||||
if (skipping)
|
||||
{
|
||||
if (currentStopMarker is not null &&
|
||||
trimmed.StartsWith(currentStopMarker, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
skipping = false;
|
||||
result.Add(line);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
var matched = stripMarkers.Keys.FirstOrDefault(
|
||||
m => trimmed.StartsWith(m, StringComparison.OrdinalIgnoreCase));
|
||||
if (matched is not null)
|
||||
{
|
||||
skipping = true;
|
||||
currentStopMarker = stripMarkers[matched];
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(line);
|
||||
}
|
||||
|
||||
return string.Join("\n", result);
|
||||
}
|
||||
|
||||
public static KnowledgeSet ForReplay(ReplayFile.Replay replay, string? baseDirectory = null)
|
||||
{
|
||||
var modName = replay.Mod.ModName?.ToLowerInvariant() switch
|
||||
{
|
||||
"corona" => "corona",
|
||||
_ => "default",
|
||||
};
|
||||
return ForMod(modName, baseDirectory);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user