Danil Iarosh 78d7679e2b
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
feat(prizes, ui): complete Mar 24-Apr 2 sprint ahead of schedule
- завершен спринт 24 марта - 2 апреля с опережением плана и закрытием задач раньше срока
- расширена и стабилизирована система призов:
  добавлены и доработаны QR-tier конфиги, новые типы и модели данных,
  сохранение `qrCodes` в `prizes.json`, fallback-логика и синхронизация
  через `PrizeSystemManager`, `PrizeDatabase` и `LevelPrizeConfig`
- переработан flow настройки QR-призов:
  добавлены `QRCodeSettingsModal` и связанный UI-код,
  разнесены короткий текст приза и полный текст сообщения,
  добавлены шаблоны форматирования, корректная подстановка текста
  в QR и в UI-модалках, а также сохранение конфигурации между запусками
- доработан flow суперприза и текстовых настроек:
  добавлены fallback-правила показа, корректная обработка пустых строк
  и строк из одних пробелов, обновлены binders и показ текста в меню
- реализована и обновлена отдельная настройка витрины:
  добавлена `PrizeStandSettingsModal`, обновлена навигация строк,
  логика выбора ячейки и порогов очков, циклический выбор ячеек,
  ускорение изменения значений и исправления для keyboard/gamepad UX
- доработана игровая модалка витрины `PrizeStandModal`:
  добавлен вывод очков игрока, форматируемый текст счета,
  улучшено отображение ячеек и связанная логика выбора приза
- исправлен подсчет очков и интеграция с prize flow:
  `ScoreSystem` переведен на событийное обновление текущего счета,
  добавлены `CurrentScore`, `CurrentScoreChanged` и корректный display score,
  витрина и связанные UI-элементы теперь получают актуальные очки,
  а не нулевые или несвоевременные preview-значения
- переработаны модальные окна и навигация UI:
  улучшены `Modal`, `ModalManager`, `UIRowPointer`, `RulesModal`,
  `QRCodeModal`, `PrizeStandModal`, `LevelPrizeSettingsModal` и др.,
  чтобы фокус, указатель и submit работали консистентно без мыши
- обновлен win/prize UI:
  доработаны `WinBannerController`, `GameEndUIController`,
  `MainMenu`, `QRCodeUIController`, `TopPanelTextController` и настройки UI,
  улучшены тексты баннеров, отображение призов и итогового счета
- обновлены сцены и ассеты под новый flow:
  `MainMenu`, `Level UI`, prefab'ы строк настроек и кнопок,
  глобальные префабы, шрифты TMP, иконки, recovery-данные и ресурсы UI
- добавлены вспомогательные UI-компоненты и служебные скрипты:
  `ButtonTextFocusColor`, `SelectableDescriptionTarget`, `QRTier`
  и другие изменения, поддерживающие новый конфигурируемый prize UI
2026-04-01 12:47:17 +03:00

402 lines
13 KiB
C#
Raw Permalink 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.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using RR.Game2D.Core.Managers;
namespace RR.Common.Controls
{
internal sealed class ComHub
{
private readonly Dictionary<string, ComDeviceClient> _devices = new();
private readonly Dictionary<string, IComControllerHandler> _handlers = new();
private readonly ConcurrentQueue<string> _coinQueue = new();
private readonly ConcurrentQueue<DeviceInputMessage> _inputQueue = new();
private readonly ConcurrentQueue<string> _authorizedQueue = new();
private readonly ConcurrentQueue<DeviceDisconnectMessage> _disconnectedQueue = new();
private readonly bool _useAllDetectedPorts;
private readonly string[] _explicitPorts;
private readonly string _defaultDevicePort;
private readonly bool _enableEncryptedHandshake;
private readonly int _handshakeTimeoutMs;
private readonly bool _keepAlive;
private readonly string _roleRequestCommand;
private readonly string _roleResponsePrefix;
private readonly int _roleRequestTimeoutMs;
private readonly string _mainControllerRole;
private readonly Func<string, string, IComControllerHandler> _handlerFactory;
private string _defaultDeviceId = string.Empty;
public ComHub(
bool useAllDetectedPorts,
string[] explicitPorts,
string defaultDevicePort,
bool enableEncryptedHandshake,
int handshakeTimeoutMs,
bool keepAlive,
string roleRequestCommand,
string roleResponsePrefix,
int roleRequestTimeoutMs,
string mainControllerRole,
Func<string, string, IComControllerHandler> handlerFactory)
{
_useAllDetectedPorts = useAllDetectedPorts;
_explicitPorts = explicitPorts ?? Array.Empty<string>();
_defaultDevicePort = defaultDevicePort ?? string.Empty;
_enableEncryptedHandshake = enableEncryptedHandshake;
_handshakeTimeoutMs = handshakeTimeoutMs;
_keepAlive = keepAlive;
_roleRequestCommand = roleRequestCommand ?? string.Empty;
_roleResponsePrefix = roleResponsePrefix ?? "ROLE";
_roleRequestTimeoutMs = roleRequestTimeoutMs;
_mainControllerRole = string.IsNullOrWhiteSpace(mainControllerRole) ? "main_controller" : mainControllerRole;
_handlerFactory = handlerFactory;
}
public string DefaultDeviceId => _defaultDeviceId;
public string[] ConnectedDeviceIds => _devices.Keys.ToArray();
public void InitializeDevices()
{
ComNativeWrapper.Serial_CloseAll();
string[] allPorts = ComNativeWrapper.ListPorts();
if (allPorts.Length == 0)
{
GameManager.Logger?.LogWarning("ComHub", "Не найдено COM-портов");
return;
}
string[] selectedPorts = SelectPorts(allPorts);
foreach (string port in selectedPorts)
{
TryCreateDevice(port);
}
ResolveDefaultDeviceId();
}
public void ShutdownAll(bool sendShutdownCommands)
{
foreach (ComDeviceClient client in _devices.Values)
{
UnsubscribeDevice(client);
client.Stop(sendShutdownCommands);
}
foreach (IComControllerHandler handler in _handlers.Values)
{
handler.Dispose();
}
_devices.Clear();
_handlers.Clear();
_defaultDeviceId = string.Empty;
while (_coinQueue.TryDequeue(out _))
{
}
while (_inputQueue.TryDequeue(out _))
{
}
while (_authorizedQueue.TryDequeue(out _))
{
}
while (_disconnectedQueue.TryDequeue(out _))
{
}
}
public bool SendToDefault(string message)
{
if (string.IsNullOrEmpty(_defaultDeviceId))
{
return false;
}
return SendToDevice(_defaultDeviceId, message);
}
public bool SendToDevice(string deviceId, string message)
{
if (string.IsNullOrWhiteSpace(deviceId))
{
return false;
}
if (!_devices.TryGetValue(deviceId, out ComDeviceClient client))
{
return false;
}
client.Send(message);
return true;
}
public void SendToAll(string message)
{
foreach (ComDeviceClient client in _devices.Values)
{
client.Send(message);
}
}
public string GetLastInputState(string deviceId)
{
if (string.IsNullOrWhiteSpace(deviceId))
{
return "11111111111111111111111111";
}
return _devices.TryGetValue(deviceId, out ComDeviceClient client)
? client.LastInputBits
: "11111111111111111111111111";
}
public bool TryDequeueCoin(out string deviceId) => _coinQueue.TryDequeue(out deviceId);
public bool TryDequeueInput(out DeviceInputMessage message) => _inputQueue.TryDequeue(out message);
public bool TryDequeueAuthorized(out string deviceId) => _authorizedQueue.TryDequeue(out deviceId);
public bool TryDequeueDisconnected(out DeviceDisconnectMessage message) => _disconnectedQueue.TryDequeue(out message);
public string GetDeviceRole(string deviceId)
{
if (string.IsNullOrWhiteSpace(deviceId))
{
return string.Empty;
}
return _devices.TryGetValue(deviceId, out ComDeviceClient client)
? client.ControllerRole
: string.Empty;
}
public string GetDeviceMac(string deviceId)
{
if (string.IsNullOrWhiteSpace(deviceId))
{
return string.Empty;
}
return _devices.TryGetValue(deviceId, out ComDeviceClient client)
? client.MacAddress
: string.Empty;
}
public PrizeStandController GetPrizeStandController(string deviceId = null)
{
// Делегируем поиск витрины в ComSystemManager
return ComSystemManager.GetPrizeStandController(deviceId);
}
private string[] SelectPorts(string[] allPorts)
{
if (_useAllDetectedPorts)
{
return allPorts;
}
if (_explicitPorts.Length == 0)
{
return Array.Empty<string>();
}
HashSet<string> explicitSet = new(_explicitPorts.Where(p => !string.IsNullOrWhiteSpace(p)));
return allPorts.Where(explicitSet.Contains).ToArray();
}
private void ResolveDefaultDeviceId()
{
if (!string.IsNullOrWhiteSpace(_defaultDevicePort) && _devices.ContainsKey(_defaultDevicePort))
{
_defaultDeviceId = _defaultDevicePort;
return;
}
_defaultDeviceId = _devices.Keys.FirstOrDefault() ?? string.Empty;
}
private void TryCreateDevice(string port)
{
ComDeviceClient client = new(
port,
_enableEncryptedHandshake,
_handshakeTimeoutMs,
_keepAlive,
_roleRequestCommand,
_roleResponsePrefix,
_roleRequestTimeoutMs);
SubscribeDevice(client);
if (!client.Start())
{
UnsubscribeDevice(client);
return;
}
_devices[client.DeviceId] = client;
}
private void SubscribeDevice(ComDeviceClient client)
{
client.CoinAccepted += OnDeviceCoinAccepted;
client.InputReceived += OnDeviceInputReceived;
client.RawLineReceived += OnDeviceRawLineReceived;
client.Authorized += OnDeviceAuthorized;
client.Disconnected += OnDeviceDisconnected;
client.ConnectionLost += OnDeviceConnectionLost;
}
private void UnsubscribeDevice(ComDeviceClient client)
{
client.CoinAccepted -= OnDeviceCoinAccepted;
client.InputReceived -= OnDeviceInputReceived;
client.RawLineReceived -= OnDeviceRawLineReceived;
client.Authorized -= OnDeviceAuthorized;
client.Disconnected -= OnDeviceDisconnected;
client.ConnectionLost -= OnDeviceConnectionLost;
}
private void OnDeviceCoinAccepted(string deviceId)
{
if (_handlers.TryGetValue(deviceId, out IComControllerHandler handler))
{
handler.HandleCoinAccepted();
return;
}
_coinQueue.Enqueue(deviceId);
}
private void OnDeviceInputReceived(string deviceId, string inputBits)
{
if (_handlers.TryGetValue(deviceId, out IComControllerHandler handler))
{
handler.HandleInputReceived(inputBits);
return;
}
_inputQueue.Enqueue(new DeviceInputMessage(deviceId, inputBits));
}
private void OnDeviceRawLineReceived(string deviceId, string line)
{
if (_handlers.TryGetValue(deviceId, out IComControllerHandler handler))
{
handler.HandleRawLine(line);
}
}
private void OnDeviceAuthorized(string deviceId)
{
if (_devices.TryGetValue(deviceId, out ComDeviceClient client) &&
string.Equals(client.ControllerRole, _mainControllerRole, StringComparison.OrdinalIgnoreCase))
{
_defaultDeviceId = deviceId;
}
else if (string.IsNullOrWhiteSpace(_defaultDeviceId))
{
_defaultDeviceId = deviceId;
}
if (_handlers.TryGetValue(deviceId, out IComControllerHandler existing))
{
existing.Dispose();
}
_handlers[deviceId] = CreateHandlerForDevice(deviceId);
_authorizedQueue.Enqueue(deviceId);
}
private void OnDeviceDisconnected(string deviceId)
{
if (!_devices.TryGetValue(deviceId, out ComDeviceClient client))
{
return;
}
string role = client.ControllerRole;
string mac = client.MacAddress;
UnsubscribeDevice(client);
_devices.Remove(deviceId);
if (_handlers.TryGetValue(deviceId, out IComControllerHandler handler))
{
handler.Dispose();
_handlers.Remove(deviceId);
}
if (_defaultDeviceId == deviceId)
{
_defaultDeviceId = _devices.Keys.FirstOrDefault() ?? string.Empty;
}
_disconnectedQueue.Enqueue(new DeviceDisconnectMessage(deviceId, role, mac));
}
private void OnDeviceConnectionLost(string deviceId)
{
string role = string.Empty;
string mac = string.Empty;
if (_devices.TryGetValue(deviceId, out ComDeviceClient client))
{
role = client.ControllerRole;
mac = client.MacAddress;
}
_disconnectedQueue.Enqueue(new DeviceDisconnectMessage(deviceId, role, mac));
}
private IComControllerHandler CreateHandlerForDevice(string deviceId)
{
string role = GetDeviceRole(deviceId);
IComControllerHandler customHandler = _handlerFactory?.Invoke(deviceId, role);
if (customHandler != null)
{
return customHandler;
}
return new DefaultComControllerHandler(deviceId, _coinQueue, _inputQueue);
}
}
internal readonly struct DeviceInputMessage
{
public readonly string DeviceId;
public readonly string InputBits;
public DeviceInputMessage(string deviceId, string inputBits)
{
DeviceId = deviceId;
InputBits = inputBits;
}
}
internal readonly struct DeviceDisconnectMessage
{
public readonly string DeviceId;
public readonly string Role;
public readonly string Mac;
public DeviceDisconnectMessage(string deviceId, string role, string mac)
{
DeviceId = deviceId;
Role = role ?? string.Empty;
Mac = mac ?? string.Empty;
}
}
}