Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
Добавлены новые UI-компоненты и исправлена надёжность подсветки - Добавлен UIElementAnimator — универсальный аниматор UI-элементов (покачивание, движение вверх-вниз, пульсация масштаба) - Добавлен GamePriceDisplay — отображение стоимости игры из настроек (payment.startGameCost, payment.currency) - Добавлен метод StartIdleEffects() в GameManager для гарантированного запуска мигания шапки и последовательности эффектов - Исправлена подсветка, не включавшаяся при переходе в главное меню: OnSceneLoaded теперь вызывает StartIdleEffects() - Исправлено восстановление подсветки при переподключении платы: ComSystemManager использует StartIdleEffects() для MainMenu - Добавлены LightController.IsHatOn и ResendHatState() для корректного отслеживания состояния шапки - Исправлено зависание спецэффектов при мигании шапки: LightSequence теперь переотправляет команду эффекта каждые 2 сек - Убраны дублирующиеся вызовы Blink/SetLight из LoadScene (теперь обрабатывается в OnSceneLoaded) - Обновлена сцена MainMenu, ассеты шрифтов, удалён старый префаб Coin
422 lines
16 KiB
C#
422 lines
16 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;
|
||
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;
|
||
|
||
// Определяем текущую сцену
|
||
string sceneName = UnityEngine.SceneManagement.SceneManager.GetActiveScene().name;
|
||
bool isMainMenu = sceneName == "MainMenu";
|
||
|
||
if (isMainMenu)
|
||
{
|
||
// В главном меню — гарантированно запускаем мигание + эффекты
|
||
GameManager.Instance?.StartIdleEffects();
|
||
}
|
||
else if (_light.IsSequencePaused || _light.IsBlinkPaused)
|
||
{
|
||
// На уровне с приостановленными эффектами
|
||
if (isSameDevice)
|
||
{
|
||
_light.ResumeAll();
|
||
}
|
||
else
|
||
{
|
||
// Другая плата — перезапускаем эффекты уровня
|
||
GameManager.Instance?.StartIdleEffects();
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// На уровне без приостановленных эффектов
|
||
if (_light.IsBlinking)
|
||
{
|
||
// Мигание уже идёт — корутина сама отправит команды
|
||
}
|
||
else if (_light.IsHatOn)
|
||
{
|
||
_light.ResendHatState();
|
||
}
|
||
|
||
if (!_light.IsSequencePlaying)
|
||
{
|
||
// Эффекты не играют — пробуем запустить idle
|
||
GameManager.Instance?.StartIdleEffects();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
}
|
||
|