using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
namespace AnotherReplayReader.Utils
{
///
/// Predefined tag taxonomy for KnowledgeEntry.
/// Tags bridge prompt rendering and validation logic.
///
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";
public const string Land = "land";
public const string Sea = "sea";
public const string Air = "air";
// ── 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";
public const string AntiGround = "antiGround";
// ── 其余 JSON 实际使用的角色/定位标签 ──────────────────────────
public const string Miner = "miner";
public const string Scout = "scout";
public const string Support = "support";
public const string Siege = "siege";
public const string Bomber = "bomber";
public const string Engineer = "engineer";
public const string Fighter = "fighter";
/// 完整标签集合:加载 JSON 时校验未知 tag 用。
public static readonly ImmutableArray All = ImmutableArray.Create(
Builder, Pack, Unpack, Amphibious, Transport, ReturnToProducer, Cloak, ToggleWeapon,
Miner, Scout, Support, Siege, Bomber,
Infantry, Vehicle, Aircraft, Naval, Structure, Hero, Production, Defense, Superweapon,
Land, Sea, Air,
AntiInfantry, AntiVehicle, AntiStructure, AntiAir, AntiNaval, AntiGround,
Engineer, Fighter);
/// Create a special power reference tag.
public static string SpecialPower(string powerName) => $"specialPower:{powerName}";
}
///
/// 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.
///
internal enum KnowledgeScopeKind
{
Global,
Faction,
Map
}
///
/// Identifies which scope a knowledge entry belongs to.
///
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);
}
///
/// 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.
///
internal sealed record KnowledgeEntry(
string Id,
ImmutableArray 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 ───────────────────────────────
/// A special power with its observable name and description.
internal sealed record SpecialPowerInfo(string Name, string Description);
///
/// Structured knowledge about a game entity (unit or building).
/// Buildings omit and ;
/// the structure tag distinguishes them from units.
///
internal sealed record EntityKnowledge(
string AssetName,
string DisplayName,
string Faction,
string? Tier,
ImmutableArray Tags,
ImmutableArray SpecialPowers,
ImmutableArray ProducedBy,
ImmutableArray Aliases,
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));
}
///
/// In-memory index of structured game knowledge loaded from knowledge_units.json.
/// Used by validation to query unit capabilities deterministically.
///
internal sealed class StructuredKnowledge
{
public ImmutableDictionary EntitiesByAssetName { get; }
public ImmutableArray AllEntities { get; }
public ImmutableDictionary> EntitiesByFaction { get; }
public ImmutableArray UnknownTags { get; }
private StructuredKnowledge(
ImmutableDictionary byAsset,
ImmutableArray allEntities,
ImmutableDictionary> byFaction,
ImmutableArray unknownTags)
{
EntitiesByAssetName = byAsset;
AllEntities = allEntities;
EntitiesByFaction = byFaction;
UnknownTags = unknownTags;
}
// ── Query helpers ────────────────────────────────────────────
public ImmutableArray EntitiesWithTag(string tag) =>
AllEntities.Where(e => e.HasTag(tag)).ToImmutableArray();
public ImmutableArray 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 _lazyDefault = new(() => GetForMod("default"));
public static StructuredKnowledge? Instance => _lazyDefault.Value;
private static readonly ConcurrentDictionary _cache =
new(StringComparer.OrdinalIgnoreCase);
/// 按 mod 加载结构化知识;文件不存在返回 null(该 mod 无结构化数据)。
public static StructuredKnowledge? GetForMod(string? modName)
{
var key = modName ?? "default";
if (_cache.TryGetValue(key, out var cached))
{
return cached;
}
var loaded = LoadFromFile(key);
if (loaded is not null)
{
_cache.TryAdd(key, loaded);
}
return loaded;
}
/// Look up the display name for an asset name.
public string? GetDisplayName(string? assetName) =>
GetEntity(assetName)?.DisplayName;
private static StructuredKnowledge? LoadFromFile(string modName)
{
var path = Path.Combine(AppContext.BaseDirectory, $"knowledge_units_{modName}.json");
if (!File.Exists(path)) return null;
try
{
var builtin = ParseFactions(path, out var unknownTags);
if (builtin is null)
{
return null;
}
// 用户知识覆盖:AnotherReplayReader.user_knowledge.json,按 (阵营, assetName) 覆盖或新增
var userPath = Path.Combine(AppContext.BaseDirectory, "AnotherReplayReader.user_knowledge.json");
if (File.Exists(userPath))
{
var user = ParseFactions(userPath, out var userUnknownTags);
if (user is not null)
{
foreach (var kv in user)
{
if (!builtin.TryGetValue(kv.Key, out var factionEntries))
{
factionEntries = new Dictionary(StringComparer.OrdinalIgnoreCase);
builtin[kv.Key] = factionEntries;
}
foreach (var entity in kv.Value)
{
factionEntries[entity.Key] = entity.Value;
}
unknownTags.UnionWith(userUnknownTags);
}
}
}
var allEntities = builtin.Values
.SelectMany(d => d.Values)
.OrderBy(e => e.Faction, StringComparer.OrdinalIgnoreCase)
.ThenBy(e => e.AssetName, StringComparer.OrdinalIgnoreCase)
.ToImmutableArray();
var byAsset = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (var entity in allEntities)
{
byAsset[entity.AssetName] = entity;
foreach (var alias in entity.Aliases)
{
byAsset[alias] = entity;
}
}
var byFaction = builtin.ToImmutableDictionary(
kv => kv.Key,
kv => kv.Value.Values.OrderBy(e => e.AssetName, StringComparer.OrdinalIgnoreCase).ToImmutableArray(),
StringComparer.OrdinalIgnoreCase);
var unknownTagsArray = unknownTags.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToImmutableArray();
if (!unknownTagsArray.IsEmpty)
{
System.Diagnostics.Debug.WriteLine(
$"[AiKnowledge] 未知标签({modName}):{string.Join(", ", unknownTagsArray)}");
}
return new StructuredKnowledge(
byAsset.ToImmutableDictionary(StringComparer.OrdinalIgnoreCase),
allEntities,
byFaction,
unknownTagsArray);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"[AiKnowledge] Failed to load knowledge_units_{modName}.json: {ex.Message}");
return null;
}
}
///
/// 解析知识 JSON 的 factions 结构,返回 faction → (assetName → EntityKnowledge);
/// 同时收集未知标签。
///
private static Dictionary>? ParseFactions(
string path,
out HashSet unknownTags)
{
var unknownTagsLocal = new HashSet(StringComparer.OrdinalIgnoreCase);
var json = File.ReadAllText(path, Encoding.UTF8);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
if (!root.TryGetProperty("factions", out var factions))
{
unknownTags = unknownTagsLocal;
return null;
}
var result = new Dictionary>(StringComparer.OrdinalIgnoreCase);
foreach (var faction in factions.EnumerateObject())
{
var factionName = faction.Name;
if (!result.TryGetValue(factionName, out var entries))
{
entries = new Dictionary(StringComparer.OrdinalIgnoreCase);
result[factionName] = entries;
}
void AddEntity(JsonElement el, bool isBuilding)
{
var assetName = GetString(el, "assetName") ?? "unknown";
var tags = GetStringArray(el, "tags");
foreach (var tag in tags)
{
if (!KnowledgeTag.All.Contains(tag, StringComparer.OrdinalIgnoreCase))
{
unknownTagsLocal.Add(tag);
}
}
var producedBy = GetStringArray(el, "producedBy");
var alsoProducedBy = GetStringArray(el, "alsoProducedBy");
var combinedProducedBy = producedBy
.Concat(alsoProducedBy)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToImmutableArray();
var ek = new EntityKnowledge(
assetName,
GetString(el, "displayName") ?? "",
factionName,
isBuilding ? null : GetString(el, "tier"),
tags,
ParseSpecialPowers(el),
isBuilding ? ImmutableArray.Empty : combinedProducedBy,
GetStringArray(el, "aliases"),
GetString(el, "text") ?? "");
entries[assetName] = ek;
}
if (faction.Value.TryGetProperty("buildings", out var bldgs))
{
foreach (var b in bldgs.EnumerateArray())
{
AddEntity(b, isBuilding: true);
}
}
if (faction.Value.TryGetProperty("units", out var units))
{
foreach (var u in units.EnumerateArray())
{
AddEntity(u, isBuilding: false);
}
}
}
unknownTags = unknownTagsLocal;
return result;
}
private static string? GetString(JsonElement el, string prop) =>
el.TryGetProperty(prop, out var v) && v.ValueKind == JsonValueKind.String
? v.GetString()
: null;
private static ImmutableArray GetStringArray(JsonElement el, string prop)
{
if (!el.TryGetProperty(prop, out var arr) || arr.ValueKind != JsonValueKind.Array)
return ImmutableArray.Empty;
var result = new List();
foreach (var item in arr.EnumerateArray())
{
if (item.ValueKind == JsonValueKind.String && item.GetString() is { } s)
result.Add(s);
}
return result.ToImmutableArray();
}
private static ImmutableArray ParseSpecialPowers(JsonElement el)
{
if (!el.TryGetProperty("specialPowers", out var arr) || arr.ValueKind != JsonValueKind.Array)
return ImmutableArray.Empty;
var result = new List();
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();
}
}
///
/// 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.
///
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 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 ByTag(string tag) =>
_entries
.Where(e => e.Entry.HasTag(tag))
.Select(e => e.Entry)
.ToImmutableArray();
public ImmutableArray ByAnyTag(params string[] tags) =>
_entries
.Where(e => e.Entry.HasAnyTag(tags))
.Select(e => e.Entry)
.ToImmutableArray();
// ── Prompt rendering ─────────────────────────────────────────
public string RenderAsPrompt(IReadOnlyList factionNames, string? mapId)
{
var sb = new StringBuilder();
foreach (var entry in ByScope(KnowledgeScopeKind.Global))
{
var text = entry.Id.StartsWith("knowledge-text-", StringComparison.Ordinal)
? FilterFlatTextByFactions(entry.Text, factionNames)
: entry.Text;
sb.AppendLine(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", "");
}
private static readonly (string Faction, string StartMarker)[] FactionSectionMarkers =
{
("盟军", "盟军常用建筑与升级"),
("神州", "神州常用建筑"),
};
/// flat 文本按参战阵营过滤:只保留全局部分与参战阵营的章节,减少 token 浪费。
private static string FilterFlatTextByFactions(string text, IReadOnlyList factionNames)
{
if (factionNames.Count == 0)
{
return text;
}
var participating = new HashSet(factionNames, StringComparer.OrdinalIgnoreCase);
var lines = text.Replace("\r", "").Split('\n');
var sb = new StringBuilder();
string? currentFaction = null;
foreach (var raw in lines)
{
var line = raw.TrimStart();
var matched = FactionSectionMarkers.FirstOrDefault(
m => line.StartsWith(m.StartMarker, StringComparison.OrdinalIgnoreCase));
if (matched.Faction is not null)
{
currentFaction = matched.Faction;
if (participating.Contains(currentFaction))
{
sb.AppendLine(raw);
}
continue;
}
if (line.StartsWith("# 地图参数", StringComparison.OrdinalIgnoreCase))
{
currentFaction = null;
}
if (currentFaction is null || participating.Contains(currentFaction))
{
sb.AppendLine(raw);
}
}
return sb.ToString().TrimEnd();
}
// ── Built-in factory ─────────────────────────────────────────
///
/// Create a built-in KnowledgeSet for a given mod name.
/// Loads text from knowledge_{modName}.md and structured entries
/// from knowledge_units.json. The flat text's unit/building
/// sections are stripped and replaced by structured entries for rendering.
///
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.GetForMod(modName);
// 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,
};
///
/// Strip unit and building sections from flat text for factions that
/// have structured data, to avoid duplication when rendering.
///
private static string StripUnitSections(string flatText, StructuredKnowledge structured)
{
var stripMarkers = new Dictionary(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();
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);
}
}
}