Files
Tornado3_2026Election/tools/KarismaSceneCatalogNet48/Program.cs
2026-05-02 05:35:16 +09:00

596 lines
18 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using KAsyncEngineLib;
namespace KarismaSceneCatalogNet48;
internal static class Program
{
[STAThread]
private static int Main(string[] args)
{
try
{
var options = CatalogOptions.Parse(args);
var session = new CatalogSession(options);
return session.Run();
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
return 1;
}
}
}
internal sealed class CatalogSession
{
private readonly CatalogOptions _options;
private readonly CatalogEventHandler _handler;
private readonly List<SceneCatalogEntry> _entries = new List<SceneCatalogEntry>();
private readonly List<SceneCatalogFailure> _failures = new List<SceneCatalogFailure>();
private IKAEngine _engine;
private IKAScene _sceneToQueryOnLoad;
private bool _connectReceived;
private int _connectErrorCode;
private bool _closeReceived;
private bool _loadReceived;
private eKResult _loadResult;
private bool _objectInfosReceived;
private ObjectInfosResult _objectInfosResult;
private bool _unloadReceived;
public CatalogSession(CatalogOptions options)
{
_options = options;
_handler = new CatalogEventHandler(this);
_engine = (IKAEngine)new KAEngineClass();
}
public int Run()
{
Console.WriteLine(
"Karisma scene catalog starting. target={0}:{1} root={2} output={3}",
_options.Host,
_options.Port,
_options.RootPath,
_options.OutputPath);
if (!ProbeRawTcp())
{
return 1;
}
Console.WriteLine("[CATALOG48] Calling Connect()...");
var connectRequested = _engine.Connect(_options.Host, _options.Port, _handler);
Console.WriteLine("[CATALOG48] Connect() returned {0} raw={1}", connectRequested != 0 ? "TRUE" : "FALSE", connectRequested);
if (connectRequested == 0)
{
return 1;
}
if (!WaitWithMessagePump(() => _connectReceived, _options.Timeout))
{
Console.WriteLine("[CATALOG48] OnConnect timed out.");
return 1;
}
if (_connectErrorCode != 0)
{
Console.WriteLine("[CATALOG48] OnConnect errorCode={0}", _connectErrorCode);
return 1;
}
var scenePaths = Directory
.EnumerateFiles(_options.RootPath, "*.tscn", SearchOption.AllDirectories)
.Where(path => string.IsNullOrWhiteSpace(_options.SceneFilter) ||
path.IndexOf(_options.SceneFilter, StringComparison.OrdinalIgnoreCase) >= 0)
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
.Take(_options.MaxScenes > 0 ? _options.MaxScenes : int.MaxValue)
.ToArray();
Console.WriteLine("[CATALOG48] Scene count={0}", scenePaths.Length);
for (var index = 0; index < scenePaths.Length; index++)
{
var scenePath = scenePaths[index];
var sceneAlias = string.Format("catalog48_{0:D4}", index);
Console.WriteLine("[CATALOG48] ({0}/{1}) {2}", index + 1, scenePaths.Length, scenePath);
ResetSceneState();
IKAScene scene = null;
try
{
scene = _engine.LoadScene(scenePath, sceneAlias);
if (scene == null)
{
_failures.Add(new SceneCatalogFailure(scenePath, "LoadScene returned null."));
continue;
}
_sceneToQueryOnLoad = scene;
if (!WaitWithMessagePump(() => _loadReceived, _options.Timeout))
{
_failures.Add(new SceneCatalogFailure(scenePath, "OnLoadScene timed out."));
continue;
}
if (_loadResult != eKResult.RESULT_SUCCESS)
{
_failures.Add(new SceneCatalogFailure(scenePath, "OnLoadScene result=" + _loadResult));
TryUnload(scene);
continue;
}
if (!WaitWithMessagePump(() => _objectInfosReceived, _options.Timeout))
{
_failures.Add(new SceneCatalogFailure(scenePath, "OnQueryObjectInfos timed out."));
TryUnload(scene);
continue;
}
if (_objectInfosResult.Result != eKResult.RESULT_SUCCESS)
{
_failures.Add(new SceneCatalogFailure(
scenePath,
string.IsNullOrWhiteSpace(_objectInfosResult.Detail)
? "OnQueryObjectInfos result=" + _objectInfosResult.Result
: "OnQueryObjectInfos result=" + _objectInfosResult.Result + " detail=" + _objectInfosResult.Detail));
TryUnload(scene);
continue;
}
_entries.Add(new SceneCatalogEntry(
scenePath,
Path.GetRelativePath(_options.RootPath, scenePath),
_objectInfosResult.Objects));
TryUnload(scene);
}
catch (Exception ex)
{
_failures.Add(new SceneCatalogFailure(scenePath, ex.Message));
if (scene != null)
{
TryUnload(scene);
}
}
}
WriteMarkdown();
try
{
_engine.Disconnect();
WaitWithMessagePump(() => _closeReceived, TimeSpan.FromSeconds(2));
}
catch
{
}
Console.WriteLine();
Console.WriteLine("Summary");
Console.WriteLine("- Scenes Found: {0}", scenePaths.Length);
Console.WriteLine("- Scenes Cataloged: {0}", _entries.Count);
Console.WriteLine("- Failures: {0}", _failures.Count);
Console.WriteLine("- Output: {0}", _options.OutputPath);
return _failures.Count == 0 ? 0 : 1;
}
public void HandleConnect(int errorCode)
{
_connectReceived = true;
_connectErrorCode = errorCode;
Console.WriteLine("[SDK] OnConnect errorCode={0}", errorCode);
}
public void HandleClose(int errorCode)
{
_closeReceived = true;
Console.WriteLine("[SDK] OnClose errorCode={0}", errorCode);
}
public void HandleLoadScene(eKResult result, string sceneName)
{
_loadReceived = true;
_loadResult = result;
Console.WriteLine("[SDK] OnLoadScene result={0} scene={1}", result, sceneName);
if (result != eKResult.RESULT_SUCCESS || _sceneToQueryOnLoad == null)
{
return;
}
try
{
_sceneToQueryOnLoad.QueryObjectInfos();
}
catch (Exception ex)
{
_objectInfosReceived = true;
_objectInfosResult = new ObjectInfosResult(eKResult.RESULT_FAILURE, sceneName, new List<SceneObjectInfo>(), ex.Message);
}
finally
{
_sceneToQueryOnLoad = null;
}
}
public void HandleQueryObjectInfos(eKResult result, string sceneName, KAObjectInfos objectInfos)
{
var objects = new List<SceneObjectInfo>();
try
{
if (result == eKResult.RESULT_SUCCESS)
{
var count = objectInfos.GetCount();
for (var index = 0; index < count; index++)
{
var info = objectInfos.GetObjectInfo(index);
objects.Add(new SceneObjectInfo(
info.Name ?? string.Empty,
info.ObjectType,
info.Value ?? string.Empty,
info.bVisible != 0));
}
}
Console.WriteLine("[SDK] OnQueryObjectInfos result={0} scene={1} count={2}", result, sceneName, objects.Count);
_objectInfosResult = new ObjectInfosResult(result, sceneName, objects, string.Empty);
}
catch (Exception ex)
{
_objectInfosResult = new ObjectInfosResult(eKResult.RESULT_FAILURE, sceneName, objects, ex.Message);
}
finally
{
_objectInfosReceived = true;
}
}
public void HandleUnloadScene(eKResult result, string sceneName)
{
_unloadReceived = true;
Console.WriteLine("[SDK] OnUnloadScene result={0} scene={1}", result, sceneName);
}
private bool ProbeRawTcp()
{
try
{
using (var client = new TcpClient())
{
var asyncResult = client.BeginConnect(_options.Host, _options.Port, null, null);
if (!asyncResult.AsyncWaitHandle.WaitOne(_options.Timeout))
{
Console.WriteLine("[RAW] Failed: timeout");
return false;
}
client.EndConnect(asyncResult);
Console.WriteLine("[RAW] Connected local={0} remote={1}", client.Client.LocalEndPoint, client.Client.RemoteEndPoint);
return true;
}
}
catch (Exception ex)
{
Console.WriteLine("[RAW] Failed: {0}", ex.Message);
return false;
}
}
private void ResetSceneState()
{
_sceneToQueryOnLoad = null;
_loadReceived = false;
_loadResult = eKResult.RESULT_FAILURE;
_objectInfosReceived = false;
_objectInfosResult = new ObjectInfosResult(eKResult.RESULT_FAILURE, string.Empty, new List<SceneObjectInfo>(), string.Empty);
_unloadReceived = false;
}
private void TryUnload(IKAScene scene)
{
try
{
_unloadReceived = false;
scene.UnloadScene();
WaitWithMessagePump(() => _unloadReceived, TimeSpan.FromSeconds(2));
}
catch
{
}
}
private void WriteMarkdown()
{
var outputDirectory = Path.GetDirectoryName(_options.OutputPath);
if (!string.IsNullOrWhiteSpace(outputDirectory))
{
Directory.CreateDirectory(outputDirectory);
}
using (var writer = new StreamWriter(_options.OutputPath, false, new UTF8Encoding(false)))
{
writer.WriteLine("# Scene Object Catalog");
writer.WriteLine();
writer.WriteLine("- Generated: {0:yyyy-MM-dd HH:mm:ss}", DateTime.Now);
writer.WriteLine("- Root: `{0}`", _options.RootPath);
writer.WriteLine("- Scene Count: {0}", _entries.Count);
writer.WriteLine("- Failure Count: {0}", _failures.Count);
writer.WriteLine();
if (_failures.Count > 0)
{
writer.WriteLine("## Failures");
writer.WriteLine();
foreach (var failure in _failures)
{
writer.WriteLine("- `{0}`: {1}", failure.ScenePath, EscapeCell(failure.Reason));
}
writer.WriteLine();
}
writer.WriteLine("## Scenes");
writer.WriteLine();
foreach (var entry in _entries)
{
writer.WriteLine("### `{0}`", entry.RelativePath);
writer.WriteLine();
writer.WriteLine("- Object Count: {0}", entry.Objects.Count);
writer.WriteLine();
writer.WriteLine("| Name | Type | Value | Visible |");
writer.WriteLine("| --- | --- | --- | --- |");
foreach (var obj in entry.Objects)
{
writer.WriteLine(
"| {0} | {1} | {2} | {3} |",
EscapeCell(obj.Name),
obj.ObjectType,
EscapeCell(obj.Value),
obj.Visible ? "true" : "false");
}
writer.WriteLine();
}
}
}
private static bool WaitWithMessagePump(Func<bool> condition, TimeSpan timeout)
{
User32.PeekMessage(out _, IntPtr.Zero, 0, 0, 0);
var deadline = DateTime.UtcNow + timeout;
while (!condition())
{
while (User32.PeekMessage(out var message, IntPtr.Zero, 0, 0, 1))
{
User32.TranslateMessage(ref message);
User32.DispatchMessage(ref message);
}
if (DateTime.UtcNow >= deadline)
{
return false;
}
Thread.Sleep(10);
}
return true;
}
private static string EscapeCell(string value)
{
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
return value.Replace("\\", "\\\\")
.Replace("|", "\\|")
.Replace("\r", " ")
.Replace("\n", "<br/>");
}
}
internal sealed class CatalogEventHandler : KAEventHandler
{
private readonly CatalogSession _owner;
public CatalogEventHandler(CatalogSession owner)
{
_owner = owner;
}
public void OnConnect(int errorCode) => _owner.HandleConnect(errorCode);
public void OnClose(int errorCode) => _owner.HandleClose(errorCode);
public void OnLoadScene(eKResult result, string sceneName) => _owner.HandleLoadScene(result, sceneName);
public void OnUnloadScene(eKResult result, string sceneName) => _owner.HandleUnloadScene(result, sceneName);
public void OnQueryObjectInfos(eKResult result, string sceneName, KAObjectInfos objectInfos) =>
_owner.HandleQueryObjectInfos(result, sceneName, objectInfos);
}
internal sealed class CatalogOptions
{
private const string FixedT3CutPath = @"D:\Elect2026\T3_Cut";
public string Host { get; private set; }
public int Port { get; private set; }
public TimeSpan Timeout { get; private set; }
public string RootPath { get; private set; }
public string OutputPath { get; private set; }
public string SceneFilter { get; private set; }
public int MaxScenes { get; private set; }
private CatalogOptions()
{
Host = "127.0.0.1";
Port = 30001;
Timeout = TimeSpan.FromSeconds(5);
RootPath = FixedT3CutPath;
OutputPath = Path.Combine(Environment.CurrentDirectory, "SCENE_OBJECT_CATALOG.md");
SceneFilter = string.Empty;
MaxScenes = 0;
}
public static CatalogOptions Parse(string[] args)
{
var options = new CatalogOptions();
for (var index = 0; index < args.Length; index++)
{
switch (args[index])
{
case "--host" when index + 1 < args.Length:
options.Host = args[++index];
break;
case "--port" when index + 1 < args.Length && int.TryParse(args[index + 1], out var port):
options.Port = port;
index++;
break;
case "--timeout" when index + 1 < args.Length && double.TryParse(args[index + 1], out var timeoutSeconds):
options.Timeout = TimeSpan.FromSeconds(timeoutSeconds);
index++;
break;
case "--root" when index + 1 < args.Length:
index++;
break;
case "--output" when index + 1 < args.Length:
options.OutputPath = Path.GetFullPath(args[++index]);
break;
case "--filter" when index + 1 < args.Length:
options.SceneFilter = args[++index];
break;
case "--max-scenes" when index + 1 < args.Length && int.TryParse(args[index + 1], out var maxScenes):
options.MaxScenes = maxScenes;
index++;
break;
}
}
if (!Directory.Exists(options.RootPath))
{
throw new DirectoryNotFoundException("Catalog root path does not exist: " + options.RootPath);
}
return options;
}
}
internal sealed class SceneCatalogEntry
{
public SceneCatalogEntry(string scenePath, string relativePath, IReadOnlyList<SceneObjectInfo> objects)
{
ScenePath = scenePath;
RelativePath = relativePath;
Objects = objects;
}
public string ScenePath { get; }
public string RelativePath { get; }
public IReadOnlyList<SceneObjectInfo> Objects { get; }
}
internal sealed class SceneCatalogFailure
{
public SceneCatalogFailure(string scenePath, string reason)
{
ScenePath = scenePath;
Reason = reason;
}
public string ScenePath { get; }
public string Reason { get; }
}
internal sealed class SceneObjectInfo
{
public SceneObjectInfo(string name, eKObjectType objectType, string value, bool visible)
{
Name = name;
ObjectType = objectType;
Value = value;
Visible = visible;
}
public string Name { get; }
public eKObjectType ObjectType { get; }
public string Value { get; }
public bool Visible { get; }
}
internal sealed class ObjectInfosResult
{
public ObjectInfosResult(eKResult result, string sceneName, IReadOnlyList<SceneObjectInfo> objects, string detail)
{
Result = result;
SceneName = sceneName;
Objects = objects;
Detail = detail;
}
public eKResult Result { get; }
public string SceneName { get; }
public IReadOnlyList<SceneObjectInfo> Objects { get; }
public string Detail { get; }
}
[StructLayout(LayoutKind.Sequential)]
internal struct NativeMessage
{
public IntPtr hwnd;
public uint message;
public UIntPtr wParam;
public IntPtr lParam;
public uint time;
public NativePoint pt;
public uint lPrivate;
}
[StructLayout(LayoutKind.Sequential)]
internal struct NativePoint
{
public int x;
public int y;
}
internal static class User32
{
[DllImport("user32.dll")]
internal static extern bool PeekMessage(out NativeMessage lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg);
[DllImport("user32.dll")]
internal static extern bool TranslateMessage([In] ref NativeMessage lpMsg);
[DllImport("user32.dll")]
internal static extern IntPtr DispatchMessage([In] ref NativeMessage lpmsg);
}