Files
Tornado3_2026Election/Tornado3_2026Election/Services/TornadoManager.cs

522 lines
16 KiB
C#

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
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 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<int, KAScenePlayer> _scenePlayers = new();
private IKAEngine? _engine;
private KAEventHandler? _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 _dispatcher.InvokeAsync(() => EnsureConnectedCore(), cancellationToken);
}
public Task<string> LoadSceneAsync(string scenePath, string sceneAlias, CancellationToken cancellationToken)
{
return _dispatcher.InvokeAsync(() =>
{
ThrowIfDisposed();
EnsureConnectedCore();
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));
}
if (_scenePaths.TryGetValue(sceneAlias, out var existingPath) &&
string.Equals(existingPath, scenePath, StringComparison.OrdinalIgnoreCase))
{
return sceneAlias;
}
var forceReload = _scenePaths.ContainsKey(sceneAlias);
var scene = forceReload
? _engine!.LoadSceneForce(scenePath, sceneAlias)
: _engine!.LoadScene(scenePath, sceneAlias);
_logService.Info(
$"Karisma {(forceReload ? "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;
_logService.Info($"Karisma scene loaded: alias={sceneAlias} path={scenePath}");
return sceneAlias;
}, cancellationToken);
}
public Task ApplyValuesAsync(string sceneAlias, IReadOnlyDictionary<string, string> values, CancellationToken cancellationToken)
{
return _dispatcher.InvokeAsync(() =>
{
ThrowIfDisposed();
EnsureConnectedCore();
var scene = GetSceneCore(sceneAlias);
_engine!.BeginTransaction();
try
{
foreach (var pair in values)
{
if (string.IsNullOrWhiteSpace(pair.Key))
{
continue;
}
try
{
var sceneObject = scene.GetObject(pair.Key);
if (sceneObject is null)
{
continue;
}
sceneObject.SetValue(pair.Value ?? string.Empty);
}
catch (Exception ex)
{
_logService.Warning($"Karisma object update skipped: scene={sceneAlias} object={pair.Key} reason={ex.Message}");
}
}
}
finally
{
_engine!.EndTransaction();
}
}, cancellationToken);
}
public Task PrepareAsync(int outputChannelIndex, int layerNo, string sceneAlias, CancellationToken cancellationToken)
{
return _dispatcher.InvokeAsync(() =>
{
ThrowIfDisposed();
EnsureConnectedCore();
GetScenePlayerCore(outputChannelIndex).Prepare(layerNo, GetSceneCore(sceneAlias));
}, cancellationToken);
}
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 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)
{
UpdateTcpConnectionState(true);
return;
}
_connected = false;
UpdateTcpConnectionState(false);
}
private void HandleClose(int errorCode)
{
_connected = false;
UpdateTcpConnectionState(false);
}
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 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;
}
}
}