383 lines
13 KiB
C#
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
|
|
}
|