Files
Tornado3_2026Election/Tornado3_2026Election/Services/KarismaSceneVariableCatalog.cs
2026-04-22 13:30:24 +09:00

383 lines
13 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace Tornado3_2026Election.Services;
public sealed class KarismaSceneVariableCatalog
{
private static readonly string[] PreferredReportNames =
[
"TSCN_VARIABLE_DISCOVERY_E_DRIVE.md",
"TSCN_VARIABLE_DISCOVERY_ELECT2026_NORMAL.md",
"TSCN_VARIABLE_DISCOVERY_ONE.md",
"TSCN_VARIABLE_DISCOVERY.md"
];
private static readonly IReadOnlyDictionary<string, KarismaSceneVariableDefinition> EmptySceneVariables =
new Dictionary<string, KarismaSceneVariableDefinition>(StringComparer.OrdinalIgnoreCase);
private readonly IReadOnlyDictionary<string, IReadOnlyDictionary<string, KarismaSceneVariableDefinition>> _scenes;
private KarismaSceneVariableCatalog(
IReadOnlyDictionary<string, IReadOnlyDictionary<string, KarismaSceneVariableDefinition>> scenes)
{
_scenes = scenes;
}
public static KarismaSceneVariableCatalog Load(LogService logService)
{
var reportPaths = FindDiscoveryReportPaths().ToArray();
if (reportPaths.Length == 0)
{
logService.Warning("Karisma scene variable catalog report was not found. Falling back to runtime value heuristics.");
return new KarismaSceneVariableCatalog(
new Dictionary<string, IReadOnlyDictionary<string, KarismaSceneVariableDefinition>>(StringComparer.OrdinalIgnoreCase));
}
try
{
var mergedScenes = new Dictionary<string, Dictionary<string, KarismaSceneVariableDefinition>>(StringComparer.OrdinalIgnoreCase);
foreach (var reportPath in reportPaths)
{
MergeScenes(mergedScenes, ParseReport(reportPath));
}
var scenes = mergedScenes.ToDictionary(
pair => pair.Key,
pair => (IReadOnlyDictionary<string, KarismaSceneVariableDefinition>)pair.Value,
StringComparer.OrdinalIgnoreCase);
logService.Info($"Karisma scene variable catalog loaded: scenes={scenes.Count} sources={reportPaths.Length}.");
return new KarismaSceneVariableCatalog(scenes);
}
catch (Exception ex)
{
logService.Warning($"Failed to load Karisma scene variable catalog: {ex.Message}");
return new KarismaSceneVariableCatalog(
new Dictionary<string, IReadOnlyDictionary<string, KarismaSceneVariableDefinition>>(StringComparer.OrdinalIgnoreCase));
}
}
public IReadOnlyDictionary<string, KarismaSceneVariableDefinition> GetSceneVariables(string t3CutPath, string scenePath)
{
if (_scenes.Count == 0 ||
string.IsNullOrWhiteSpace(t3CutPath) ||
string.IsNullOrWhiteSpace(scenePath))
{
return EmptySceneVariables;
}
var relativePath = NormalizeRelativePath(Path.GetRelativePath(t3CutPath, scenePath));
if (_scenes.TryGetValue(relativePath, out var variables))
{
return variables;
}
var fileName = Path.GetFileName(relativePath);
if (!string.IsNullOrWhiteSpace(fileName))
{
var fileNameMatches = _scenes
.Where(pair => string.Equals(Path.GetFileName(pair.Key), fileName, StringComparison.OrdinalIgnoreCase))
.Take(2)
.ToArray();
if (fileNameMatches.Length == 1)
{
return fileNameMatches[0].Value;
}
}
return EmptySceneVariables;
}
private static IReadOnlyDictionary<string, IReadOnlyDictionary<string, KarismaSceneVariableDefinition>> ParseReport(string reportPath)
{
var scenes = new Dictionary<string, Dictionary<string, KarismaSceneVariableDefinition>>(StringComparer.OrdinalIgnoreCase);
string? currentScene = null;
string? reportRootRelativePath = null;
foreach (var rawLine in File.ReadLines(reportPath, Encoding.UTF8))
{
var line = rawLine.Trim();
if (TryParseReportRoot(line, out var reportRoot))
{
reportRootRelativePath = NormalizeReportRootRelativePath(reportRoot);
continue;
}
if (TryParseSceneHeader(line, out var sceneRelativePath))
{
currentScene = NormalizeSceneKey(reportRootRelativePath, sceneRelativePath);
if (!scenes.ContainsKey(currentScene))
{
scenes[currentScene] = new Dictionary<string, KarismaSceneVariableDefinition>(StringComparer.OrdinalIgnoreCase);
}
continue;
}
if (string.IsNullOrWhiteSpace(currentScene) || !line.StartsWith('|'))
{
continue;
}
var cells = SplitMarkdownRow(line);
if (cells.Count < 4 ||
string.Equals(cells[0], "Variable", StringComparison.OrdinalIgnoreCase) ||
string.Equals(cells[0], "---", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var variableName = cells[0];
if (string.IsNullOrWhiteSpace(variableName))
{
continue;
}
var method = cells[1];
var payload = cells[2];
var result = cells[3];
if (!string.Equals(result, "RESULT_SUCCESS", StringComparison.OrdinalIgnoreCase))
{
continue;
}
scenes[currentScene][variableName] = new KarismaSceneVariableDefinition(
variableName,
ResolveKind(variableName, method, payload),
method,
payload);
}
return scenes.ToDictionary(
pair => pair.Key,
pair => (IReadOnlyDictionary<string, KarismaSceneVariableDefinition>)pair.Value,
StringComparer.OrdinalIgnoreCase);
}
private static void MergeScenes(
IDictionary<string, Dictionary<string, KarismaSceneVariableDefinition>> target,
IReadOnlyDictionary<string, IReadOnlyDictionary<string, KarismaSceneVariableDefinition>> source)
{
foreach (var (scenePath, variables) in source)
{
if (!target.TryGetValue(scenePath, out var mergedVariables))
{
mergedVariables = new Dictionary<string, KarismaSceneVariableDefinition>(StringComparer.OrdinalIgnoreCase);
target[scenePath] = mergedVariables;
}
foreach (var (variableName, definition) in variables)
{
mergedVariables[variableName] = definition;
}
}
}
private static bool TryParseSceneHeader(string line, out string sceneRelativePath)
{
sceneRelativePath = string.Empty;
if (!line.StartsWith("### `", StringComparison.Ordinal) || !line.EndsWith('`'))
{
return false;
}
sceneRelativePath = line.Substring(5, line.Length - 6);
return !string.IsNullOrWhiteSpace(sceneRelativePath);
}
private static bool TryParseReportRoot(string line, out string reportRoot)
{
reportRoot = string.Empty;
const string prefix = "- Root: `";
if (!line.StartsWith(prefix, StringComparison.Ordinal) || !line.EndsWith('`'))
{
return false;
}
reportRoot = line.Substring(prefix.Length, line.Length - prefix.Length - 1);
return !string.IsNullOrWhiteSpace(reportRoot);
}
private static List<string> SplitMarkdownRow(string line)
{
var cells = line.Split('|');
if (cells.Length <= 2)
{
return [];
}
return cells
.Skip(1)
.Take(cells.Length - 2)
.Select(cell => cell.Trim())
.ToList();
}
private static KarismaSceneVariableKind ResolveKind(string variableName, string method, string payload)
{
if (string.Equals(method, "SetCounterNumberKey", StringComparison.OrdinalIgnoreCase))
{
return KarismaSceneVariableKind.Counter;
}
if (IsLikelyCounterVariableName(variableName))
{
return KarismaSceneVariableKind.Counter;
}
if (variableName.StartsWith("\uC720\uD655\uB2F9", StringComparison.OrdinalIgnoreCase))
{
return KarismaSceneVariableKind.VideoResource;
}
if (payload.EndsWith(".vrv", StringComparison.OrdinalIgnoreCase))
{
return KarismaSceneVariableKind.VideoResource;
}
if (payload.EndsWith(".png", StringComparison.OrdinalIgnoreCase) ||
payload.EndsWith(".jpg", StringComparison.OrdinalIgnoreCase) ||
payload.EndsWith(".jpeg", StringComparison.OrdinalIgnoreCase) ||
payload.EndsWith(".webp", StringComparison.OrdinalIgnoreCase))
{
return KarismaSceneVariableKind.Image;
}
return KarismaSceneVariableKind.Text;
}
private static bool IsLikelyCounterVariableName(string variableName)
{
return variableName.StartsWith("득표율", StringComparison.Ordinal) ||
variableName.StartsWith("투표율", StringComparison.Ordinal) ||
variableName.StartsWith("전국투표율", StringComparison.Ordinal);
}
private static IEnumerable<string> FindDiscoveryReportPaths()
{
var seenPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var reportPaths = new List<string>();
foreach (var startPath in EnumerateSearchRoots())
{
var current = startPath;
for (var depth = 0; depth < 8 && !string.IsNullOrWhiteSpace(current); depth++)
{
foreach (var reportName in PreferredReportNames)
{
var candidate = Path.Combine(current, reportName);
if (File.Exists(candidate) && seenPaths.Add(candidate))
{
reportPaths.Add(candidate);
}
}
foreach (var wildcardCandidate in TryFindDiscoveryReports(current))
{
if (seenPaths.Add(wildcardCandidate))
{
reportPaths.Add(wildcardCandidate);
}
}
current = Path.GetDirectoryName(current);
}
}
return reportPaths;
}
private static IEnumerable<string> EnumerateSearchRoots()
{
var roots = new List<string> { AppContext.BaseDirectory };
try
{
roots.Add(Directory.GetCurrentDirectory());
}
catch
{
}
return roots;
}
private static IEnumerable<string> TryFindDiscoveryReports(string directoryPath)
{
try
{
return Directory.EnumerateFiles(directoryPath, "TSCN_VARIABLE_DISCOVERY*.md", SearchOption.TopDirectoryOnly)
.Where(path => !Path.GetFileName(path).Contains("SAMPLE", StringComparison.OrdinalIgnoreCase))
.OrderByDescending(path => File.GetLastWriteTimeUtc(path))
.ThenBy(path => path, StringComparer.OrdinalIgnoreCase)
.ToArray();
}
catch
{
return [];
}
}
private static string NormalizeSceneKey(string? reportRootRelativePath, string sceneRelativePath)
{
var normalizedScenePath = NormalizeRelativePath(sceneRelativePath);
if (string.IsNullOrWhiteSpace(reportRootRelativePath) ||
string.IsNullOrWhiteSpace(normalizedScenePath) ||
normalizedScenePath.Contains('\\'))
{
return normalizedScenePath;
}
return NormalizeRelativePath(Path.Combine(reportRootRelativePath, normalizedScenePath));
}
private static string? NormalizeReportRootRelativePath(string reportRootPath)
{
if (string.IsNullOrWhiteSpace(reportRootPath))
{
return null;
}
var normalized = reportRootPath.Replace('/', '\\').Trim().TrimEnd('\\');
if (normalized.EndsWith("\\T3_Cut", StringComparison.OrdinalIgnoreCase))
{
return string.Empty;
}
const string marker = "\\T3_Cut\\";
var markerIndex = normalized.IndexOf(marker, StringComparison.OrdinalIgnoreCase);
if (markerIndex >= 0)
{
return NormalizeRelativePath(normalized[(markerIndex + marker.Length)..]);
}
var leafFolder = Path.GetFileName(normalized);
return string.IsNullOrWhiteSpace(leafFolder)
? null
: NormalizeRelativePath(leafFolder);
}
private static string NormalizeRelativePath(string relativePath)
{
return relativePath
.Replace('/', '\\')
.Trim();
}
}
public sealed record KarismaSceneVariableDefinition(
string Name,
KarismaSceneVariableKind Kind,
string Method,
string Payload);
public enum KarismaSceneVariableKind
{
Text,
Image,
VideoResource,
Counter
}