Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
Архитектура COM и роли - ComMultiManager переименован в ComHub и переведен на фабричный паттерн обработчиков по ROLE. - Добавлен единый контракт обработчиков COM-контроллеров: - IComControllerHandler - DefaultComControllerHandler - Ответственность за оркестрацию перенесена в ComSystemManager, чтобы инициализация COM и жизненный цикл контроллеров управлялись централизованно. - ComWrapper сохранен как слой совместимости на время миграции логики в новую схему. - PrizeStandController отрефакторен под новый role-handler pipeline вместо старой прямой привязки. - Добавлен удобный доступ к PrizeStandController из менеджера системы там, где это требуется игровому коду. Handshake/авторизация платы (ComDeviceClient) - Авторизация платы переработана в строгую поэтапную последовательность: 1) ПК отправляет 07 2) ожидает CHAL 3) отправляет 08 в ответ на CHAL 4) ожидает RAES - Парсер стадий теперь принимает только ожидаемые токены на каждой стадии: - WaitingChal -> только CHAL - WaitingRaes -> только RAES - Любой посторонний вывод из serial во время handshake игнорируется (без немедленного падения), что устраняет ложные ошибки на нерелевантных COM-устройствах. - Если ожидаемый ответ после 07 не приходит, порт быстро пропускается как нерелевантный, без эскалации в критическую ошибку. - Для случаев с неподходящими/сторонними устройствами уровень логирования снижен с ошибок до предупреждений там, где это уместно. - Добавлена подробная диагностика авторизации (переходы стадий, принятые значимые кадры, контекст таймаутов) для упрощения отладки. Тестовая утилита handshake - Добавлен Assets/Temp/Handshake_Test.cs как изолированный диагностический скрипт. - Включены очень подробные логи по каждому шагу handshake и по каждому решению парсера. - Позволяет отдельно гонять тестовый сценарий и сравнивать поведение платы, не ломая production-поток. Стабильность COM и поведение reconnect - Подправлена логика keep-alive, чтобы исключить агрессивные реконнекты. - Reconnect теперь срабатывает только после нескольких последовательных пропусков INPUT-ответа, что снижает случайные обрывы COM и прерывания подсветки. Поведение подсветки во время выполнения - Циклы подсветки сделаны независимыми от Time.timeScale: - LightController: WaitForSeconds -> WaitForSecondsRealtime - LightSequence: WaitForSeconds -> WaitForSecondsRealtime - Это устраняет случайные паузы/замедления световых циклов при изменении игрового времени (пауза, slow-mo, временные изменения scale). Итоговый эффект - COM-стек стал расширяемым по ролям и проще в сопровождении. - Handshake стал детерминированным, устойчивым к serial-шуму и безопасным при работе со сторонними устройствами. - Циклы подсветки стабилизированы для длительных сессий и больше не зависят от масштабирования игрового времени.
306 lines
10 KiB
C#
306 lines
10 KiB
C#
using System;
|
|
using RR.Game2D.Core.Managers;
|
|
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
|
|
namespace RR.Common.Controls
|
|
{
|
|
/// <summary>
|
|
/// Публичный компонент COM-системы.
|
|
/// </summary>
|
|
[AddComponentMenu("RR/Controls/Com System Manager")]
|
|
public class ComSystemManager : MonoBehaviour
|
|
{
|
|
private enum ControllerHandlerType
|
|
{
|
|
Default = 0,
|
|
PrizeStand = 1
|
|
}
|
|
|
|
[Serializable]
|
|
private sealed class RoleHandlerBinding
|
|
{
|
|
public string role;
|
|
public ControllerHandlerType handlerType = ControllerHandlerType.Default;
|
|
[Min(0)] public int prizeStandBoxesCount;
|
|
}
|
|
|
|
[Header("Основные настройки")]
|
|
[SerializeField] private bool saveMoneyBetweenSessions;
|
|
[SerializeField] private bool enablePrizeChecking;
|
|
[SerializeField] private bool keepAlive = true;
|
|
|
|
[Header("Авторизация")]
|
|
[SerializeField] private bool enableEncryptedHandshake = true;
|
|
[SerializeField] private int handshakeTimeoutMs = 5000;
|
|
|
|
[Header("Роль контроллера (после handshake)")]
|
|
[SerializeField] private string roleRequestCommand = "97";
|
|
[SerializeField] private string roleResponsePrefix = "ROLE";
|
|
[SerializeField] private int roleRequestTimeoutMs = 1500;
|
|
[SerializeField] private string mainControllerRole = "main_controller";
|
|
|
|
[Header("Мульти-устройства")]
|
|
[SerializeField] private bool useAllDetectedPorts = true;
|
|
[SerializeField] private string[] explicitPorts = Array.Empty<string>();
|
|
[SerializeField] private string defaultDevicePort = "";
|
|
|
|
[Header("Фабрика обработчиков по ROLE")]
|
|
[SerializeField] private RoleHandlerBinding[] roleHandlerBindings =
|
|
{
|
|
new RoleHandlerBinding { role = "prize_stand", handlerType = ControllerHandlerType.PrizeStand, prizeStandBoxesCount = 24 },
|
|
new RoleHandlerBinding { role = "prize_stand_controller", handlerType = ControllerHandlerType.PrizeStand, prizeStandBoxesCount = 24 }
|
|
};
|
|
|
|
public static UnityEvent<int> OnCreditAdded;
|
|
public static UnityEvent<int> OnCreditRemoved;
|
|
public static UnityEvent<string> OnInputReceived;
|
|
public static Action OnInitialize;
|
|
|
|
public static UnityEvent<string, string> OnDeviceInputReceived;
|
|
public static UnityEvent<string> OnDeviceAuthorized;
|
|
|
|
private static ComSystemManager _instance;
|
|
private static int _creditsCount;
|
|
|
|
private ComHub _manager;
|
|
private LightController _light;
|
|
private bool _initializeInvoked;
|
|
|
|
public LightController Light => _light;
|
|
|
|
public void Initialize()
|
|
{
|
|
_instance = this;
|
|
|
|
OnCreditAdded = new UnityEvent<int>();
|
|
OnCreditRemoved = new UnityEvent<int>();
|
|
OnInputReceived = new UnityEvent<string>();
|
|
OnDeviceInputReceived = new UnityEvent<string, string>();
|
|
OnDeviceAuthorized = new UnityEvent<string>();
|
|
|
|
if (!saveMoneyBetweenSessions) PlayerPrefs.SetInt("CoinsCount", 0);
|
|
_creditsCount = PlayerPrefs.GetInt("CoinsCount", 0);
|
|
|
|
_light = GetComponent<LightController>() ?? gameObject.AddComponent<LightController>();
|
|
|
|
_manager = new ComHub(
|
|
useAllDetectedPorts,
|
|
explicitPorts,
|
|
defaultDevicePort,
|
|
enableEncryptedHandshake,
|
|
handshakeTimeoutMs,
|
|
keepAlive || enablePrizeChecking,
|
|
roleRequestCommand,
|
|
roleResponsePrefix,
|
|
roleRequestTimeoutMs,
|
|
mainControllerRole,
|
|
CreateRoleHandler);
|
|
|
|
_manager.InitializeDevices();
|
|
}
|
|
|
|
public static void AddCredit()
|
|
{
|
|
_creditsCount++;
|
|
PlayerPrefs.SetInt("CoinsCount", _creditsCount);
|
|
OnCreditAdded?.Invoke(_creditsCount);
|
|
}
|
|
|
|
public static bool Pay(int amount = 1)
|
|
{
|
|
if (_creditsCount < amount)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
_creditsCount -= amount;
|
|
PlayerPrefs.SetInt("CoinsCount", _creditsCount);
|
|
|
|
PlayerPrefs.SetInt(PrizeSystem.key_starts, PlayerPrefs.GetInt(PrizeSystem.key_starts) + 1);
|
|
PlayerPrefs.SetInt(PrizeSystem.key_startsNul, PlayerPrefs.GetInt(PrizeSystem.key_startsNul) + 1);
|
|
|
|
OnCreditRemoved?.Invoke(_creditsCount);
|
|
return true;
|
|
}
|
|
|
|
public static int GetCredits() => _creditsCount;
|
|
|
|
public static void SetCredits(int amount)
|
|
{
|
|
int oldCredits = _creditsCount;
|
|
_creditsCount = Mathf.Max(0, amount);
|
|
PlayerPrefs.SetInt("CoinsCount", _creditsCount);
|
|
|
|
if (_creditsCount > oldCredits)
|
|
{
|
|
OnCreditAdded?.Invoke(_creditsCount);
|
|
}
|
|
else if (_creditsCount < oldCredits)
|
|
{
|
|
OnCreditRemoved?.Invoke(_creditsCount);
|
|
}
|
|
}
|
|
|
|
public static void Send(string message)
|
|
{
|
|
if (_instance == null || string.IsNullOrWhiteSpace(message)) return;
|
|
_instance._manager?.SendToDefault(message);
|
|
}
|
|
|
|
public static void SendToDevice(string deviceId, string message)
|
|
{
|
|
if (_instance == null || string.IsNullOrWhiteSpace(message)) return;
|
|
_instance._manager?.SendToDevice(deviceId, message);
|
|
}
|
|
|
|
public static void SendToAll(string message)
|
|
{
|
|
if (_instance == null || string.IsNullOrWhiteSpace(message)) return;
|
|
_instance._manager?.SendToAll(message);
|
|
}
|
|
|
|
public static string GetLastInputState()
|
|
{
|
|
if (_instance?._manager == null) return "11111111111111111111111111";
|
|
string defaultId = _instance._manager.DefaultDeviceId;
|
|
return string.IsNullOrWhiteSpace(defaultId)
|
|
? "11111111111111111111111111"
|
|
: _instance._manager.GetLastInputState(defaultId);
|
|
}
|
|
|
|
public static string GetLastInputState(string deviceId)
|
|
{
|
|
return _instance?._manager?.GetLastInputState(deviceId) ?? "11111111111111111111111111";
|
|
}
|
|
|
|
public static string[] GetConnectedDeviceIds()
|
|
{
|
|
return _instance?._manager?.ConnectedDeviceIds ?? Array.Empty<string>();
|
|
}
|
|
|
|
public static string GetDefaultDeviceId()
|
|
{
|
|
return _instance?._manager?.DefaultDeviceId ?? string.Empty;
|
|
}
|
|
|
|
public static string GetDeviceRole(string deviceId)
|
|
{
|
|
return _instance?._manager?.GetDeviceRole(deviceId) ?? string.Empty;
|
|
}
|
|
|
|
public static PrizeStandController GetPrizeStandController(string deviceId = null)
|
|
{
|
|
return _instance?._manager?.GetPrizeStandController(deviceId);
|
|
}
|
|
|
|
private IComControllerHandler CreateRoleHandler(string deviceId, string role)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(role) || roleHandlerBindings == null || roleHandlerBindings.Length == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
RoleHandlerBinding binding = FindRoleBinding(role);
|
|
if (binding == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
if (binding.handlerType == ControllerHandlerType.PrizeStand)
|
|
{
|
|
PrizeStandController controller = new PrizeStandController(
|
|
deviceId,
|
|
(id, message) => _manager != null && _manager.SendToDevice(id, message),
|
|
message => GameManager.Logger?.Log("PrizeStandController", $"[{deviceId}] {message}"));
|
|
|
|
controller.SetBoxesCount(binding.prizeStandBoxesCount);
|
|
return controller;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private RoleHandlerBinding FindRoleBinding(string role)
|
|
{
|
|
for (int i = 0; i < roleHandlerBindings.Length; i++)
|
|
{
|
|
RoleHandlerBinding binding = roleHandlerBindings[i];
|
|
if (binding == null || string.IsNullOrWhiteSpace(binding.role))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (string.Equals(binding.role.Trim(), role.Trim(), StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return binding;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public void Blink() => _light.Blink();
|
|
|
|
public void StopBlink(bool enableHat = true) => _light.StopBlink(enableHat);
|
|
|
|
private void Update()
|
|
{
|
|
if (_manager == null) return;
|
|
|
|
int coinsProcessed = 0;
|
|
while (_manager.TryDequeueCoin(out _))
|
|
{
|
|
AddCredit();
|
|
coinsProcessed++;
|
|
}
|
|
|
|
while (_manager.TryDequeueInput(out DeviceInputMessage msg))
|
|
{
|
|
OnDeviceInputReceived?.Invoke(msg.DeviceId, msg.InputBits);
|
|
|
|
if (msg.DeviceId == _manager.DefaultDeviceId)
|
|
{
|
|
OnInputReceived?.Invoke(msg.InputBits);
|
|
}
|
|
}
|
|
|
|
while (_manager.TryDequeueAuthorized(out string deviceId))
|
|
{
|
|
OnDeviceAuthorized?.Invoke(deviceId);
|
|
|
|
if (!_initializeInvoked)
|
|
{
|
|
_initializeInvoked = true;
|
|
OnInitialize?.Invoke();
|
|
}
|
|
|
|
if (deviceId == _manager.DefaultDeviceId)
|
|
{
|
|
_light.Blink();
|
|
}
|
|
}
|
|
|
|
if (coinsProcessed > 0)
|
|
{
|
|
GameManager.Logger?.Log("ComSystemManager", $"Обработано монет: {coinsProcessed}");
|
|
}
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
_manager?.ShutdownAll(false);
|
|
_manager = null;
|
|
_initializeInvoked = false;
|
|
}
|
|
|
|
private void OnApplicationQuit()
|
|
{
|
|
_manager?.ShutdownAll(true);
|
|
_manager = null;
|
|
_initializeInvoked = false;
|
|
}
|
|
}
|
|
}
|
|
|