diff --git a/Tornado3_2026Election.slnx b/Tornado3_2026Election.slnx
index 6f22afe..3c856ed 100644
--- a/Tornado3_2026Election.slnx
+++ b/Tornado3_2026Election.slnx
@@ -1,13 +1,12 @@
-
-
-
-
+
+
+
diff --git a/Tornado3_2026Election/Persistence/AppState.cs b/Tornado3_2026Election/Persistence/AppState.cs
index acd373b..01186c7 100644
--- a/Tornado3_2026Election/Persistence/AppState.cs
+++ b/Tornado3_2026Election/Persistence/AppState.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
+using Tornado3_2026Election.Services;
namespace Tornado3_2026Election.Persistence;
@@ -10,7 +11,7 @@ public sealed class AppState
public string SelectedStationId { get; set; } = "KNN";
- public string ImageRootPath { get; set; } = @"C:\Users\y2keu\Downloads\T3_Cut";
+ public string ImageRootPath { get; set; } = TornadoPathResolver.GetDefaultT3CutPath();
public bool AutoRestoreSchedules { get; set; } = true;
@@ -48,4 +49,3 @@ public sealed class AppState
public Dictionary StationRegionFilters { get; set; } = [];
}
-
diff --git a/Tornado3_2026Election/Services/KarismaTornado3Adapter.cs b/Tornado3_2026Election/Services/KarismaTornado3Adapter.cs
index 69ccc7b..a3dd197 100644
--- a/Tornado3_2026Election/Services/KarismaTornado3Adapter.cs
+++ b/Tornado3_2026Election/Services/KarismaTornado3Adapter.cs
@@ -86,10 +86,20 @@ public sealed class KarismaTornado3Adapter : ITornado3Adapter, IDisposable
logService.Info($"Karisma adapter using default port {DefaultKarismaPort}.");
}
- var t3CutPath = t3CutPathProvider();
+ var configuredT3CutPath = t3CutPathProvider();
+ var t3CutPath = string.IsNullOrWhiteSpace(configuredT3CutPath)
+ ? TornadoPathResolver.GetDefaultT3CutPath()
+ : TornadoPathResolver.NormalizeConfiguredPath(configuredT3CutPath);
+
+ if (!string.Equals(configuredT3CutPath, t3CutPath, StringComparison.OrdinalIgnoreCase) &&
+ !string.IsNullOrWhiteSpace(t3CutPath))
+ {
+ logService.Info($"Karisma adapter normalized T3_Cut path to '{t3CutPath}'.");
+ }
+
if (string.IsNullOrWhiteSpace(t3CutPath) || !Directory.Exists(t3CutPath))
{
- logService.Warning("Karisma adapter disabled: set a valid T3_Cut path in Settings.");
+ logService.Warning($"Karisma adapter disabled: set a valid T3_Cut path in Settings. current='{configuredT3CutPath}'");
adapter = new MockTornado3Adapter(logService);
return false;
}
@@ -230,7 +240,11 @@ public sealed class KarismaTornado3Adapter : ITornado3Adapter, IDisposable
private string ResolveT3CutPath()
{
- var path = _t3CutPathProvider();
+ var configuredPath = _t3CutPathProvider();
+ var path = string.IsNullOrWhiteSpace(configuredPath)
+ ? TornadoPathResolver.GetDefaultT3CutPath()
+ : TornadoPathResolver.NormalizeConfiguredPath(configuredPath);
+
if (string.IsNullOrWhiteSpace(path) || !Directory.Exists(path))
{
throw new DirectoryNotFoundException("T3_Cut path is not configured or does not exist.");
diff --git a/Tornado3_2026Election/Services/TornadoManager.cs b/Tornado3_2026Election/Services/TornadoManager.cs
index c43dcae..b68a027 100644
--- a/Tornado3_2026Election/Services/TornadoManager.cs
+++ b/Tornado3_2026Election/Services/TornadoManager.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using KAsyncEngineLib;
@@ -372,9 +373,14 @@ public sealed class TornadoManager : IDisposable
private sealed class StaDispatcher : IDisposable
{
- private readonly BlockingCollection _queue = new();
+ private const uint WmApp = 0x8000;
+ private const uint WmDispatch = WmApp + 1;
+
+ private readonly ConcurrentQueue _queue = new();
private readonly Thread _thread;
+ private readonly ManualResetEventSlim _threadReady = new();
private bool _disposed;
+ private uint _threadId;
public StaDispatcher(string threadName)
{
@@ -385,6 +391,7 @@ public sealed class TornadoManager : IDisposable
};
_thread.SetApartmentState(ApartmentState.STA);
_thread.Start();
+ _threadReady.Wait();
}
public Task InvokeAsync(Action action, CancellationToken cancellationToken)
@@ -402,18 +409,29 @@ public sealed class TornadoManager : IDisposable
cancellationToken.ThrowIfCancellationRequested();
var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
- _queue.Add(() =>
+ _queue.Enqueue(() =>
{
+ if (cancellationToken.IsCancellationRequested)
+ {
+ completion.TrySetCanceled(cancellationToken);
+ return;
+ }
+
try
{
- completion.SetResult(action());
+ completion.TrySetResult(action());
}
catch (Exception ex)
{
- completion.SetException(ex);
+ completion.TrySetException(ex);
}
});
+ if (!PostThreadMessage(_threadId, WmDispatch, UIntPtr.Zero, IntPtr.Zero))
+ {
+ throw new InvalidOperationException("Failed to signal Karisma STA dispatcher.");
+ }
+
return cancellationToken.CanBeCanceled
? completion.Task.WaitAsync(cancellationToken)
: completion.Task;
@@ -427,17 +445,77 @@ public sealed class TornadoManager : IDisposable
}
_disposed = true;
- _queue.CompleteAdding();
+ PostThreadMessage(_threadId, WmQuit, UIntPtr.Zero, IntPtr.Zero);
_thread.Join();
- _queue.Dispose();
+ _threadReady.Dispose();
}
private void Run()
{
- foreach (var action in _queue.GetConsumingEnumerable())
+ PeekMessage(out _, IntPtr.Zero, 0, 0, 0);
+ _threadId = GetCurrentThreadId();
+ _threadReady.Set();
+
+ while (GetMessage(out var message, IntPtr.Zero, 0, 0) > 0)
+ {
+ if (message.message == WmDispatch)
+ {
+ DrainQueue();
+ continue;
+ }
+
+ TranslateMessage(ref message);
+ DispatchMessage(ref message);
+ }
+
+ DrainQueue();
+ }
+
+ private void DrainQueue()
+ {
+ while (_queue.TryDequeue(out var action))
{
action();
}
}
+
+ private const uint WmQuit = 0x0012;
+
+ [DllImport("kernel32.dll")]
+ private static extern uint GetCurrentThreadId();
+
+ [DllImport("user32.dll", SetLastError = true)]
+ private static extern bool PostThreadMessage(uint idThread, uint msg, UIntPtr wParam, IntPtr lParam);
+
+ [DllImport("user32.dll")]
+ private static extern int GetMessage(out NativeMessage lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
+
+ [DllImport("user32.dll")]
+ private static extern bool TranslateMessage([In] ref NativeMessage lpMsg);
+
+ [DllImport("user32.dll")]
+ private static extern IntPtr DispatchMessage([In] ref NativeMessage lpMsg);
+
+ [DllImport("user32.dll")]
+ private static extern bool PeekMessage(out NativeMessage lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax, uint wRemoveMsg);
+
+ [StructLayout(LayoutKind.Sequential)]
+ private 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)]
+ private struct NativePoint
+ {
+ public int x;
+ public int y;
+ }
}
}
diff --git a/Tornado3_2026Election/Services/TornadoPathResolver.cs b/Tornado3_2026Election/Services/TornadoPathResolver.cs
new file mode 100644
index 0000000..5544389
--- /dev/null
+++ b/Tornado3_2026Election/Services/TornadoPathResolver.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Tornado3_2026Election.Services;
+
+public static class TornadoPathResolver
+{
+ public static string GetDefaultT3CutPath()
+ {
+ foreach (var candidate in GetCandidatePaths())
+ {
+ var normalized = NormalizeConfiguredPath(candidate);
+ if (!string.IsNullOrWhiteSpace(normalized) && Directory.Exists(normalized))
+ {
+ return normalized;
+ }
+ }
+
+ return NormalizeConfiguredPath(Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
+ "Tornado3 Data",
+ "T3_Cut",
+ "T3_Cut"));
+ }
+
+ public static string NormalizeConfiguredPath(string? configuredPath)
+ {
+ if (string.IsNullOrWhiteSpace(configuredPath))
+ {
+ return string.Empty;
+ }
+
+ var trimmedPath = configuredPath.Trim();
+ if (!Directory.Exists(trimmedPath))
+ {
+ return trimmedPath;
+ }
+
+ var nestedDefault = Path.Combine(trimmedPath, "T3_Cut");
+ if (ContainsSceneFiles(nestedDefault))
+ {
+ return nestedDefault;
+ }
+
+ if (ContainsSceneFiles(trimmedPath))
+ {
+ return trimmedPath;
+ }
+
+ var nestedDirectory = TryFindNestedSceneDirectory(trimmedPath);
+ return nestedDirectory ?? trimmedPath;
+ }
+
+ private static IEnumerable GetCandidatePaths()
+ {
+ var userProfile = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+ var documents = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
+ var configured = Environment.GetEnvironmentVariable("TORNADO_T3CUT_PATH");
+
+ if (!string.IsNullOrWhiteSpace(configured))
+ {
+ yield return configured;
+ }
+
+ if (!string.IsNullOrWhiteSpace(documents))
+ {
+ yield return Path.Combine(documents, "Tornado3 Data", "T3_Cut", "T3_Cut");
+ yield return Path.Combine(documents, "Tornado3 Data", "T3_Cut");
+ }
+
+ if (!string.IsNullOrWhiteSpace(userProfile))
+ {
+ yield return Path.Combine(userProfile, "Downloads", "T3_Cut");
+ }
+ }
+
+ private static string? TryFindNestedSceneDirectory(string rootPath)
+ {
+ try
+ {
+ return Directory.EnumerateDirectories(rootPath, "*", SearchOption.TopDirectoryOnly)
+ .Select(NormalizeConfiguredPath)
+ .FirstOrDefault(path => !string.IsNullOrWhiteSpace(path) && ContainsSceneFiles(path));
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private static bool ContainsSceneFiles(string? path)
+ {
+ if (string.IsNullOrWhiteSpace(path) || !Directory.Exists(path))
+ {
+ return false;
+ }
+
+ try
+ {
+ return Directory.EnumerateFiles(path, "*.tscn", SearchOption.AllDirectories).Any();
+ }
+ catch
+ {
+ return false;
+ }
+ }
+}
diff --git a/Tornado3_2026Election/Tornado3_2026Election.csproj b/Tornado3_2026Election/Tornado3_2026Election.csproj
index 193f0aa..f488be2 100644
--- a/Tornado3_2026Election/Tornado3_2026Election.csproj
+++ b/Tornado3_2026Election/Tornado3_2026Election.csproj
@@ -6,24 +6,20 @@
Tornado3_2026Election
app.manifest
Assets\AppIcon.ico
- x86;x64;ARM64
- win-x86;win-x64;win-arm64
+ x64
+ win-x64
true
false
true
enable
-
+
win-x64
-
- win-x86
-
-
-
- win-arm64
+
+ C:\Karisma SDK
@@ -89,7 +85,7 @@
- ..\..\..\..\..\..\Karisma SDK\Bin\C#\Interop.KAsyncEngineLib.dll
+ $(KarismaSdkDir)\Bin\C#\Interop.KAsyncEngineLib.dll
diff --git a/Tornado3_2026Election/ViewModels/MainViewModel.cs b/Tornado3_2026Election/ViewModels/MainViewModel.cs
index 7ca616f..91d29c6 100644
--- a/Tornado3_2026Election/ViewModels/MainViewModel.cs
+++ b/Tornado3_2026Election/ViewModels/MainViewModel.cs
@@ -24,6 +24,7 @@ public sealed class MainViewModel : ObservableObject
private readonly FormatCatalogService _formatCatalogService;
private readonly AppStateStore _stateStore;
private readonly LogService _logService;
+ private readonly ITornado3Adapter _sharedTornadoAdapter;
private readonly SemaphoreSlim _stateSaveLock = new(1, 1);
private AppPage _currentPage = AppPage.IntegratedSchedule;
private ChannelOperationMode _operationMode = ChannelOperationMode.General;
@@ -56,11 +57,12 @@ public sealed class MainViewModel : ObservableObject
Settings.PropertyChanged += Settings_PropertyChanged;
Data.PropertyChanged += Data_PropertyChanged;
+ _sharedTornadoAdapter = KarismaTornado3Adapter.CreateOrFallback(_logService, () => Settings.ImageRootPath);
- NormalChannel = CreateChannelViewModel(BroadcastChannel.Normal, "노멀");
- TopLeftChannel = CreateChannelViewModel(BroadcastChannel.TopLeft, "좌상단");
- BottomChannel = CreateChannelViewModel(BroadcastChannel.Bottom, "하단");
- VideoWallChannel = CreateChannelViewModel(BroadcastChannel.VideoWall, "비디오월");
+ NormalChannel = CreateChannelViewModel(BroadcastChannel.Normal, "노멀", _sharedTornadoAdapter);
+ TopLeftChannel = CreateChannelViewModel(BroadcastChannel.TopLeft, "좌상단", _sharedTornadoAdapter);
+ BottomChannel = CreateChannelViewModel(BroadcastChannel.Bottom, "하단", _sharedTornadoAdapter);
+ VideoWallChannel = CreateChannelViewModel(BroadcastChannel.VideoWall, "비디오월", _sharedTornadoAdapter);
Channels = [NormalChannel, TopLeftChannel, BottomChannel, VideoWallChannel];
foreach (var channel in Channels)
@@ -82,6 +84,7 @@ public sealed class MainViewModel : ObservableObject
_logService.Entries.CollectionChanged += (_, _) => RebuildFilteredLogs();
SelectedLogFilterOption = LogFilterOptions[0];
_logService.Info("SYSTEM_SPEC 기반 MVVM 구조를 초기화했습니다.");
+ _ = WarmupSharedCgConnectionAsync();
}
public DataViewModel Data { get; }
@@ -439,6 +442,7 @@ public sealed class MainViewModel : ObservableObject
}
_logService.Info("저장한 시작 옵션에 따라 상태를 자동 복원했습니다.");
+ _ = WarmupSharedCgConnectionAsync();
}
public async Task RestoreAsync()
@@ -471,6 +475,11 @@ public sealed class MainViewModel : ObservableObject
public async Task ShutdownAsync()
{
await SaveStateCoreAsync(writeLog: false);
+ if (_sharedTornadoAdapter is IDisposable disposableAdapter)
+ {
+ disposableAdapter.Dispose();
+ }
+
Data.Dispose();
}
@@ -520,6 +529,7 @@ public sealed class MainViewModel : ObservableObject
if (args.PropertyName is nameof(SettingsViewModel.SelectedStationId) or nameof(SettingsViewModel.ImageRootPath))
{
QueueAutomaticSave();
+ _ = WarmupSharedCgConnectionAsync();
}
}
@@ -694,9 +704,25 @@ public sealed class MainViewModel : ObservableObject
}
}
- private ChannelScheduleViewModel CreateChannelViewModel(BroadcastChannel channel, string title)
+ private async Task WarmupSharedCgConnectionAsync()
+ {
+ if (!_sharedTornadoAdapter.IsLiveCg)
+ {
+ return;
+ }
+
+ try
+ {
+ await _sharedTornadoAdapter.EnsureConnectedAsync(CancellationToken.None).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ _logService.Warning($"CG startup connection attempt failed: {ex.Message}");
+ }
+ }
+
+ private ChannelScheduleViewModel CreateChannelViewModel(BroadcastChannel channel, string title, ITornado3Adapter adapter)
{
- var adapter = KarismaTornado3Adapter.CreateOrFallback(_logService, () => Settings.ImageRootPath);
var queue = new ObservableCollection();
var engine = new ChannelScheduleEngine(
channel,
diff --git a/Tornado3_2026Election/ViewModels/SettingsViewModel.cs b/Tornado3_2026Election/ViewModels/SettingsViewModel.cs
index 33e0562..459caf4 100644
--- a/Tornado3_2026Election/ViewModels/SettingsViewModel.cs
+++ b/Tornado3_2026Election/ViewModels/SettingsViewModel.cs
@@ -3,13 +3,14 @@ using System.Collections.ObjectModel;
using System.Linq;
using Tornado3_2026Election.Common;
using Tornado3_2026Election.Domain;
+using Tornado3_2026Election.Services;
namespace Tornado3_2026Election.ViewModels;
public sealed class SettingsViewModel : ObservableObject
{
private string _selectedStationId = "KNN";
- private string _imageRootPath = @"C:\Users\y2keu\Downloads\T3_Cut";
+ private string _imageRootPath = TornadoPathResolver.GetDefaultT3CutPath();
public SettingsViewModel(IEnumerable stations)
{
@@ -50,7 +51,7 @@ public sealed class SettingsViewModel : ObservableObject
public string ImageRootPath
{
get => _imageRootPath;
- set => SetProperty(ref _imageRootPath, value);
+ set => SetProperty(ref _imageRootPath, TornadoPathResolver.NormalizeConfiguredPath(value));
}
public StationFilterItemViewModel SelectedStation
diff --git a/tools/KarismaTcpProbe/KarismaTcpProbe.csproj b/tools/KarismaTcpProbe/KarismaTcpProbe.csproj
new file mode 100644
index 0000000..dd01204
--- /dev/null
+++ b/tools/KarismaTcpProbe/KarismaTcpProbe.csproj
@@ -0,0 +1,19 @@
+
+
+ Exe
+ net8.0-windows
+ x64
+ x64
+ win-x64
+ enable
+ enable
+ C:\Karisma SDK
+
+
+
+
+ $(KarismaSdkDir)\Bin\C#\Interop.KAsyncEngineLib.dll
+ true
+
+
+
diff --git a/tools/KarismaTcpProbe/Program.cs b/tools/KarismaTcpProbe/Program.cs
new file mode 100644
index 0000000..79b5cd4
--- /dev/null
+++ b/tools/KarismaTcpProbe/Program.cs
@@ -0,0 +1,426 @@
+using System;
+using System.Net.Sockets;
+using System.Runtime.InteropServices;
+using System.Threading;
+using System.Threading.Tasks;
+using KAsyncEngineLib;
+
+var options = ProbeOptions.Parse(args);
+
+Console.WriteLine($"Karisma TCP probe starting. target={options.Host}:{options.Port} timeout={options.Timeout.TotalSeconds:0}s");
+
+var rawTcpOk = await ProbeRawTcpAsync(options).ConfigureAwait(false);
+var sdkResult = await ProbeSdkAsync(options).ConfigureAwait(false);
+
+Console.WriteLine();
+Console.WriteLine("Summary");
+Console.WriteLine($"- Raw TCP: {(rawTcpOk ? "OK" : "FAILED")}");
+Console.WriteLine($"- SDK Connect(): {(sdkResult.ConnectRequestAccepted ? "ACCEPTED" : "REJECTED")}");
+Console.WriteLine($"- SDK OnConnect: {sdkResult.ConnectOutcome}");
+Console.WriteLine($"- SDK Detail: {sdkResult.Detail}");
+
+Environment.ExitCode = rawTcpOk && sdkResult.ConnectRequestAccepted && sdkResult.ConnectOutcome == "SUCCESS"
+ ? 0
+ : 1;
+
+static async Task ProbeRawTcpAsync(ProbeOptions options)
+{
+ try
+ {
+ using var client = new TcpClient();
+ using var cts = new CancellationTokenSource(options.Timeout);
+ await client.ConnectAsync(options.Host, options.Port, cts.Token).ConfigureAwait(false);
+ Console.WriteLine($"[RAW] Connected local={client.Client.LocalEndPoint} remote={client.Client.RemoteEndPoint}");
+ return true;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"[RAW] Failed: {ex.Message}");
+ return false;
+ }
+}
+
+static Task ProbeSdkAsync(ProbeOptions options)
+{
+ var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ var thread = new Thread(() =>
+ {
+ try
+ {
+ var handler = new ProbeEventHandler();
+ var engine = (IKAEngine)new KAEngineClass();
+
+ Console.WriteLine("[SDK] Calling Connect()...");
+ var connectRequested = engine.Connect(options.Host, options.Port, handler);
+ Console.WriteLine($"[SDK] Connect() returned {(connectRequested != 0 ? "TRUE" : "FALSE")} raw={connectRequested}");
+
+ if (connectRequested == 0)
+ {
+ completion.TrySetResult(new SdkProbeResult(false, "FAILED", "Connect() returned 0."));
+ return;
+ }
+
+ if (!WaitForTaskWithMessagePump(handler.ConnectTask, options.Timeout))
+ {
+ completion.TrySetResult(new SdkProbeResult(true, "TIMEOUT", $"OnConnect was not received within {options.Timeout.TotalSeconds:0} seconds."));
+ return;
+ }
+
+ var errorCode = handler.ConnectTask.Result;
+ var outcome = errorCode == 0 ? "SUCCESS" : "FAILED";
+ completion.TrySetResult(new SdkProbeResult(true, outcome, $"OnConnect errorCode={errorCode}"));
+
+ try
+ {
+ engine.Disconnect();
+ handler.CloseTask.Wait(TimeSpan.FromSeconds(2));
+ }
+ catch
+ {
+ }
+ }
+ catch (Exception ex)
+ {
+ completion.TrySetResult(new SdkProbeResult(false, "EXCEPTION", ex.ToString()));
+ }
+ })
+ {
+ IsBackground = true,
+ Name = "KarismaTcpProbe"
+ };
+
+ thread.SetApartmentState(ApartmentState.STA);
+ thread.Start();
+ return completion.Task;
+}
+
+static bool WaitForTaskWithMessagePump(Task task, TimeSpan timeout)
+{
+ User32.PeekMessage(out _, IntPtr.Zero, 0, 0, 0);
+
+ var deadline = DateTime.UtcNow + timeout;
+ while (!task.IsCompleted)
+ {
+ 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;
+}
+
+internal sealed record ProbeOptions(string Host, int Port, TimeSpan Timeout)
+{
+ public static ProbeOptions Parse(string[] args)
+ {
+ var host = "127.0.0.1";
+ var port = 30001;
+ var timeout = TimeSpan.FromSeconds(5);
+
+ for (var index = 0; index < args.Length; index++)
+ {
+ switch (args[index])
+ {
+ case "--host" when index + 1 < args.Length:
+ host = args[++index];
+ break;
+ case "--port" when index + 1 < args.Length && int.TryParse(args[index + 1], out var parsedPort):
+ port = parsedPort;
+ index++;
+ break;
+ case "--timeout" when index + 1 < args.Length && double.TryParse(args[index + 1], out var parsedTimeoutSeconds):
+ timeout = TimeSpan.FromSeconds(parsedTimeoutSeconds);
+ index++;
+ break;
+ }
+ }
+
+ return new ProbeOptions(host, port, timeout);
+ }
+}
+
+internal sealed record SdkProbeResult(bool ConnectRequestAccepted, string ConnectOutcome, string Detail);
+
+internal sealed class ProbeEventHandler : KAEventHandler
+{
+ private readonly TaskCompletionSource _connectTask = new(TaskCreationOptions.RunContinuationsAsynchronously);
+ private readonly TaskCompletionSource _closeTask = new(TaskCreationOptions.RunContinuationsAsynchronously);
+
+ public Task ConnectTask => _connectTask.Task;
+
+ public Task CloseTask => _closeTask.Task;
+
+ public void OnConnect(int ErrorCode)
+ {
+ Console.WriteLine($"[SDK] OnConnect errorCode={ErrorCode}");
+ _connectTask.TrySetResult(ErrorCode);
+ }
+
+ public void OnClose(int ErrorCode)
+ {
+ Console.WriteLine($"[SDK] OnClose errorCode={ErrorCode}");
+ _closeTask.TrySetResult(ErrorCode);
+ }
+
+ public void OnLoadScene(eKResult Result, string SceneName) { }
+ public void OnLoadSceneForce(eKResult Result, string SceneName) { }
+ public void OnLogMessage(string LogMessage) { }
+ public void OnMessageNo(uint MessageNo) { }
+ public void OnBeginTransaction(eKResult Result) { }
+ public void OnEndTransaction(eKResult Result) { }
+ public void OnHeartBeat(eKResult Result) { }
+ public void OnUnloadAll(eKResult Result) { }
+ public void OnSetTrialPlayoutMode(eKResult Result) { }
+ public void OnCheckVersion(eKResult Result, string ServerVersion, string SDKVersion) { }
+ public void OnSetAudioOutput(eKResult Result) { }
+ public void OnScenePrepare(eKResult Result, int OutputChannelIndex, int LayerNo) { }
+ public void OnScenePrepareEx(eKResult Result, int OutputChannelIndex, int LayerNo) { }
+ public void OnPlay(eKResult Result, int OutputChannelIndex, int LayerNo) { }
+ public void OnPlayOut(eKResult Result, int OutputChannelIndex, int LayerNo) { }
+ public void OnStop(eKResult Result, int OutputChannelIndex, int LayerNo) { }
+ public void OnStopAll(eKResult Result) { }
+ public void OnPause(eKResult Result, int OutputChannelIndex, int LayerNo) { }
+ public void OnResume(eKResult Result, int OutputChannelIndex, int LayerNo) { }
+ public void OnQueryIsOnAir(eKResult Result, int OutputChannelIndex, int LayerNo, int bOnAir) { }
+ public void OnTrigger(eKResult Result, int OutputChannelIndex, int LayerNo) { }
+ public void OnScenePlayingStarted(string SceneName, int OutputChannelIndex, int LayerNo) { }
+ public void OnScenePlayed(string SceneName, int OutputChannelIndex, int LayerNo) { }
+ public void OnSceneAnimationPlayed(string SceneName, int OutputChannelIndex, int LayerNo, string AnimationName) { }
+ public void OnScenePaused(string SceneName, int OutputChannelIndex, int LayerNo, int bLastPause) { }
+ public void OnSceneSaved(eKResult Result, string FileName) { }
+ public void OnTriggerObject(eKResult Result, int OutputChannelIndex, int LayerNo) { }
+ public void OnResumeBackground(eKResult Result, int OutputChannelIndex, int LayerNo) { }
+ public void OnSaveMixedPreviewImage(eKResult Result, int OutputChannelIndex, int LayerNo) { }
+ public void OnPlayDirect(eKResult Result, int OutputChannelIndex, int LayerNo) { }
+ public void OnCutIn(eKResult Result, int OutputChannelIndex, int LayerNo) { }
+ public void OnCutOut(eKResult Result, int OutputChannelIndex, int LayerNo) { }
+ public void OnClearNextPreview(eKResult Result, int OutputChannelIndex, int LayerNo) { }
+ public void OnPlayRange(eKResult Result, int OutputChannelIndex, int LayerNo) { }
+ public void OnQueryPlaybackRangeCount(eKResult Result, string SceneName, int PlaybackRangeCount) { }
+ public void OnQueryPlaybackRange(eKResult Result, string SceneName, int PlaybackRangeNo, int Start, int End) { }
+ public void OnQueryOutputChannelIndex(eKResult Result, string SceneName, int OutputChannelIndex) { }
+ public void OnPlayInNextPreview(eKResult Result, int OutputChannelIndex, int LayerNo) { }
+ public void OnPlayOutNextPreview(eKResult Result, int OutputChannelIndex, int LayerNo) { }
+ public void OnSetBackgroundFill(eKResult Result, string SceneName) { }
+ public void OnSetBackgroundTexture(eKResult Result, string SceneName) { }
+ public void OnSetBackgroundVideo(eKResult Result, string SceneName) { }
+ public void OnSetBackgroundLiveIn(eKResult Result, string SceneName) { }
+ public void OnUseBackground(eKResult Result, string SceneName) { }
+ public void OnSetBackgroundVideoPlayInfo(eKResult Result, string SceneName) { }
+ public void OnQueryBackgroundVideoPlayInfo(eKResult Result, string SceneName, ref sKVideoPlayInfo pVideoPlayInfo) { }
+ public void OnSetSceneEffectType(eKResult Result, string SceneName) { }
+ public void OnSaveSceneImage(eKResult Result, string SceneName) { }
+ public void OnSaveScene(eKResult Result, string SceneName) { }
+ public void OnUnloadScene(eKResult Result, string SceneName) { }
+ public void OnReloadScene(eKResult Result, string SceneName) { }
+ public void OnUpdateTextures(eKResult Result, string SceneName) { }
+ public void OnSetSceneAudioFile(eKResult Result, string SceneName) { }
+ public void OnEnableSceneAudio(eKResult Result, string SceneName) { }
+ public void OnSetSceneDuration(eKResult Result, string SceneName) { }
+ public void OnSetBackgroundPauseType(eKResult Result, string SceneName) { }
+ public void OnSetBackgroundChangeType(eKResult Result, string SceneName) { }
+ public void OnSetBackgroundPauseAtZeroFrameAsStandBy(eKResult Result, string SceneName) { }
+ public void OnResetDuration(eKResult Result, string SceneName) { }
+ public void OnSetDuration(eKResult Result, string SceneName) { }
+ public void OnAddObject(eKResult Result, string SceneName) { }
+ public void OnAddCloneObject(eKResult Result, string SceneName) { }
+ public void OnUpdateThumbnail(eKResult Result, string SceneName) { }
+ public void OnExportVideo(eKResult Result, string SceneName) { }
+ public void OnStopVideoExporting(eKResult Result) { }
+ public void OnQueryVideoExportingProgress(eKResult Result, string TargetName, int CurrentFrame, int TotalFrame) { }
+ public void OnFinishedVideoExporting(eKResult Result, string FileName) { }
+ public void OnAddPause(eKResult Result, string SceneName) { }
+ public void OnDeletePause(eKResult Result, string SceneName) { }
+ public void OnSetPause(eKResult Result, string SceneName) { }
+ public void OnSetPauseWithIndex(eKResult Result, string SceneName) { }
+ public void OnDeletePauseWithIndex(eKResult Result, string SceneName) { }
+ public void OnQueryPauseCount(eKResult Result, string SceneName, int PauseCount) { }
+ public void OnQueryObjectInfos(eKResult Result, string SceneName, KAObjectInfos pObjectInfos) { }
+ public void OnQueryAnimationNames(eKResult Result, string SceneName, KAStrings pAnimationNames) { }
+ public void OnQueryAnimationCount(eKResult Result, string SceneName, int AnimationCount) { }
+ public void OnQueryObjectInfosByScreenPoint(eKResult Result, KAObjectInfos pObjectInfos) { }
+ public void OnQuerySceneEffectType(eKResult Result, string SceneName, int bInEffect, eKEffectType EffectType, int Duration) { }
+ public void OnQueryDuration(eKResult Result, string SceneName, string AnimationName, int Duration) { }
+ public void OnQueryContentsOfTextObjects(eKResult Result, string SceneName, KAStrings pTexts) { }
+ public void OnSetStyleColor(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetStyleTexture(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetFaceTextColor(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetEdgeTextColor(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetShadowTextColor(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetVisible(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetValue(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnAddText(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnStoreTextStyle(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetTextStyle(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnEditText(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetFont(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetTextRange(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnResetTextRange(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnQueryObjectType(eKResult Result, string SceneName, string ObjectName, eKObjectType ObjectType) { }
+ public void OnSetChartCSVFile(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetChartCellData(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnQueryChartDataTable(eKResult Result, string SceneName, string ObjectName, KAChartDataTable Table) { }
+ public void OnQuerySize(eKResult Result, string SceneName, string ObjectName, float Width, float Height) { }
+ public void OnSetSize(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetCounterNumberKey(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetPositionKey(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetRotationKey(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetScaleKey(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetCylinderAngleKey(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetSphereAngleKey(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetCircleAngleKey(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetCropKey(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetCountDown(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetPosition(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetRotation(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetScale(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnAddPathPoint(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnClearPathPoints(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnAddPathShapePoint(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnClearPathShapePoints(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnQueryScrollRemainingDistance(eKResult Result, string SceneName, string ObjectName, int ScrollRemainingDistance) { }
+ public void OnQueryScrollChildRemainingDistance(eKResult Result, string SceneName, string ObjectName, string ChildName, int ScrollRemainingDistance) { }
+ public void OnAddScrollObject(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnAdjustScrollSpeed(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetScrollSpeed(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetVariableName(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetLoftPositionKey(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetChangeOut(eKResult Result, string SceneName) { }
+ public void OnModifyPathPoint(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnInitScrollObject(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetCounterInfo(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetCounterNumber(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetCounterRange(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetCounterRemainingTime(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetCounterElapsedTime(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSaveObjectImage(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetPositionOfPathAnimation(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetPositionKeyOfPathAnimation(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetStartFrame(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetObjectEffectType(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetObjectOutEffectDelay(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetColor(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetColorKey(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetEmissiveColor(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetEmissiveColorKey(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetTransparencyOpacity(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetTransparencyOpacityKey(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetExposure(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetExposureKey(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetMaterialTextureType(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetMaterialTextureFile(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetMaterialTextureOffset(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetMaterialTextureOffsetKey(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetMaterialTextureTiling(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetMaterialTextureTilingKey(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetMaterialTextureRotation(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetMaterialTextureRotationKey(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetMaterialTextureOpacity(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetMaterialTextureOpacityKey(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnQueryGroupType(eKResult Result, string SceneName, string ObjectName, eKGroupType GroupType) { }
+ public void OnQueryImageType(eKResult Result, string SceneName, string ObjectName, eKImageType ImageType) { }
+ public void OnSetVideoPlayInfo(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnQueryVideoPlayInfo(eKResult Result, string SceneName, string ObjectName, ref sKVideoPlayInfo pVideoPlayInfo) { }
+ public void OnQueryIs3D(eKResult Result, string SceneName, string ObjectName, int b3D) { }
+ public void OnQueryPosition(eKResult Result, string SceneName, string ObjectName, float X, float Y, float Z) { }
+ public void OnSetImageType(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetMemo(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnQueryMemo(eKResult Result, string SceneName, string ObjectName, string Memo) { }
+ public void OnQueryFont(eKResult Result, string SceneName, string ObjectName, ref sKFont Param) { }
+ public void OnSetImageOriginalSize(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnApplyChangeEffectLibrary(eKResult Result, string SceneName) { }
+ public void OnApplyObjectLibrary(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnApplyTextureEffectLibrary(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetTableValue(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetTableColor(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnQueryTableValue(eKResult Result, string SceneName, string ObjectName, int Row, int Column, string Value) { }
+ public void OnQueryTableValues(eKResult Result, string SceneName, string ObjectName, KATableValues pValues) { }
+ public void OnSetPathShapeOutlineThickness(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnEnablePathShapeOutline(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetPlaybackCamera(eKResult Result, string SceneName) { }
+ public void OnSetMaterialTextureVideoPlayInfo(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnQueryMaterialTextureVideoPlayInfo(eKResult Result, string SceneName, string ObjectName, ref sKVideoPlayInfo VideoPlayInfo) { }
+ public void OnQueryVideoFormat(eKResult Result, ref sKVideoFormat VideoFormat) { }
+ public void OnQueryLiveStreamingStatus(eKResult Result, string StreamingURI, eKLiveStreamingStatus Status) { }
+ public void OnPreloadLiveStreaming(eKResult Result, string StreamingURI) { }
+ public void OnReleaseLiveStreaming(eKResult Result, string StreamingURI) { }
+ public void OnUpdateImageResource(eKResult Result) { }
+ public void OnQueryLayerCount(eKResult Result, int LayerCount) { }
+ public void OnSetLayerViewportRate(eKResult Result, int OutputChannelIndex, int LayerNo) { }
+ public void OnSetLayerViewportRateEx(eKResult Result, int OutputChannelIndex, int LayerNo) { }
+ public void OnSetFitting(eKResult Result, string SceneName) { }
+ public void OnSetFittingOffset(eKResult Result, string SceneName) { }
+ public void OnSetFittingScale(eKResult Result, string SceneName) { }
+ public void OnSetLightColor(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnEnableLight(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetDirectionalLight(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetPointLight(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetSpotLight(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetInfinitePointLight(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetMaterialTextureLiveStreamingURI(eKResult Result, string SceneName, string ObjectName) { }
+ public void OnSetBackgroundLiveStreamingURI(eKResult Result, string SceneName) { }
+ public void OnLoadProject(eKResult Result, string FilePath, string AliasName) { }
+ public void OnNewProject(eKResult Result, string AliasName) { }
+ public void OnUnloadAllProject(eKResult Result) { }
+ public void OnSaveProject(eKResult Result, string AliasName) { }
+ public void OnQuerySceneItemCount(eKResult Result, string AliasName, int SceneItemCount) { }
+ public void OnQuerySceneItemInfos(eKResult Result, string AliasName, KASceneItemInfos SceneItemInfos) { }
+ public void OnAddSceneItem(eKResult Result, string AliasName, int Index) { }
+ public void OnInsertSceneItem(eKResult Result, string AliasName) { }
+ public void OnDeleteSceneItem(eKResult Result, string AliasNAme) { }
+ public void OnQueryProjectFormat(eKResult Result, ref sKVideoFormat ProjectFormat) { }
+ public void OnSetTimecode(eKResult Result, string AliasName) { }
+ public void OnSetTimecodeInOut(eKResult Result, string AliasName) { }
+ public void OnSetTimecodeTrack(eKResult Result, string AliasName) { }
+ public void OnSetTimecodeInOutType(eKResult Result, string AliasName) { }
+ public void OnDeleteTimecode(eKResult Result, string AliasName) { }
+ public void OnQueryTimecode(eKResult Result, int TrackNo, int In, int Out, int bOnTrack) { }
+ public void OnUnloadProject(eKResult Result, string AliasName) { }
+ public void OnEnableSyncWithSceneEffect(eKResult Result, string AliasName) { }
+ public void OnExportProjectVideo(eKResult Result, string AliasName) { }
+ public void OnExportSceneImage(eKResult Result, string SceneName) { }
+ public void OnStartVideoCapture(eKResult Result) { }
+ public void OnStopVideoCapture(eKResult Result) { }
+ public void OnCaptureImage(eKResult Result) { }
+}
+
+[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);
+}