Danil Iarosh 820179aa7d
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
fix: don't kill running light sequence on first device authorization
- ComSystemManager: при авторизации дефолтного устройства больше не
  вызывается StopSequence() если последовательность уже играет.
  Это исправляет баг, при котором спецэффекты подсветки, запущенные
  GameManager-ом до завершения handshake, уничтожались и заменялись
  мигалкой шляпы.
- Логика авторизации разделена на три ветки:
  • приостановленные эффекты → ResumeAll() или сброс (если другая плата);
  • ничего не играет → Blink();
  • последовательность уже играет → не трогаем.
- Обновлены шрифтовые ассеты TextMesh Pro (Roboto-Bold SDF,
  LiberationSans SDF Fallback).
2026-03-04 14:26:48 +03:00

402 lines
15 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 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;
public static UnityEvent<string> OnDeviceDisconnected;
private static ComSystemManager _instance;
private static int _creditsCount;
private ComHub _manager;
private LightController _light;
private bool _initializeInvoked;
private string _lastDefaultDeviceId = string.Empty;
private string _lastDefaultRole = string.Empty;
private string _lastDefaultMac = string.Empty;
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>();
OnDeviceDisconnected = 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();
// Если плата изначально не подключена — ставим подсветку на паузу
if (string.IsNullOrWhiteSpace(_manager.DefaultDeviceId))
{
_light.PauseAll();
GameManager.Logger?.LogWarning("ComSystemManager",
"Плата не обнаружена при инициализации — подсветка приостановлена");
}
}
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 string GetDeviceMac(string deviceId)
{
return _instance?._manager?.GetDeviceMac(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.TryDequeueDisconnected(out DeviceDisconnectMessage dcMsg))
{
// Запоминаем идентификатор отключившегося устройства по умолчанию
if (dcMsg.DeviceId == _lastDefaultDeviceId || string.IsNullOrWhiteSpace(_manager.DefaultDeviceId))
{
_lastDefaultRole = dcMsg.Role;
_lastDefaultMac = dcMsg.Mac;
GameManager.Logger?.Log("ComSystemManager",
$"Устройство [{dcMsg.DeviceId}] отключилось (role={_lastDefaultRole}, mac={_lastDefaultMac})");
_light.PauseAll();
}
OnDeviceDisconnected?.Invoke(dcMsg.DeviceId);
}
while (_manager.TryDequeueAuthorized(out string deviceId))
{
OnDeviceAuthorized?.Invoke(deviceId);
if (!_initializeInvoked)
{
_initializeInvoked = true;
OnInitialize?.Invoke();
}
if (deviceId == _manager.DefaultDeviceId)
{
string newRole = _manager.GetDeviceRole(deviceId);
string newMac = _manager.GetDeviceMac(deviceId);
bool isSameDevice = IsSameDevice(_lastDefaultRole, _lastDefaultMac, newRole, newMac);
GameManager.Logger?.Log("ComSystemManager",
$"Устройство [{deviceId}] авторизовано (role={newRole}, mac={newMac}, " +
$"sameDevice={isSameDevice})");
_lastDefaultDeviceId = deviceId;
_lastDefaultRole = newRole;
_lastDefaultMac = newMac;
if (_light.IsSequencePaused || _light.IsBlinkPaused)
{
// Есть приостановленные эффекты
if (isSameDevice)
{
_light.ResumeAll();
}
else
{
// Другая плата — сбрасываем паузу и запускаем мигание
_light.StopSequence();
_light.Blink();
}
}
else if (!_light.IsSequencePlaying && !_light.IsBlinking)
{
// Ничего не играет и не на паузе — запускаем мигание
_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;
}
/// <summary>
/// Сравнивает идентификатор устройства по роли и MAC-адресу.
/// Если MAC известен у обоих — сравниваем по MAC (самый надёжный).
/// Если MAC неизвестен — сравниваем по роли.
/// Если ничего нет — считаем что это то же устройство (reconnect на том же порту).
/// </summary>
private static bool IsSameDevice(string oldRole, string oldMac, string newRole, string newMac)
{
bool hasOldMac = !string.IsNullOrWhiteSpace(oldMac);
bool hasNewMac = !string.IsNullOrWhiteSpace(newMac);
// Если оба MAC-адреса известны — сравниваем по MAC
if (hasOldMac && hasNewMac)
{
return string.Equals(oldMac, newMac, StringComparison.OrdinalIgnoreCase);
}
bool hasOldRole = !string.IsNullOrWhiteSpace(oldRole);
bool hasNewRole = !string.IsNullOrWhiteSpace(newRole);
// Если оба роля известны — сравниваем по роли
if (hasOldRole && hasNewRole)
{
return string.Equals(oldRole, newRole, StringComparison.OrdinalIgnoreCase);
}
// Нет данных для сравнения — считаем что это то же устройство (reconnect)
return true;
}
}
}