sega 8a6d23b77d Реорг Scripts
Разделено на 4 модуля
- Menu
- Gameplay
- Settings
- Common
2026-07-15 10:49:55 +03:00

547 lines
16 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Text;
using UnityEngine;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
/// <summary>
/// Зависимости:<br/>
/// - UnityMainThreadDispatcher<br/>
/// - ComNativeWrapper<br/>
/// </summary>
public static class ComWrapper
{
public static event Action PaymentReceived;
public static event Action<string> InputRecieved;
private const string REQUEST_INPUT_COMMAND = "91";
private const string REQUEST_ENCRYPTED_COMMAND = "666";
private const string REQUEST_CONFIRMATION_COMMAND = "777";
private static CancellationTokenSource _lifetimeCts;
private static Thread _readerThread;
private static CancellationTokenSource _readerCts;
private static int h;
public static int BoardVersion { get; private set; }
private static ILogger logger;
private static string[] AvailablePorts => ComNativeWrapper.ListPorts();
private static int _lastInputTick = 0;
private const int INPUT_IDLE_BEFORE_PING_MS = 60_000;//1 минута
public static void Initialize(string port, int boardVersion)//Добавить логгер в конструктор
{
BoardVersion = boardVersion;
if (_lifetimeCts != null) return;
_ = OpenPortAsync(port);
if (h <= 0)
{
Log("Порт не открылся при инициализации");
return;
}
UnityMainThreadDispatcher.Initialize();
_lifetimeCts = new CancellationTokenSource();
StartReaderThread();
_ = SetupBoardAsync(_lifetimeCts.Token);
}
public static void Send(string message)
{
if (h <= 0) return;
try
{
int w = ComNativeWrapper.Serial_WriteStringUTF8(h, message, 1);
}
catch { logger?.Log($"Ошибка отправки строкового сигнала. {message}"); }
}
private static void StartReaderThread()
{
StopReaderThread();
_readerCts = new CancellationTokenSource();
var ct = _readerCts.Token;
_readerThread = new Thread(() =>
{
byte[] buffer = new byte[1024];
while (!ct.IsCancellationRequested)
{
if (h <= 0) { Thread.Sleep(20); continue; }
int read = 0;
try
{
//блок до таймаута чтения
read = ComNativeWrapper.Serial_ReadLineUTF8_bytes(h, buffer, buffer.Length);
}
catch (Exception ex)
{
Log("Com reader exception: " + ex.Message);
Thread.Sleep(50);// пауза чтобы не уйти в спин при ошибке
continue;
}
if (ct.IsCancellationRequested) break;
if (read > 0)
{
var line = Encoding.UTF8.GetString(buffer, 0, read).Trim();
EnqueueLine(line);
}
}
});
_readerThread.IsBackground = true;
_readerThread.Start();
}
private static void StopReaderThread()
{
if (_readerCts != null)
{
_readerCts.Cancel();
_readerCts.Dispose();
_readerCts = null;
}
if (_readerThread != null)
{
_readerThread.Join(200);
_readerThread = null;
}
}
static void EnqueueLine(string line)
{
UnityMainThreadDispatcher.Instance.Enqueue(() => OnLine(line));
}
private static void ProcessComInput(string line)
{
Log($"COM. h={h}: {line}");
if (line == "COIN")
{
PaymentReceived?.Invoke();
Send("10");
}
else if (line.StartsWith("INPUT"))
{
Volatile.Write(ref _lastInputTick, Environment.TickCount);
string rawInputString = line.Substring(5);
string inputString = BoardVersion == 2 ? DecimalToBinary(rawInputString) : rawInputString;
InputRecieved?.Invoke(inputString);
}
}
static readonly object _waitLock = new();
static readonly List<Waiter> _waiters = new();
sealed class Waiter
{
public Func<string, bool> Match;
public TaskCompletionSource<string> Tcs;
}
static Task<string> WaitForAsync(Func<string, bool> match, int timeoutMs, CancellationToken ct)
{
var tcs = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
var w = new Waiter { Match = match, Tcs = tcs };
lock (_waitLock) _waiters.Add(w);//
//отмена
if (ct.CanBeCanceled)//
{
ct.Register(() =>
{
lock (_waitLock) _waiters.Remove(w);
tcs.TrySetCanceled(ct);
});
}
_ = Task.Delay(timeoutMs, CancellationToken.None).ContinueWith(_ =>
{
//if (tcs.Task.IsCompleted) return;
lock (_waitLock) _waiters.Remove(w);
tcs.TrySetException(new TimeoutException($"Timeout waiting for line"));
});
return tcs.Task;
}
/// <summary>
/// Очищает буфер порта от накопившихся старых данных
/// </summary>
private static void FlushPortBuffer()
{
try
{
// Проверяем валидность handle
if (h <= 0)
{
return;
}
// Используем локальный буфер
byte[] localBuf = new byte[1024];
int flushedCount = 0;
// Читаем все накопившиеся данные из порта
int read;
while ((read = ComNativeWrapper.Serial_ReadLineUTF8_bytes(h, localBuf, localBuf.Length)) > 0)
{
flushedCount++;
if (flushedCount > 100) break; // Защита от бесконечного цикла
}
Log($"Очищено: {flushedCount} сообщений");
}
catch (Exception e)
{
Log($"FlushPortBuffer exception: {e.Message}");
}
}
static Task<string> WaitForPrefixAsync(string prefix, int timeoutMs, CancellationToken ct)
=> WaitForAsync(s => s.StartsWith(prefix, StringComparison.Ordinal), timeoutMs, ct);
static Task<string> WaitForContainsAsync(string prefix, int timeoutMs, CancellationToken ct)
=> WaitForAsync(s => s.IndexOf(prefix, StringComparison.Ordinal) >= 0, timeoutMs, ct);
static void OnLine(string line)
{
if (string.IsNullOrEmpty(line))
return;
//1. обычная обработка
ProcessComInput(line);
//2. Разубудить ожидающих
List<Waiter> toFire = null;
lock (_waitLock)
{
for (int i = _waiters.Count - 1; i >= 0; i--)
{
var waiter = _waiters[i];
if (waiter.Match(line))
{
_waiters.RemoveAt(i);
(toFire ??= new List<Waiter>()).Add(waiter);
}
}
}
if (toFire != null)
foreach (var w in toFire)
w.Tcs.TrySetResult(line);
}
private static async Task SetupBoardAsync(CancellationToken ct)
{
bool auth;
if (BoardVersion == 1)
auth = await BoardAuthAsync(ct);
else
auth = true;
if (!auth)
{
Log("Авторизация на плате не пройдена.");
UnityMainThreadDispatcher.Instance.Enqueue(() => Application.Quit());
return;
}
_ = StartKeepAliveProcessAsync(ct);
}
//private static async Task OpenPortAsync(string[] availablePorts)
//{
// Log("Инициализация порта");
// var ports = availablePorts;
// if (ports.Length == 0) { Log("Нет портов, завершаю"); return; }
// string port = ports[^1];
// ComNativeWrapper.Serial_CloseAll();
// Log("Открываю порт: " + port);
// h = ComNativeWrapper.Serial_Open(port, 115200, 8, 0, 1, 4, 10);
// Log($"handle: {h}");
// if (h <= 0)
// {
// Log("Open failed: " + ComNativeWrapper.GetLastError());
// return;
// }
// else
// {
// Log($"Открыл порт: {port}");
// FlushPortBuffer();
// }
// WaitAllCoins();
//}
private static async Task OpenPortAsync(string port)
{
Log("Инициализация порта");
ComNativeWrapper.Serial_CloseAll();
Log("Открываю порт: " + port);
h = ComNativeWrapper.Serial_Open(port, 115200, 8, 0, 1, 4, 10);
Log($"handle: {h}");
if (h <= 0)
{
Log("Open failed: " + ComNativeWrapper.GetLastError());
return;
}
else
{
Log($"Открыл порт: {port}");
FlushPortBuffer();
}
WaitAllCoins();
}
static void WaitAllCoins()
{
int count = 0;
int maxCount = 100;
long timeout = 3000;
byte[] bytes = new byte[1024];
long startTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
long lastCoinTime = startTime;
while (DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - lastCoinTime < 250)
{
int w = ComNativeWrapper.Serial_ReadLineUTF8_bytes(h, bytes, bytes.Length);
if (w <= 0) continue;
if( Encoding.UTF8.GetString(bytes, 0, w) == "COIN")
{
lastCoinTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
Send("10");
count++;
}
if (count >= maxCount) break;
if(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() - startTime >= timeout) break;
}
Debug.Log($"Before game coins: {count}");
}
/// <summary>
/// Преобразует строку с десятичным числом в двоичное представление.
/// Младшие биты слева, старшие справа (перевёрнутый порядок для соответствия индексам пинов).
/// </summary>
/// <param name="decimalString">Строка с десятичным числом</param>
/// <returns>Строка с двоичным представлением</returns>
private static string DecimalToBinary(string decimalString)
{
try
{
if (long.TryParse(decimalString, out long decimalValue))
{
// Стандартное преобразование: старшие биты слева
string binary = Convert.ToString(decimalValue, 2);
// Переворачиваем строку: младшие биты слева, старшие справа
char[] chars = binary.ToCharArray();
Array.Reverse(chars);
return new string(chars);
}
Debug.Log($"Не удалось распарсить значение INPUT: {decimalString}");
return "11111111111111111111111111"; // 26 единиц по умолчанию
}
catch (Exception e)
{
Debug.Log($"Ошибка преобразования в двоичную систему: {e.Message}");
return "11111111111111111111111111"; // 26 единиц по умолчанию
}
}
private static async Task<bool> BoardAuthAsync(CancellationToken ct)
{
//Проверяем зашифрована или нет
try
{
Send(REQUEST_INPUT_COMMAND);
var inputLine = await WaitForPrefixAsync("INPUT", 1000, ct);
return true;
}
catch (TimeoutException)
{
//Не пришел инпут
Log("Шифрованная плата");
}
//Запрашиваем шифрованную строку
string encLine;
try
{
Send(REQUEST_ENCRYPTED_COMMAND);
encLine = await WaitForPrefixAsync("ENC:", 1000, ct);
}
catch
{
Log($"Зашифрованная строка не пришла");
return false;
}
//Расшифровка
var encrypted = encLine.Substring(4);
var decrypted = BoardUtility.DecryptSerial(encrypted);
//Отправляем расшифрованное на подтверждение
try
{
Send(REQUEST_CONFIRMATION_COMMAND + decrypted);
await WaitForContainsAsync("OK", 1000, ct);
return true;
}
catch
{
Log("Подтверждение не пришло");
return false;
}
}
public static void Shutdown()
{
_lifetimeCts?.Cancel();
_lifetimeCts?.Dispose();
_lifetimeCts = null;
StopReaderThread();
CancelAllWaiters();
ClosePort();
}
private static void CancelAllWaiters()
{
List<Waiter> snapshot;
lock (_waitLock)
{
snapshot = new List<Waiter>(_waiters);
_waiters.Clear();
}
foreach (Waiter waiter in snapshot)
{
waiter.Tcs.TrySetCanceled();
}
}
private static void ClosePort()
{
Log($"Отпускаю порт handle:{h}");
if (h > 0)
{
ComNativeWrapper.Serial_Close(h);
h = 0;
}
}
//Сделать ожидание простоя около минуты на проверку и переподелючение.
private static async Task StartKeepAliveProcessAsync(CancellationToken ct)
{
try
{
Log("Запуск асинхронной проверки порта");
while (!ct.IsCancellationRequested)
{
if (!IsInputIdleTooLong())
{
await Task.Delay(5000, ct);
continue;
}
//Пингуем плату
if (!await CheckPortConnectionAsync(ct))
{
Log("Плата отвалилась. Запускаю переподключение.");
await ReinitializePortAsync(ct);
}
await Task.Delay(10000, ct);
}
}
catch (OperationCanceledException)
{
Log("Асинхронная проверка порта остановлена");
}
catch (Exception e)
{
Log($"Ошибка проверки порта: {e.Message}");
}
}
private static async Task<bool> CheckPortConnectionAsync(CancellationToken ct)
{
try
{
Send(REQUEST_INPUT_COMMAND);
await WaitForPrefixAsync("INPUT", 1000, ct);
return true;
}
catch
{
return false;
}
}
//реализовать
private static async Task ReinitializePortAsync(CancellationToken ct)
{
StopReaderThread();
CancelAllWaiters();
ClosePort();
//OpenPortAsync(AvailablePorts);
OpenPortAsync(AvailablePorts[^1]);
if (h <= 0)
{
Log($"Ошибка при переподключении.");
return;
}
StartReaderThread();
var auth = await BoardAuthAsync(ct);
if (!auth)
UnityMainThreadDispatcher.Instance.Enqueue(() => (new GameObject()).AddComponent(typeof(VfxPrefab)));
}
static bool IsInputIdleTooLong()
{
int last = Volatile.Read(ref _lastInputTick);
if (last == 0) return true;
int now = Environment.TickCount;
// переполнение TickCount тут ок: сравнение интервала < 24 дней работает корректно
int elapsed = unchecked(now - last);
return elapsed >= INPUT_IDLE_BEFORE_PING_MS;
}
private static void Log(string message) => logger?.Log($"CW{message}");
}