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 _entries = new List(); private readonly List _failures = new List(); 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(), ex.Message); } finally { _sceneToQueryOnLoad = null; } } public void HandleQueryObjectInfos(eKResult result, string sceneName, KAObjectInfos objectInfos) { var objects = new List(); 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(), 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 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", "
"); } } 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 { 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 = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Tornado3 Data", "T3_Cut", "T3_Cut"); 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: options.RootPath = Path.GetFullPath(args[++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 objects) { ScenePath = scenePath; RelativePath = relativePath; Objects = objects; } public string ScenePath { get; } public string RelativePath { get; } public IReadOnlyList 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 objects, string detail) { Result = result; SceneName = sceneName; Objects = objects; Detail = detail; } public eKResult Result { get; } public string SceneName { get; } public IReadOnlyList 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); }