Files
Tornado3_2026Election/Tornado3_2026Election/Services/TornadoManager.cs
2026-05-13 11:21:48 +09:00

1107 lines
38 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using KAsyncEngineLib;
namespace Tornado3_2026Election.Services;
public sealed class TornadoManager : IDisposable
{
private static readonly TimeSpan AutoReconnectInterval = TimeSpan.FromSeconds(5);
private static readonly TimeSpan AsyncOperationTimeout = TimeSpan.FromSeconds(30);
private readonly string _host;
private readonly int _port;
private readonly LogService _logService;
private readonly StaDispatcher _dispatcher;
private readonly Timer _reconnectTimer;
private readonly Dictionary<string, KAScene> _scenes = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, string> _scenePaths = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, DateTime> _sceneWriteTimes = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<int, KAScenePlayer> _scenePlayers = new();
private IKAEngine? _engine;
private KarismaEventHandler? _eventHandler;
private bool _connected;
private bool _isTcpConnected;
private bool _autoReconnectEnabled;
private bool _disposed;
private int _reconnectInProgress;
public bool IsConnected => _isTcpConnected;
public event EventHandler? ConnectionChanged;
public TornadoManager(string host, int port, LogService logService)
{
_host = string.IsNullOrWhiteSpace(host)
? throw new ArgumentException("Host is required.", nameof(host))
: host;
_port = port > 0
? port
: throw new ArgumentOutOfRangeException(nameof(port), "Port must be a positive integer.");
_logService = logService;
_dispatcher = new StaDispatcher("Karisma Async Engine");
_reconnectTimer = new Timer(OnReconnectTimerTick, null, AutoReconnectInterval, AutoReconnectInterval);
}
public Task EnsureConnectedAsync(CancellationToken cancellationToken)
{
return EnsureConnectedInternalAsync(cancellationToken);
}
public Task<string> LoadSceneAsync(string scenePath, string sceneAlias, CancellationToken cancellationToken)
{
return LoadSceneAsync(scenePath, sceneAlias, forceReload: false, cancellationToken);
}
public async Task<string> LoadSceneAsync(
string scenePath,
string sceneAlias,
bool forceReload,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(scenePath))
{
throw new ArgumentException("Scene path is required.", nameof(scenePath));
}
if (string.IsNullOrWhiteSpace(sceneAlias))
{
throw new ArgumentException("Scene alias is required.", nameof(sceneAlias));
}
var sceneWriteTime = ResolveSceneWriteTime(scenePath);
var existingAlias = await _dispatcher.InvokeAsync(() =>
{
ThrowIfDisposed();
EnsureConnectedCore();
if (!forceReload &&
_scenePaths.TryGetValue(sceneAlias, out var existingPath) &&
string.Equals(existingPath, scenePath, StringComparison.OrdinalIgnoreCase) &&
_sceneWriteTimes.TryGetValue(sceneAlias, out var existingWriteTime) &&
existingWriteTime == sceneWriteTime)
{
return sceneAlias;
}
return string.Empty;
}, cancellationToken).ConfigureAwait(false);
if (!string.IsNullOrWhiteSpace(existingAlias))
{
return existingAlias;
}
var eventHandler = GetEventHandlerCore();
var completion = eventHandler.BeginLoadSceneWait(sceneAlias);
try
{
await _dispatcher.InvokeAsync(() =>
{
ThrowIfDisposed();
EnsureConnectedCore();
var shouldForceReload = forceReload || _scenePaths.ContainsKey(sceneAlias);
var scene = shouldForceReload
? _engine!.LoadSceneForce(scenePath, sceneAlias)
: _engine!.LoadScene(scenePath, sceneAlias);
_logService.Info(
$"Karisma {(shouldForceReload ? "LoadSceneForce" : "LoadScene")}() return={(scene is null ? "null" : "scene-handle")} alias={sceneAlias} path={scenePath}");
_scenes[sceneAlias] = scene ?? throw new InvalidOperationException($"Failed to load Karisma scene: {scenePath}");
_scenePaths[sceneAlias] = scenePath;
_sceneWriteTimes[sceneAlias] = sceneWriteTime;
}, cancellationToken).ConfigureAwait(false);
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(AsyncOperationTimeout);
var result = await completion.WaitAsync(timeoutCts.Token).ConfigureAwait(false);
if (result != eKResult.RESULT_SUCCESS)
{
throw new InvalidOperationException($"LoadScene failed for '{sceneAlias}': {result} ({(int)result})");
}
_logService.Info($"Karisma scene loaded: alias={sceneAlias} path={scenePath}");
return sceneAlias;
}
catch (Exception ex)
{
eventHandler.CancelPendingLoadScene(sceneAlias, ex);
try
{
await _dispatcher.InvokeAsync(() =>
{
_scenes.Remove(sceneAlias);
_scenePaths.Remove(sceneAlias);
_sceneWriteTimes.Remove(sceneAlias);
}, CancellationToken.None).ConfigureAwait(false);
}
catch
{
}
throw;
}
}
public async Task ApplyValuesAsync(
string sceneAlias,
IReadOnlyList<KarismaVisibilityUpdate> visibilityUpdatesBeforeValue,
IReadOnlyDictionary<string, string> values,
IReadOnlyList<KarismaCounterNumberKeyUpdate> counterNumberKeys,
IReadOnlyList<KarismaChartCellUpdate> chartCellUpdates,
IReadOnlyList<KarismaPositionUpdate> positionUpdates,
IReadOnlyList<KarismaStyleColorUpdate> styleColorUpdates,
IReadOnlyList<KarismaVisibilityUpdate> visibilityUpdatesAfterValue,
CancellationToken cancellationToken)
{
Task<eKResult>? transactionCompletion = null;
var eventHandler = GetEventHandlerCore();
try
{
await _dispatcher.InvokeAsync(() =>
{
ThrowIfDisposed();
EnsureConnectedCore();
var scene = GetSceneCore(sceneAlias);
var counterNumberKeyObjectNames = counterNumberKeys
.Where(update => update.KeyIndex != 0 && !string.IsNullOrWhiteSpace(update.ObjectName))
.Select(update => update.ObjectName)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var counterSetValueObjectNames = counterNumberKeys
.Where(update => update.AllowSetValue && !string.IsNullOrWhiteSpace(update.ObjectName))
.Select(update => update.ObjectName)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
_engine!.BeginTransaction();
try
{
ApplyVisibilityUpdates(sceneAlias, scene, visibilityUpdatesBeforeValue);
foreach (var pair in values)
{
if (string.IsNullOrWhiteSpace(pair.Key))
{
continue;
}
try
{
var sceneObject = scene.GetObject(pair.Key);
if (sceneObject is null)
{
continue;
}
if (counterNumberKeyObjectNames.Contains(pair.Key) &&
!counterSetValueObjectNames.Contains(pair.Key) &&
sceneObject is IKACounter)
{
continue;
}
sceneObject.SetValue(pair.Value ?? string.Empty);
}
catch (Exception ex)
{
_logService.Warning($"Karisma object update skipped: scene={sceneAlias} object={pair.Key} reason={ex.Message}");
}
}
foreach (var counterNumberKey in counterNumberKeys)
{
if (string.IsNullOrWhiteSpace(counterNumberKey.ObjectName))
{
continue;
}
try
{
if (counterNumberKey.KeyIndex == 0 && !counterNumberKey.AllowKeyZero)
{
continue;
}
var sceneObject = scene.GetObject(counterNumberKey.ObjectName);
if (sceneObject is not IKACounter counter)
{
_logService.Warning(
$"Karisma counter update skipped: scene={sceneAlias} object={counterNumberKey.ObjectName} reason=object is not a counter");
continue;
}
counter.SetCounterNumberKey(counterNumberKey.KeyIndex, counterNumberKey.Number);
}
catch (Exception ex)
{
_logService.Warning(
$"Karisma counter update skipped: scene={sceneAlias} object={counterNumberKey.ObjectName} keyIndex={counterNumberKey.KeyIndex} reason={ex.Message}");
}
}
foreach (var chartCellUpdate in chartCellUpdates)
{
if (string.IsNullOrWhiteSpace(chartCellUpdate.ObjectName))
{
continue;
}
try
{
var sceneObject = scene.GetObject(chartCellUpdate.ObjectName);
if (sceneObject is not IKAChart chart)
{
_logService.Warning(
$"Karisma chart cell update skipped: scene={sceneAlias} object={chartCellUpdate.ObjectName} reason=object is not a chart");
continue;
}
chart.SetChartCellData(chartCellUpdate.Row, chartCellUpdate.Column, chartCellUpdate.Value);
}
catch (Exception ex)
{
_logService.Warning(
$"Karisma chart cell update skipped: scene={sceneAlias} object={chartCellUpdate.ObjectName} row={chartCellUpdate.Row} column={chartCellUpdate.Column} reason={ex.Message}");
}
}
foreach (var positionUpdate in positionUpdates)
{
if (string.IsNullOrWhiteSpace(positionUpdate.ObjectName))
{
continue;
}
try
{
var sceneObject = scene.GetObject(positionUpdate.ObjectName);
if (sceneObject is null)
{
continue;
}
if (positionUpdate.KeyIndex >= 0)
{
sceneObject.SetPositionKey(
positionUpdate.KeyIndex,
positionUpdate.X,
positionUpdate.Y,
positionUpdate.Z,
positionUpdate.VectorType);
}
else
{
sceneObject.SetPosition(
positionUpdate.X,
positionUpdate.Y,
positionUpdate.Z,
positionUpdate.VectorType);
}
}
catch (Exception ex)
{
_logService.Warning(
$"Karisma position update skipped: scene={sceneAlias} object={positionUpdate.ObjectName} keyIndex={positionUpdate.KeyIndex} reason={ex.Message}");
}
}
foreach (var styleColorUpdate in styleColorUpdates)
{
if (string.IsNullOrWhiteSpace(styleColorUpdate.ObjectName))
{
continue;
}
try
{
var sceneObject = scene.GetObject(styleColorUpdate.ObjectName);
if (sceneObject is not IKAStyle style)
{
_logService.Warning(
$"Karisma style color update skipped: scene={sceneAlias} object={styleColorUpdate.ObjectName} reason=object does not implement IKAStyle");
continue;
}
style.SetStyleColor(
styleColorUpdate.StyleType,
styleColorUpdate.Order,
styleColorUpdate.R,
styleColorUpdate.G,
styleColorUpdate.B,
styleColorUpdate.A);
}
catch (Exception ex)
{
_logService.Warning(
$"Karisma style color update skipped: scene={sceneAlias} object={styleColorUpdate.ObjectName} style={styleColorUpdate.StyleType} order={styleColorUpdate.Order} reason={ex.Message}");
}
}
ApplyVisibilityUpdates(sceneAlias, scene, visibilityUpdatesAfterValue);
}
finally
{
transactionCompletion = eventHandler.BeginEndTransactionWait();
_engine!.EndTransaction();
}
}, cancellationToken).ConfigureAwait(false);
if (transactionCompletion is not null)
{
var transactionResult = await WaitForKarismaResultAsync(transactionCompletion, cancellationToken).ConfigureAwait(false);
ThrowIfKarismaFailed(transactionResult, $"EndTransaction failed for scene={sceneAlias}");
}
await UpdateSceneTexturesAsync(sceneAlias, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
eventHandler.CancelPendingEndTransaction(ex);
eventHandler.CancelPendingUpdateTextures(sceneAlias, ex);
throw;
}
}
private static DateTime ResolveSceneWriteTime(string scenePath)
{
try
{
return File.GetLastWriteTimeUtc(scenePath);
}
catch
{
return DateTime.MinValue;
}
}
private void ApplyVisibilityUpdates(string sceneAlias, KAScene scene, IReadOnlyList<KarismaVisibilityUpdate> visibilityUpdates)
{
foreach (var visibilityUpdate in visibilityUpdates)
{
if (string.IsNullOrWhiteSpace(visibilityUpdate.ObjectName))
{
continue;
}
try
{
var sceneObject = scene.GetObject(visibilityUpdate.ObjectName);
if (sceneObject is null)
{
continue;
}
sceneObject.SetVisible(visibilityUpdate.IsVisible ? 1 : 0);
}
catch (Exception ex)
{
_logService.Warning(
$"Karisma visibility update skipped: scene={sceneAlias} object={visibilityUpdate.ObjectName} visible={visibilityUpdate.IsVisible} reason={ex.Message}");
}
}
}
private async Task UpdateSceneTexturesAsync(string sceneAlias, CancellationToken cancellationToken)
{
var eventHandler = GetEventHandlerCore();
var completion = eventHandler.BeginUpdateTexturesWait(sceneAlias);
try
{
await _dispatcher.InvokeAsync(() =>
{
ThrowIfDisposed();
EnsureConnectedCore();
GetSceneCore(sceneAlias).UpdateTextures();
}, cancellationToken).ConfigureAwait(false);
var result = await WaitForKarismaResultAsync(completion, cancellationToken).ConfigureAwait(false);
ThrowIfKarismaFailed(result, $"UpdateTextures failed for scene={sceneAlias}");
}
catch (Exception ex)
{
eventHandler.CancelPendingUpdateTextures(sceneAlias, ex);
throw;
}
}
public async Task PrepareAsync(int outputChannelIndex, int layerNo, string sceneAlias, CancellationToken cancellationToken)
{
var eventHandler = GetEventHandlerCore();
var completion = eventHandler.BeginScenePrepareWait(outputChannelIndex, layerNo);
try
{
await _dispatcher.InvokeAsync(() =>
{
ThrowIfDisposed();
EnsureConnectedCore();
GetScenePlayerCore(outputChannelIndex).Prepare(layerNo, GetSceneCore(sceneAlias));
}, cancellationToken).ConfigureAwait(false);
var result = await WaitForKarismaResultAsync(completion, cancellationToken).ConfigureAwait(false);
ThrowIfKarismaFailed(result, $"Prepare failed for output={outputChannelIndex} layer={layerNo} scene={sceneAlias}");
// Give Karisma one render tick after Prepare before a mixed-preview screenshot is requested.
await Task.Delay(TimeSpan.FromMilliseconds(250), cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
eventHandler.CancelPendingScenePrepare(outputChannelIndex, layerNo, ex);
throw;
}
}
public Task PlayAsync(int outputChannelIndex, int layerNo, bool cutIn, CancellationToken cancellationToken)
{
return _dispatcher.InvokeAsync(() =>
{
ThrowIfDisposed();
EnsureConnectedCore();
var scenePlayer = GetScenePlayerCore(outputChannelIndex);
if (cutIn)
{
scenePlayer.CutIn(layerNo);
return;
}
scenePlayer.Play(layerNo);
}, cancellationToken);
}
public Task PlayOutAsync(int outputChannelIndex, int layerNo, bool cutOut, CancellationToken cancellationToken)
{
return _dispatcher.InvokeAsync(() =>
{
ThrowIfDisposed();
EnsureConnectedCore();
var scenePlayer = GetScenePlayerCore(outputChannelIndex);
if (cutOut)
{
scenePlayer.CutOut(layerNo);
return;
}
scenePlayer.PlayOut(layerNo);
}, cancellationToken);
}
public Task ClearNextPreviewAsync(int outputChannelIndex, int layerNo, CancellationToken cancellationToken)
{
return _dispatcher.InvokeAsync(() =>
{
ThrowIfDisposed();
EnsureConnectedCore();
GetScenePlayerCore(outputChannelIndex).ClearNextPreview(layerNo);
}, cancellationToken);
}
public async Task SaveMixedPreviewImageAsync(
int outputChannelIndex,
int layerNo,
string fileName,
int width,
int height,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(fileName))
{
throw new ArgumentException("Image file path is required.", nameof(fileName));
}
await _dispatcher.InvokeAsync(() =>
{
ThrowIfDisposed();
EnsureConnectedCore();
}, cancellationToken).ConfigureAwait(false);
var eventHandler = GetEventHandlerCore();
var completion = eventHandler.BeginSaveMixedPreviewImageWait();
try
{
await _dispatcher.InvokeAsync(() =>
{
ThrowIfDisposed();
EnsureConnectedCore();
var directory = Path.GetDirectoryName(fileName);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
GetScenePlayerCore(outputChannelIndex).SaveMixedPreviewImage(fileName, width, height);
}, cancellationToken).ConfigureAwait(false);
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(TimeSpan.FromSeconds(30));
var result = await completion.WaitAsync(timeoutCts.Token).ConfigureAwait(false);
if (result.Result != eKResult.RESULT_SUCCESS)
{
throw new InvalidOperationException(
$"SaveMixedPreviewImage failed for output={outputChannelIndex} layer={layerNo}: {result.Result} ({(int)result.Result})");
}
await WaitForFileAsync(fileName, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
eventHandler.CancelPendingSaveMixedPreviewImage(ex);
throw;
}
}
public async Task SaveSceneImageAsync(
string sceneAlias,
string fileName,
int width,
int height,
int frame,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(sceneAlias))
{
throw new ArgumentException("Scene alias is required.", nameof(sceneAlias));
}
if (string.IsNullOrWhiteSpace(fileName))
{
throw new ArgumentException("Image file path is required.", nameof(fileName));
}
await _dispatcher.InvokeAsync(() =>
{
ThrowIfDisposed();
EnsureConnectedCore();
}, cancellationToken).ConfigureAwait(false);
var eventHandler = GetEventHandlerCore();
var completion = eventHandler.BeginSaveSceneImageWait();
try
{
await _dispatcher.InvokeAsync(() =>
{
ThrowIfDisposed();
EnsureConnectedCore();
var directory = Path.GetDirectoryName(fileName);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
GetSceneCore(sceneAlias).SaveSceneImage(fileName, width, height, frame);
}, cancellationToken).ConfigureAwait(false);
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(TimeSpan.FromSeconds(30));
var result = await completion.WaitAsync(timeoutCts.Token).ConfigureAwait(false);
if (result.Result != eKResult.RESULT_SUCCESS)
{
throw new InvalidOperationException($"SaveSceneImage failed for '{sceneAlias}': {result.Result} ({(int)result.Result})");
}
await WaitForFileAsync(fileName, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex)
{
eventHandler.CancelPendingSaveSceneImage(ex);
throw;
}
}
public Task UnloadSceneAsync(string sceneAlias, CancellationToken cancellationToken)
{
return _dispatcher.InvokeAsync(() =>
{
ThrowIfDisposed();
if (!_scenes.TryGetValue(sceneAlias, out var scene))
{
return;
}
scene.UnloadScene();
_scenes.Remove(sceneAlias);
_scenePaths.Remove(sceneAlias);
}, cancellationToken);
}
public Task TriggerAsync(int outputChannelIndex, int layerNo, string animationName, CancellationToken cancellationToken)
{
return _dispatcher.InvokeAsync(() =>
{
ThrowIfDisposed();
EnsureConnectedCore();
if (string.IsNullOrWhiteSpace(animationName))
{
return;
}
GetScenePlayerCore(outputChannelIndex).Trigger(layerNo, animationName);
}, cancellationToken);
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
_autoReconnectEnabled = false;
_reconnectTimer.Dispose();
try
{
_dispatcher.InvokeAsync(() =>
{
foreach (var scene in _scenes.Values)
{
try
{
scene.UnloadScene();
}
catch
{
}
}
try
{
if (_connected)
{
_engine?.Disconnect();
}
}
catch
{
}
_scenes.Clear();
_scenePaths.Clear();
_scenePlayers.Clear();
_engine = null;
_eventHandler = null;
_connected = false;
}, CancellationToken.None).GetAwaiter().GetResult();
}
finally
{
_dispatcher.Dispose();
}
}
private void EnsureConnectedCore()
{
ThrowIfDisposed();
if (_connected)
{
return;
}
_engine ??= (IKAEngine)new KAEngineClass();
_eventHandler ??= new KarismaEventHandler(_logService, HandleConnect, HandleClose);
_autoReconnectEnabled = true;
var requested = _engine.Connect(_host, _port, _eventHandler);
_logService.Info($"Karisma Connect() return={(requested != 0 ? "TRUE" : "FALSE")} raw={requested} target={_host}:{_port}");
if (requested == 0)
{
throw new InvalidOperationException($"Karisma Async Engine connection request failed: {_host}:{_port}");
}
_connected = true;
_logService.Info($"Karisma Async Engine connect requested: {_host}:{_port}");
}
private void HandleConnect(int errorCode)
{
if (errorCode == 0)
{
_connected = true;
UpdateTcpConnectionState(true);
return;
}
_connected = false;
UpdateTcpConnectionState(false);
}
private void HandleClose(int errorCode)
{
_connected = false;
UpdateTcpConnectionState(false);
if (_eventHandler is not null)
{
var error = new IOException($"Karisma connection closed while waiting for a callback (errorCode={errorCode}).");
_eventHandler.CancelPendingConnect(error);
_eventHandler.CancelAllPendingLoadScenes(error);
_eventHandler.CancelPendingSaveSceneImage(error);
}
}
private void UpdateTcpConnectionState(bool isConnected)
{
if (_isTcpConnected == isConnected)
{
return;
}
_isTcpConnected = isConnected;
ConnectionChanged?.Invoke(this, EventArgs.Empty);
}
private void OnReconnectTimerTick(object? state)
{
if (_disposed || !_autoReconnectEnabled || _connected)
{
return;
}
if (Interlocked.Exchange(ref _reconnectInProgress, 1) == 1)
{
return;
}
_ = TryAutoReconnectAsync();
}
private async Task TryAutoReconnectAsync()
{
try
{
if (_disposed || !_autoReconnectEnabled || _connected)
{
return;
}
await _dispatcher.InvokeAsync(() =>
{
if (_disposed || _connected)
{
return;
}
try
{
EnsureConnectedCore();
}
catch (Exception ex)
{
_logService.Warning($"Karisma auto reconnect attempt failed: {ex.Message}");
}
}, CancellationToken.None).ConfigureAwait(false);
}
finally
{
Interlocked.Exchange(ref _reconnectInProgress, 0);
}
}
private KAScene GetSceneCore(string sceneAlias)
{
if (_scenes.TryGetValue(sceneAlias, out var scene))
{
return scene;
}
throw new InvalidOperationException($"Karisma scene is not loaded: {sceneAlias}");
}
private KAScenePlayer GetScenePlayerCore(int outputChannelIndex)
{
if (_scenePlayers.TryGetValue(outputChannelIndex, out var scenePlayer))
{
return scenePlayer;
}
scenePlayer = _engine!.GetScenePlayer(outputChannelIndex)
?? throw new InvalidOperationException($"Karisma scene player is unavailable: output={outputChannelIndex}");
_scenePlayers[outputChannelIndex] = scenePlayer;
return scenePlayer;
}
private KarismaEventHandler GetEventHandlerCore()
{
return _eventHandler ?? throw new InvalidOperationException("Karisma event handler is unavailable.");
}
private static async Task<eKResult> WaitForKarismaResultAsync(Task<eKResult> completion, CancellationToken cancellationToken)
{
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(AsyncOperationTimeout);
return await completion.WaitAsync(timeoutCts.Token).ConfigureAwait(false);
}
private static void ThrowIfKarismaFailed(eKResult result, string message)
{
if (result != eKResult.RESULT_SUCCESS)
{
throw new InvalidOperationException($"{message}: {result} ({(int)result})");
}
}
private async Task EnsureConnectedInternalAsync(CancellationToken cancellationToken)
{
Task<int>? connectCompletion = null;
try
{
await _dispatcher.InvokeAsync(() =>
{
ThrowIfDisposed();
if (_isTcpConnected)
{
return;
}
_engine ??= (IKAEngine)new KAEngineClass();
_eventHandler ??= new KarismaEventHandler(_logService, HandleConnect, HandleClose);
_autoReconnectEnabled = true;
connectCompletion = _eventHandler.BeginConnectWait();
if (_connected)
{
return;
}
var requested = _engine.Connect(_host, _port, _eventHandler);
_logService.Info($"Karisma Connect() return={(requested != 0 ? "TRUE" : "FALSE")} raw={requested} target={_host}:{_port}");
if (requested == 0)
{
throw new InvalidOperationException($"Karisma Async Engine connection request failed: {_host}:{_port}");
}
_connected = true;
_logService.Info($"Karisma Async Engine connect requested: {_host}:{_port}");
}, cancellationToken).ConfigureAwait(false);
if (connectCompletion is null)
{
return;
}
using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeoutCts.CancelAfter(AsyncOperationTimeout);
var errorCode = await connectCompletion.WaitAsync(timeoutCts.Token).ConfigureAwait(false);
if (errorCode != 0)
{
throw new InvalidOperationException($"Karisma Async Engine OnConnect failed: errorCode={errorCode}");
}
}
catch (Exception ex)
{
if (_eventHandler is not null)
{
_eventHandler.CancelPendingConnect(ex);
}
try
{
await _dispatcher.InvokeAsync(() =>
{
_connected = false;
_isTcpConnected = false;
}, CancellationToken.None).ConfigureAwait(false);
}
catch
{
}
throw;
}
}
private static async Task WaitForFileAsync(string fileName, CancellationToken cancellationToken)
{
for (var attempt = 0; attempt < 20; attempt++)
{
if (File.Exists(fileName))
{
var fileInfo = new FileInfo(fileName);
if (fileInfo.Length > 0)
{
return;
}
}
await Task.Delay(150, cancellationToken).ConfigureAwait(false);
}
throw new IOException($"Saved scene image was not created: {fileName}");
}
private void ThrowIfDisposed()
{
ObjectDisposedException.ThrowIf(_disposed, this);
}
private sealed class StaDispatcher : IDisposable
{
private const uint WmApp = 0x8000;
private const uint WmDispatch = WmApp + 1;
private readonly ConcurrentQueue<Action> _queue = new();
private readonly Thread _thread;
private readonly ManualResetEventSlim _threadReady = new();
private bool _disposed;
private uint _threadId;
public StaDispatcher(string threadName)
{
_thread = new Thread(Run)
{
IsBackground = true,
Name = threadName
};
_thread.SetApartmentState(ApartmentState.STA);
_thread.Start();
_threadReady.Wait();
}
public Task InvokeAsync(Action action, CancellationToken cancellationToken)
{
return InvokeAsync(() =>
{
action();
return true;
}, cancellationToken);
}
public Task<T> InvokeAsync<T>(Func<T> action, CancellationToken cancellationToken)
{
ObjectDisposedException.ThrowIf(_disposed, this);
cancellationToken.ThrowIfCancellationRequested();
var completion = new TaskCompletionSource<T>(TaskCreationOptions.RunContinuationsAsynchronously);
_queue.Enqueue(() =>
{
if (cancellationToken.IsCancellationRequested)
{
completion.TrySetCanceled(cancellationToken);
return;
}
try
{
completion.TrySetResult(action());
}
catch (Exception 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;
}
public void Dispose()
{
if (_disposed)
{
return;
}
_disposed = true;
PostThreadMessage(_threadId, WmQuit, UIntPtr.Zero, IntPtr.Zero);
_thread.Join();
_threadReady.Dispose();
}
private void Run()
{
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;
}
}
}