refactor: Refactor work with COM to ComSystemManager
- Добавлен ComSystemManager как новый публичный компонент COM-системы для поэтапной замены ComWrapper - Реализовано получение роли контроллера только после успешного handshake в ComDeviceClient - Добавлено публичное поле ControllerRole в ComDeviceClient - Добавлен запрос роли по команде 97 и разбор ответа формата ROLE : <value> (с пробелами) - В ComMultiManager добавлена логика выбора DefaultDeviceId по роли main_controller - Добавлен доступ к роли устройства через GetDeviceRole(deviceId) в менеджере и фасаде - В ComWrapper добавлены настройки роли: roleRequestCommand, roleResponsePrefix, roleRequestTimeoutMs, mainControllerRole - Дефолтная команда запроса роли изменена на 97 - Выполнена кодовая миграция зависимостей с ComWrapper на ComSystemManager в GameManager, GlobalsManager, PaymentManager, BalanceManager, InactivityManager, COMInput, LightController, PrizeSystem - В GlobalsManager исправлена инициализация ссылки после AddComponent<ComSystemManager>(), чтобы исключить NullReference
This commit is contained in:
parent
6b02c76ef3
commit
32cfa79578
0
.ai/mcp/mcp.json
Normal file
0
.ai/mcp/mcp.json
Normal file
@ -12,6 +12,7 @@ GameObject:
|
||||
- component: {fileID: -6830167632558745604}
|
||||
- component: {fileID: 7290507550622501242}
|
||||
- component: {fileID: 4577859410310799261}
|
||||
- component: {fileID: -3401184227323537348}
|
||||
- component: {fileID: -6245296037342915518}
|
||||
- component: {fileID: 6566017136146807207}
|
||||
- component: {fileID: 5633451267958679388}
|
||||
@ -80,6 +81,39 @@ MonoBehaviour:
|
||||
saveMoneyBetweenSessions: 0
|
||||
enablePrizeChecking: 0
|
||||
keepAlive: 1
|
||||
enableEncryptedHandshake: 1
|
||||
handshakeTimeoutMs: 5000
|
||||
roleRequestCommand: 97
|
||||
roleResponsePrefix: ROLE
|
||||
roleRequestTimeoutMs: 1500
|
||||
mainControllerRole: main_controller
|
||||
useAllDetectedPorts: 1
|
||||
explicitPorts: []
|
||||
defaultDevicePort:
|
||||
--- !u!114 &-3401184227323537348
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 4763496076470024150}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: 1c4ba28ebcf4f5e42bddbc16592fe81c, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::RR.Common.Controls.ComSystemManager
|
||||
saveMoneyBetweenSessions: 0
|
||||
enablePrizeChecking: 0
|
||||
keepAlive: 1
|
||||
enableEncryptedHandshake: 1
|
||||
handshakeTimeoutMs: 5000
|
||||
roleRequestCommand: 97
|
||||
roleResponsePrefix: ROLE
|
||||
roleRequestTimeoutMs: 1500
|
||||
mainControllerRole: main_controller
|
||||
useAllDetectedPorts: 1
|
||||
explicitPorts: []
|
||||
defaultDevicePort:
|
||||
--- !u!114 &-6245296037342915518
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using RR.Game2D.Core.Managers;
|
||||
using UnityEngine;
|
||||
|
||||
@ -31,7 +31,7 @@ namespace RR.Common.Controls
|
||||
gm = GameManager.Instance;
|
||||
|
||||
//ОБЯЗАТЕЛЬНЫЙ БЛОК
|
||||
ComWrapper.OnInputReceived.AddListener(UpdateInputState);
|
||||
ComSystemManager.OnInputReceived.AddListener(UpdateInputState);
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -261,4 +261,4 @@ namespace RR.Common.Controls
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
723
Assets/Scripts/Controls/Inputs/ComSystem/ComDeviceClient.cs
Normal file
723
Assets/Scripts/Controls/Inputs/ComSystem/ComDeviceClient.cs
Normal file
@ -0,0 +1,723 @@
|
||||
using System;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using RR.Game2D.Core.Managers;
|
||||
|
||||
namespace RR.Common.Controls
|
||||
{
|
||||
internal sealed class ComDeviceClient
|
||||
{
|
||||
private const string RequestInput = "91";
|
||||
private const string DefaultInputBits = "11111111111111111111111111";
|
||||
private const long CoinDebounceMs = 250;
|
||||
|
||||
private static readonly byte[] AesEncryptedKey =
|
||||
{
|
||||
0x69, 0x40, 0xC7, 0x0E, 0xF2, 0xC7, 0x45, 0x70,
|
||||
0x72, 0xCD, 0x28, 0x58, 0x99, 0x8F, 0x73, 0xA8,
|
||||
0x5F, 0x5F, 0xED, 0xB4, 0xC1, 0x62, 0xB4, 0x24,
|
||||
0xAC, 0xD7, 0xA3, 0xD6, 0xD8, 0x0B, 0x50, 0x37
|
||||
};
|
||||
|
||||
private readonly object _ioLock = new object();
|
||||
|
||||
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 CancellationTokenSource _lifecycleCts;
|
||||
private Task _pollTask;
|
||||
private Task _keepAliveTask;
|
||||
|
||||
private int _handle;
|
||||
private long _lastCoinTime;
|
||||
private int _inputToggle;
|
||||
|
||||
private bool _isAuthorized;
|
||||
private bool _isOperational;
|
||||
private bool _handshakeInProgress;
|
||||
|
||||
private string _requestHex = string.Empty;
|
||||
private string _lastInputBits = DefaultInputBits;
|
||||
private byte[] _aesKey32;
|
||||
|
||||
private HandshakeStage _handshakeStage = HandshakeStage.None;
|
||||
private TaskCompletionSource<bool> _handshakeCompletion;
|
||||
private TaskCompletionSource<string> _roleCompletion;
|
||||
|
||||
private enum HandshakeStage
|
||||
{
|
||||
None,
|
||||
WaitingRaes,
|
||||
WaitingChal,
|
||||
WaitingReady,
|
||||
Completed,
|
||||
Failed
|
||||
}
|
||||
|
||||
public string DeviceId { get; }
|
||||
public string PortName { get; }
|
||||
|
||||
public bool IsOperational => _isOperational;
|
||||
public string LastInputBits => _lastInputBits;
|
||||
public string ControllerRole { get; private set; } = string.Empty;
|
||||
|
||||
public event Action<string> CoinAccepted;
|
||||
public event Action<string, string> InputReceived;
|
||||
public event Action<string> Authorized;
|
||||
public event Action<string> Disconnected;
|
||||
|
||||
public ComDeviceClient(
|
||||
string portName,
|
||||
bool enableEncryptedHandshake,
|
||||
int handshakeTimeoutMs,
|
||||
bool keepAlive,
|
||||
string roleRequestCommand,
|
||||
string roleResponsePrefix,
|
||||
int roleRequestTimeoutMs)
|
||||
{
|
||||
PortName = portName;
|
||||
DeviceId = portName;
|
||||
_enableEncryptedHandshake = enableEncryptedHandshake;
|
||||
_handshakeTimeoutMs = Math.Max(500, handshakeTimeoutMs);
|
||||
_keepAlive = keepAlive;
|
||||
_roleRequestCommand = roleRequestCommand?.Trim() ?? string.Empty;
|
||||
_roleResponsePrefix = string.IsNullOrWhiteSpace(roleResponsePrefix) ? "ROLE" : roleResponsePrefix.Trim();
|
||||
_roleRequestTimeoutMs = Math.Max(200, roleRequestTimeoutMs);
|
||||
|
||||
if (_enableEncryptedHandshake)
|
||||
{
|
||||
_aesKey32 = BuildAesKey();
|
||||
}
|
||||
}
|
||||
|
||||
public bool Start()
|
||||
{
|
||||
if (!OpenPort()) return false;
|
||||
|
||||
_lifecycleCts = new CancellationTokenSource();
|
||||
_pollTask = Task.Run(() => PollLoopAsync(_lifecycleCts.Token), _lifecycleCts.Token);
|
||||
|
||||
if (_enableEncryptedHandshake)
|
||||
{
|
||||
_ = RunHandshakeAsync(_lifecycleCts.Token);
|
||||
}
|
||||
else
|
||||
{
|
||||
MarkOperational();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Stop(bool sendShutdownCommands)
|
||||
{
|
||||
try
|
||||
{
|
||||
_lifecycleCts?.Cancel();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
|
||||
if (sendShutdownCommands)
|
||||
{
|
||||
SendRaw("2170", true);
|
||||
SendRaw("2130", true);
|
||||
}
|
||||
|
||||
lock (_ioLock)
|
||||
{
|
||||
if (_handle > 0)
|
||||
{
|
||||
ComNativeWrapper.Serial_Close(_handle);
|
||||
_handle = 0;
|
||||
}
|
||||
}
|
||||
|
||||
_isOperational = false;
|
||||
_isAuthorized = false;
|
||||
_handshakeInProgress = false;
|
||||
_handshakeStage = HandshakeStage.None;
|
||||
_handshakeCompletion?.TrySetCanceled();
|
||||
_roleCompletion?.TrySetCanceled();
|
||||
ControllerRole = string.Empty;
|
||||
|
||||
Disconnected?.Invoke(DeviceId);
|
||||
|
||||
_lifecycleCts?.Dispose();
|
||||
_lifecycleCts = null;
|
||||
}
|
||||
|
||||
public void Send(string message)
|
||||
{
|
||||
SendRaw(message, false);
|
||||
}
|
||||
|
||||
private bool OpenPort()
|
||||
{
|
||||
int handle = ComNativeWrapper.Serial_Open(PortName, 115200, 8, 0, 1, 5, 10);
|
||||
if (handle <= 0)
|
||||
{
|
||||
GameManager.Logger?.LogError("ComDeviceClient", $"[{DeviceId}] Open failed: {ComNativeWrapper.GetLastError()}");
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_ioLock)
|
||||
{
|
||||
_handle = handle;
|
||||
}
|
||||
|
||||
_lastCoinTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
FlushPortBuffer();
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task PollLoopAsync(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(1, token);
|
||||
string line = ReadBufferLine();
|
||||
if (string.IsNullOrEmpty(line)) continue;
|
||||
ProcessLine(line);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
GameManager.Logger?.LogError("ComDeviceClient", $"[{DeviceId}] Poll error: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessLine(string line)
|
||||
{
|
||||
line = line.Trim();
|
||||
if (string.IsNullOrEmpty(line)) return;
|
||||
|
||||
if (_handshakeInProgress)
|
||||
{
|
||||
TryProcessHandshakeLine(line);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_isOperational) return;
|
||||
|
||||
if (TryHandleRoleLine(line))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.StartsWith("INPUT", StringComparison.Ordinal))
|
||||
{
|
||||
_inputToggle = 1;
|
||||
if (line.Length <= 5) return;
|
||||
|
||||
string rawData = line.Substring(5);
|
||||
if (string.IsNullOrWhiteSpace(rawData)) return;
|
||||
|
||||
string bits = DecimalToBinary(rawData);
|
||||
_lastInputBits = bits;
|
||||
InputReceived?.Invoke(DeviceId, bits);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RunHandshakeAsync(CancellationToken token)
|
||||
{
|
||||
_handshakeInProgress = true;
|
||||
_isAuthorized = false;
|
||||
_isOperational = false;
|
||||
_handshakeStage = HandshakeStage.WaitingRaes;
|
||||
_handshakeCompletion = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
SendRaw("00", true);
|
||||
await Task.Delay(1000, token);
|
||||
|
||||
_requestHex = GenerateRandomBlock16Hex();
|
||||
SendRaw("07" + _requestHex, true);
|
||||
|
||||
Task timeoutTask = Task.Delay(_handshakeTimeoutMs, token);
|
||||
Task finished = await Task.WhenAny(_handshakeCompletion.Task, timeoutTask);
|
||||
|
||||
if (finished == _handshakeCompletion.Task && _handshakeCompletion.Task.Result)
|
||||
{
|
||||
MarkOperational();
|
||||
return;
|
||||
}
|
||||
|
||||
FailHandshake("Таймаут рукопожатия");
|
||||
}
|
||||
|
||||
private void TryProcessHandshakeLine(string line)
|
||||
{
|
||||
if (line.Equals("READY", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (_handshakeStage != HandshakeStage.WaitingReady)
|
||||
{
|
||||
FailHandshake($"READY в неверной фазе: {_handshakeStage}");
|
||||
return;
|
||||
}
|
||||
|
||||
_handshakeStage = HandshakeStage.Completed;
|
||||
_isAuthorized = true;
|
||||
_handshakeCompletion?.TrySetResult(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.StartsWith("RAES", StringComparison.Ordinal))
|
||||
{
|
||||
HandleRaes(line);
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.StartsWith("CHAL", StringComparison.Ordinal))
|
||||
{
|
||||
HandleChal(line);
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleRaes(string line)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_handshakeStage != HandshakeStage.WaitingRaes)
|
||||
{
|
||||
FailHandshake($"RAES в неверной фазе: {_handshakeStage}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.Length < 36)
|
||||
{
|
||||
FailHandshake("Некорректная длина RAES");
|
||||
return;
|
||||
}
|
||||
|
||||
string hex = line.Substring(4, 32);
|
||||
if (!IsHexString(hex, 32))
|
||||
{
|
||||
FailHandshake("RAES содержит некорректный HEX");
|
||||
return;
|
||||
}
|
||||
|
||||
string decrypted = DecryptHexBlock16(hex);
|
||||
if (!string.Equals(decrypted, _requestHex, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
FailHandshake("RAES не совпадает с запросом 07");
|
||||
return;
|
||||
}
|
||||
|
||||
_handshakeStage = HandshakeStage.WaitingChal;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
FailHandshake($"Ошибка RAES: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void HandleChal(string line)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_handshakeStage != HandshakeStage.WaitingChal)
|
||||
{
|
||||
FailHandshake($"CHAL в неверной фазе: {_handshakeStage}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.Length < 36)
|
||||
{
|
||||
FailHandshake("Некорректная длина CHAL");
|
||||
return;
|
||||
}
|
||||
|
||||
string hex = line.Substring(4, 32);
|
||||
if (!IsHexString(hex, 32))
|
||||
{
|
||||
FailHandshake("CHAL содержит некорректный HEX");
|
||||
return;
|
||||
}
|
||||
|
||||
string encrypted = EncryptHexBlock16(hex);
|
||||
SendRaw("08" + encrypted, true);
|
||||
_handshakeStage = HandshakeStage.WaitingReady;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
FailHandshake($"Ошибка CHAL: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void MarkOperational()
|
||||
{
|
||||
_handshakeInProgress = false;
|
||||
_isAuthorized = true;
|
||||
_isOperational = true;
|
||||
_handshakeStage = HandshakeStage.Completed;
|
||||
ControllerRole = string.Empty;
|
||||
|
||||
SendRaw("2171", true);
|
||||
|
||||
if (_keepAlive)
|
||||
{
|
||||
_keepAliveTask = Task.Run(() => KeepAliveLoopAsync(_lifecycleCts.Token), _lifecycleCts.Token);
|
||||
}
|
||||
|
||||
_ = Task.Run(() => ResolveRoleAfterAuthorizationAsync(_lifecycleCts.Token), _lifecycleCts.Token);
|
||||
}
|
||||
|
||||
private void FailHandshake(string message)
|
||||
{
|
||||
_isAuthorized = false;
|
||||
_isOperational = false;
|
||||
_handshakeInProgress = false;
|
||||
_handshakeStage = HandshakeStage.Failed;
|
||||
_handshakeCompletion?.TrySetResult(false);
|
||||
GameManager.Logger?.LogError("ComDeviceClient", $"[{DeviceId}] {message}");
|
||||
}
|
||||
|
||||
private async Task KeepAliveLoopAsync(CancellationToken token)
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(2000, token);
|
||||
|
||||
if (!_isOperational)
|
||||
{
|
||||
await Task.Delay(8000, token);
|
||||
continue;
|
||||
}
|
||||
|
||||
SendRaw(RequestInput, false);
|
||||
_inputToggle = 0;
|
||||
|
||||
await Task.Delay(1000, token);
|
||||
if (_inputToggle == 0)
|
||||
{
|
||||
await ReconnectAsync(token);
|
||||
}
|
||||
|
||||
await Task.Delay(7000, token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
GameManager.Logger?.LogError("ComDeviceClient", $"[{DeviceId}] KeepAlive error: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ReconnectAsync(CancellationToken token)
|
||||
{
|
||||
_isOperational = false;
|
||||
_isAuthorized = false;
|
||||
_handshakeInProgress = false;
|
||||
_handshakeStage = HandshakeStage.None;
|
||||
_lastInputBits = DefaultInputBits;
|
||||
_inputToggle = 0;
|
||||
ControllerRole = string.Empty;
|
||||
|
||||
lock (_ioLock)
|
||||
{
|
||||
if (_handle > 0)
|
||||
{
|
||||
ComNativeWrapper.Serial_Close(_handle);
|
||||
_handle = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!OpenPort())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_enableEncryptedHandshake)
|
||||
{
|
||||
await RunHandshakeAsync(token);
|
||||
}
|
||||
else
|
||||
{
|
||||
MarkOperational();
|
||||
}
|
||||
}
|
||||
|
||||
private void FlushPortBuffer()
|
||||
{
|
||||
lock (_ioLock)
|
||||
{
|
||||
if (_handle <= 0) return;
|
||||
|
||||
byte[] localBuf = new byte[1024];
|
||||
int flushedCount = 0;
|
||||
|
||||
int read;
|
||||
while ((read = ComNativeWrapper.Serial_ReadLineUTF8_bytes(_handle, localBuf, localBuf.Length)) > 0)
|
||||
{
|
||||
flushedCount++;
|
||||
if (flushedCount > 100) break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string ReadBufferLine()
|
||||
{
|
||||
lock (_ioLock)
|
||||
{
|
||||
if (_handle <= 0) return string.Empty;
|
||||
|
||||
byte[] localBuf = new byte[1024];
|
||||
int read = ComNativeWrapper.Serial_ReadLineUTF8_bytes(_handle, localBuf, localBuf.Length);
|
||||
if (read <= 0) return string.Empty;
|
||||
|
||||
if (read > localBuf.Length) read = localBuf.Length;
|
||||
|
||||
string line = Encoding.UTF8.GetString(localBuf, 0, read).Trim();
|
||||
if (line != "COIN") return line;
|
||||
|
||||
long now = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
if (now - _lastCoinTime < CoinDebounceMs) return string.Empty;
|
||||
|
||||
_lastCoinTime = now;
|
||||
|
||||
if (_isOperational)
|
||||
{
|
||||
ComNativeWrapper.Serial_WriteStringUTF8(_handle, "10", 1);
|
||||
CoinAccepted?.Invoke(DeviceId);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private void SendRaw(string message, bool bypassAuthorization)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message)) return;
|
||||
if (!bypassAuthorization && !_isOperational) return;
|
||||
|
||||
lock (_ioLock)
|
||||
{
|
||||
if (_handle <= 0) return;
|
||||
ComNativeWrapper.Serial_WriteStringUTF8(_handle, message, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryHandleRoleLine(string line)
|
||||
{
|
||||
if (_roleCompletion == null) return false;
|
||||
if (!_roleCompletion.Task.IsCompleted && TryExtractRole(line, out string role))
|
||||
{
|
||||
ControllerRole = role;
|
||||
_roleCompletion.TrySetResult(role);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task ResolveRoleAfterAuthorizationAsync(CancellationToken token)
|
||||
{
|
||||
// Роль считываем только после завершенной авторизации рукопожатия.
|
||||
if (!_enableEncryptedHandshake || !_isAuthorized || string.IsNullOrWhiteSpace(_roleRequestCommand))
|
||||
{
|
||||
Authorized?.Invoke(DeviceId);
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_roleCompletion = new TaskCompletionSource<string>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
SendRaw(_roleRequestCommand, false);
|
||||
|
||||
Task timeoutTask = Task.Delay(_roleRequestTimeoutMs, token);
|
||||
Task finished = await Task.WhenAny(_roleCompletion.Task, timeoutTask);
|
||||
if (finished == _roleCompletion.Task &&
|
||||
_roleCompletion.Task.IsCompletedSuccessfully &&
|
||||
!string.IsNullOrWhiteSpace(_roleCompletion.Task.Result))
|
||||
{
|
||||
ControllerRole = _roleCompletion.Task.Result;
|
||||
}
|
||||
else
|
||||
{
|
||||
GameManager.Logger?.LogWarning("ComDeviceClient",
|
||||
$"[{DeviceId}] Не удалось получить роль контроллера за {_roleRequestTimeoutMs}ms");
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignore
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
GameManager.Logger?.LogError("ComDeviceClient", $"[{DeviceId}] Role resolve error: {e.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
Authorized?.Invoke(DeviceId);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryExtractRole(string line, out string role)
|
||||
{
|
||||
role = string.Empty;
|
||||
if (string.IsNullOrWhiteSpace(line)) return false;
|
||||
|
||||
string trimmed = line.Trim();
|
||||
if (!trimmed.StartsWith(_roleResponsePrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string tail = trimmed.Substring(_roleResponsePrefix.Length).Trim();
|
||||
if (tail.StartsWith(":") || tail.StartsWith("="))
|
||||
{
|
||||
tail = tail.Substring(1).Trim();
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(tail)) return false;
|
||||
role = tail;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string DecimalToBinary(string decimalString)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!long.TryParse(decimalString, out long value)) return DefaultInputBits;
|
||||
|
||||
string binary = Convert.ToString(value, 2);
|
||||
char[] chars = binary.ToCharArray();
|
||||
Array.Reverse(chars);
|
||||
return new string(chars);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return DefaultInputBits;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] BuildAesKey()
|
||||
{
|
||||
byte[] key = new byte[32];
|
||||
for (int i = 0; i < key.Length; i++)
|
||||
{
|
||||
key[i] = (byte)(AesEncryptedKey[i] ^ 0x37);
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
private string EncryptHexBlock16(string plainHex)
|
||||
{
|
||||
byte[] plain = HexToBytes(plainHex);
|
||||
if (plain.Length != 16) throw new InvalidOperationException("Plain must be 16 bytes.");
|
||||
return BytesToHex(Aes256EcbEncrypt(plain));
|
||||
}
|
||||
|
||||
private string DecryptHexBlock16(string cipherHex)
|
||||
{
|
||||
byte[] cipher = HexToBytes(cipherHex);
|
||||
if (cipher.Length != 16) throw new InvalidOperationException("Cipher must be 16 bytes.");
|
||||
return BytesToHex(Aes256EcbDecrypt(cipher));
|
||||
}
|
||||
|
||||
private byte[] Aes256EcbEncrypt(byte[] data)
|
||||
{
|
||||
using (Aes aes = Aes.Create())
|
||||
{
|
||||
aes.Key = _aesKey32;
|
||||
aes.Mode = CipherMode.ECB;
|
||||
aes.Padding = PaddingMode.None;
|
||||
|
||||
using (ICryptoTransform encryptor = aes.CreateEncryptor())
|
||||
{
|
||||
return encryptor.TransformFinalBlock(data, 0, data.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] Aes256EcbDecrypt(byte[] data)
|
||||
{
|
||||
using (Aes aes = Aes.Create())
|
||||
{
|
||||
aes.Key = _aesKey32;
|
||||
aes.Mode = CipherMode.ECB;
|
||||
aes.Padding = PaddingMode.None;
|
||||
|
||||
using (ICryptoTransform decryptor = aes.CreateDecryptor())
|
||||
{
|
||||
return decryptor.TransformFinalBlock(data, 0, data.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GenerateRandomBlock16Hex()
|
||||
{
|
||||
byte[] buffer = new byte[16];
|
||||
RandomNumberGenerator.Fill(buffer);
|
||||
return BytesToHex(buffer);
|
||||
}
|
||||
|
||||
private static byte[] HexToBytes(string hex)
|
||||
{
|
||||
if (hex == null) throw new ArgumentNullException(nameof(hex));
|
||||
hex = hex.Trim();
|
||||
|
||||
if (hex.Length % 2 != 0)
|
||||
{
|
||||
throw new InvalidOperationException("Hex string length must be even.");
|
||||
}
|
||||
|
||||
byte[] result = new byte[hex.Length / 2];
|
||||
for (int i = 0; i < result.Length; i++)
|
||||
{
|
||||
result[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static string BytesToHex(byte[] bytes)
|
||||
{
|
||||
StringBuilder builder = new StringBuilder(bytes.Length * 2);
|
||||
for (int i = 0; i < bytes.Length; i++)
|
||||
{
|
||||
builder.AppendFormat("{0:X2}", bytes[i]);
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static bool IsHexString(string value, int expectedLength)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value) || value.Length != expectedLength)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < value.Length; i++)
|
||||
{
|
||||
if (!Uri.IsHexDigit(value[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a37a338f0b42c7d45b7fae0e3702ea38
|
||||
244
Assets/Scripts/Controls/Inputs/ComSystem/ComMultiManager.cs
Normal file
244
Assets/Scripts/Controls/Inputs/ComSystem/ComMultiManager.cs
Normal file
@ -0,0 +1,244 @@
|
||||
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 ComMultiManager
|
||||
{
|
||||
private readonly Dictionary<string, ComDeviceClient> _devices = new();
|
||||
private readonly ConcurrentQueue<string> _coinQueue = new();
|
||||
private readonly ConcurrentQueue<DeviceInputMessage> _inputQueue = new();
|
||||
private readonly ConcurrentQueue<string> _authorizedQueue = 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 string _defaultDeviceId = string.Empty;
|
||||
|
||||
public ComMultiManager(
|
||||
bool useAllDetectedPorts,
|
||||
string[] explicitPorts,
|
||||
string defaultDevicePort,
|
||||
bool enableEncryptedHandshake,
|
||||
int handshakeTimeoutMs,
|
||||
bool keepAlive,
|
||||
string roleRequestCommand,
|
||||
string roleResponsePrefix,
|
||||
int roleRequestTimeoutMs,
|
||||
string mainControllerRole)
|
||||
{
|
||||
_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;
|
||||
}
|
||||
|
||||
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("ComMultiManager", "Не найдено 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);
|
||||
}
|
||||
|
||||
_devices.Clear();
|
||||
_defaultDeviceId = string.Empty;
|
||||
|
||||
while (_coinQueue.TryDequeue(out _)) { }
|
||||
while (_inputQueue.TryDequeue(out _)) { }
|
||||
while (_authorizedQueue.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 string GetDeviceRole(string deviceId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(deviceId)) return string.Empty;
|
||||
return _devices.TryGetValue(deviceId, out ComDeviceClient client)
|
||||
? client.ControllerRole
|
||||
: string.Empty;
|
||||
}
|
||||
|
||||
private string[] SelectPorts(string[] allPorts)
|
||||
{
|
||||
if (_useAllDetectedPorts) return allPorts;
|
||||
if (_explicitPorts.Length == 0) return Array.Empty<string>();
|
||||
|
||||
HashSet<string> explicitSet = new HashSet<string>(_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 ComDeviceClient(
|
||||
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.Authorized += OnDeviceAuthorized;
|
||||
client.Disconnected += OnDeviceDisconnected;
|
||||
}
|
||||
|
||||
private void UnsubscribeDevice(ComDeviceClient client)
|
||||
{
|
||||
client.CoinAccepted -= OnDeviceCoinAccepted;
|
||||
client.InputReceived -= OnDeviceInputReceived;
|
||||
client.Authorized -= OnDeviceAuthorized;
|
||||
client.Disconnected -= OnDeviceDisconnected;
|
||||
}
|
||||
|
||||
private void OnDeviceCoinAccepted(string deviceId)
|
||||
{
|
||||
_coinQueue.Enqueue(deviceId);
|
||||
}
|
||||
|
||||
private void OnDeviceInputReceived(string deviceId, string inputBits)
|
||||
{
|
||||
_inputQueue.Enqueue(new DeviceInputMessage(deviceId, inputBits));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
_authorizedQueue.Enqueue(deviceId);
|
||||
}
|
||||
|
||||
private void OnDeviceDisconnected(string deviceId)
|
||||
{
|
||||
if (!_devices.TryGetValue(deviceId, out ComDeviceClient client)) return;
|
||||
UnsubscribeDevice(client);
|
||||
_devices.Remove(deviceId);
|
||||
|
||||
if (_defaultDeviceId == deviceId)
|
||||
{
|
||||
_defaultDeviceId = _devices.Keys.FirstOrDefault() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal readonly struct DeviceInputMessage
|
||||
{
|
||||
public readonly string DeviceId;
|
||||
public readonly string InputBits;
|
||||
|
||||
public DeviceInputMessage(string deviceId, string inputBits)
|
||||
{
|
||||
DeviceId = deviceId;
|
||||
InputBits = inputBits;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: edad4940535f5fb46bb83766432da396
|
||||
13
Assets/Scripts/Controls/Inputs/ComSystem/ComSystemManager.cs
Normal file
13
Assets/Scripts/Controls/Inputs/ComSystem/ComSystemManager.cs
Normal file
@ -0,0 +1,13 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace RR.Common.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Новый публичный компонент COM-системы.
|
||||
/// Наследует реализацию от <see cref="ComWrapper"/> для плавной миграции.
|
||||
/// </summary>
|
||||
[AddComponentMenu("RR/Controls/Com System Manager")]
|
||||
public class ComSystemManager : ComWrapper
|
||||
{
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c4ba28ebcf4f5e42bddbc16592fe81c
|
||||
@ -1,7 +1,4 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using RR.Game2D.Core.Managers;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
@ -9,607 +6,226 @@ using UnityEngine.Events;
|
||||
namespace RR.Common.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Основной класс-обертка для работы с COM-портом и периферийной платой.
|
||||
/// Управляет всей коммуникацией с аппаратным обеспечением: прием монет, чтение кнопок, управление подсветкой, отправка сигналов.
|
||||
/// Совместимый фасад над мульти-устройственной COM-системой.
|
||||
/// Старый API сохранен для текущего проекта.
|
||||
/// </summary>
|
||||
public class ComWrapper : MonoBehaviour
|
||||
{
|
||||
//Настраиваем из инспектора
|
||||
[Header("Основные настройки")]
|
||||
[SerializeField] private bool saveMoneyBetweenSessions;
|
||||
[SerializeField] private bool enablePrizeChecking = false; // Если хопер с проверкой наличия приза ставим true
|
||||
[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 = "";
|
||||
|
||||
public static UnityEvent<int> OnCreditAdded;
|
||||
public static UnityEvent<int> OnCreditRemoved;
|
||||
public static UnityEvent<string> OnInputReceived;
|
||||
public static Action OnInitialize;
|
||||
|
||||
#region Приватные поля
|
||||
|
||||
private const string REQUEST_INPUT = "91";
|
||||
public static UnityEvent<string, string> OnDeviceInputReceived;
|
||||
public static UnityEvent<string> OnDeviceAuthorized;
|
||||
|
||||
private static int h; // идентификатор хэндла
|
||||
private static readonly object bufferLock = new object(); // Блокировка для синхронизации доступа к COM-порту
|
||||
private static ComWrapper _instance;
|
||||
private static int _creditsCount;
|
||||
|
||||
private static int CreditsCount;
|
||||
|
||||
// Асинхронная работа
|
||||
private CancellationTokenSource pollCancellationTokenSource;
|
||||
private int inputToggle; // Флаг для Проверки получения INPUT от платы
|
||||
|
||||
// Очередь для обработки COIN в главном потоке
|
||||
private static readonly System.Collections.Concurrent.ConcurrentQueue<int> coinQueue = new System.Collections.Concurrent.ConcurrentQueue<int>();
|
||||
|
||||
// Защита от спама COIN: отслеживание времени последней монеты
|
||||
private static long lastCoinTime; // в миллисекундах
|
||||
private const long COIN_DEBOUNCE_MS = 250; // Минимальный интервал между монетами (250ms)
|
||||
|
||||
// StringBuilder для эффективного формирования строки INPUT без лишних аллокаций
|
||||
private static readonly StringBuilder inputBuilder = new StringBuilder(32);
|
||||
|
||||
// Кэш последнего состояния INPUT
|
||||
// ВАЖНО: 0 = кнопка нажата, 1 = кнопка отпущена, поэтому по умолчанию все единицы
|
||||
private static string lastInputBits = "11111111111111111111111111"; // 26 бит по умолчанию
|
||||
|
||||
// Контроллер подсветки (добавляется автоматически вместе с этим компонентом)
|
||||
private ComMultiManager _manager;
|
||||
private LightController _light;
|
||||
|
||||
/// <summary>Доступ к контроллеру подсветки.</summary>
|
||||
private bool _initializeInvoked;
|
||||
|
||||
public LightController Light => _light;
|
||||
|
||||
#endregion
|
||||
|
||||
//===========================start: PUBLIC API===========================//
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует систему работы с COM-портом.
|
||||
/// Открывает соединение, настраивает события и запускает асинхронные запросы состояния.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Обязателен к вызову при старте приложения. Выполняет:
|
||||
/// - Создание UnityEvent'ов для уведомлений
|
||||
/// - Загрузку количества кредитов из PlayerPrefs
|
||||
/// - Открытие COM-порта
|
||||
/// - Запуск асинхронного опроса пинов (если включена проверка призов)
|
||||
/// </remarks>
|
||||
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);
|
||||
_creditsCount = PlayerPrefs.GetInt("CoinsCount", 0);
|
||||
|
||||
// Получаем или создаём LightController
|
||||
_light = GetComponent<LightController>() ?? gameObject.AddComponent<LightController>();
|
||||
|
||||
initPort();
|
||||
_manager = new ComMultiManager(
|
||||
useAllDetectedPorts,
|
||||
explicitPorts,
|
||||
defaultDevicePort,
|
||||
enableEncryptedHandshake,
|
||||
handshakeTimeoutMs,
|
||||
keepAlive || enablePrizeChecking,
|
||||
roleRequestCommand,
|
||||
roleResponsePrefix,
|
||||
roleRequestTimeoutMs,
|
||||
mainControllerRole);
|
||||
|
||||
// Отправляем сигнал запуска игры
|
||||
Send("2171");
|
||||
_light.Blink();
|
||||
|
||||
// Запускаем асинхронный опрос порта
|
||||
pollCancellationTokenSource = new CancellationTokenSource();
|
||||
StartAsyncPolling();
|
||||
|
||||
// Запускаем keep-alive механизм если включен
|
||||
if (keepAlive || enablePrizeChecking)
|
||||
{
|
||||
StartKeepAliveProcess();
|
||||
}
|
||||
|
||||
OnInitialize?.Invoke();
|
||||
_manager.InitializeDevices();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Добавляет один кредит в систему и сохраняет обновленное количество.
|
||||
/// Вызывается автоматически событием при получении сигнала "COIN" из COM-порта.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Увеличивает счетчик кредитов и вызывает событие onCoinAdded для уведомления подписчиков.
|
||||
/// Автоматически сохраняет состояние в PlayerPrefs.
|
||||
/// </remarks>
|
||||
public static void AddCredit()
|
||||
{
|
||||
CreditsCount++;
|
||||
PlayerPrefs.SetInt("CoinsCount", CreditsCount);
|
||||
OnCreditAdded?.Invoke(CreditsCount);
|
||||
|
||||
// PlayerPrefs.SetInt(PrizeSystem.key_starts, PlayerPrefs.GetInt(PrizeSystem.key_starts) + 1);
|
||||
// PlayerPrefs.SetInt(PrizeSystem.key_startsNul, PlayerPrefs.GetInt(PrizeSystem.key_startsNul) + 1);
|
||||
_creditsCount++;
|
||||
PlayerPrefs.SetInt("CoinsCount", _creditsCount);
|
||||
OnCreditAdded?.Invoke(_creditsCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Отправляет произвольную строковую команду на периферийную плату через COM-порт.
|
||||
/// </summary>
|
||||
/// <param name="message">Команда для отправки</param>
|
||||
/// <remarks>
|
||||
/// Используется для отправки пользовательских команд на плату.
|
||||
/// Команда должна быть в формате, ожидаемом прошивкой платы.
|
||||
/// </remarks>
|
||||
public static void Send(string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (h <= 0)
|
||||
{
|
||||
GameManager.Logger.Log("ComWrapper: Попытка отправки при закрытом порте");
|
||||
return;
|
||||
}
|
||||
|
||||
ComNativeWrapper.Serial_WriteStringUTF8(h, message, 1);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
GameManager.Logger.Log($"Ошибка отправки сигнала: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Выполняет списание платы за запуск игры.
|
||||
/// Проверяет наличие доступных кредитов или режим бесплатной игры.
|
||||
/// При успешном списании увеличивает счетчики запусков.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// true - если списание прошло успешно (бесплатный режим или есть кредиты),
|
||||
/// false - если кредитов недостаточно и платный режим
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// В случае успеха:
|
||||
/// - Увеличивает общие счетчики запусков (key_starts и key_startsNul)
|
||||
/// - При платном режиме уменьшает количество кредитов на 1
|
||||
/// - Вызывает событие onCoinRemoved при списании кредита
|
||||
/// </remarks>
|
||||
public static bool Pay(int amount = 1)
|
||||
{
|
||||
// блок проверки оплаты
|
||||
// if (SettingsManager.instance.GetSettingValue("Бесплатная игра") == "Вкл")
|
||||
// {
|
||||
// //Счетчики пробега
|
||||
// PlayerPrefs.SetInt(PrizeSystem.key_starts, PlayerPrefs.GetInt(PrizeSystem.key_starts) + 1);
|
||||
// PlayerPrefs.SetInt(PrizeSystem.key_startsNul, PlayerPrefs.GetInt(PrizeSystem.key_startsNul) + 1);
|
||||
//
|
||||
// return true;
|
||||
// }
|
||||
|
||||
if (CreditsCount >= amount)
|
||||
if (_creditsCount < amount)
|
||||
{
|
||||
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;
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Запускает мигание подсветки. Делегируется в <see cref="LightController"/>.
|
||||
/// </summary>
|
||||
public void Blink() => _light.Blink();
|
||||
public static int GetCredits() => _creditsCount;
|
||||
|
||||
/// <summary>
|
||||
/// Останавливает мигание подсветки. Делегируется в <see cref="LightController"/>.
|
||||
/// </summary>
|
||||
public void StopBlink(bool enableHat = true) => _light.StopBlink(enableHat);
|
||||
|
||||
/// <summary>
|
||||
/// Получаем текущий баланс кредитов
|
||||
/// </summary>
|
||||
public static int GetCredits() => CreditsCount;
|
||||
|
||||
/// <summary>
|
||||
/// Устанавливает количество кредитов напрямую (для синхронизации с балансом)
|
||||
/// </summary>
|
||||
public static void SetCredits(int amount)
|
||||
{
|
||||
int oldCredits = CreditsCount;
|
||||
CreditsCount = Mathf.Max(0, amount);
|
||||
PlayerPrefs.SetInt("CoinsCount", CreditsCount);
|
||||
int oldCredits = _creditsCount;
|
||||
_creditsCount = Mathf.Max(0, amount);
|
||||
PlayerPrefs.SetInt("CoinsCount", _creditsCount);
|
||||
|
||||
if (CreditsCount > oldCredits)
|
||||
if (_creditsCount > oldCredits)
|
||||
{
|
||||
OnCreditAdded?.Invoke(CreditsCount);
|
||||
OnCreditAdded?.Invoke(_creditsCount);
|
||||
}
|
||||
else if (CreditsCount < oldCredits)
|
||||
else if (_creditsCount < oldCredits)
|
||||
{
|
||||
OnCreditRemoved?.Invoke(CreditsCount);
|
||||
OnCreditRemoved?.Invoke(_creditsCount);
|
||||
}
|
||||
}
|
||||
|
||||
//===========================end: PUBLIC API===========================//
|
||||
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 void Blink() => _light.Blink();
|
||||
|
||||
public void StopBlink(bool enableHat = true) => _light.StopBlink(enableHat);
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Обрабатываем COIN из очереди в главном потоке
|
||||
if (_manager == null) return;
|
||||
|
||||
int coinsProcessed = 0;
|
||||
|
||||
while (coinQueue.TryDequeue(out _))
|
||||
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($"Обработано монет: {coinsProcessed}");
|
||||
GameManager.Logger?.Log("ComWrapper", $"Обработано монет: {coinsProcessed}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
StopAsyncPolling();
|
||||
ClosePort();
|
||||
_manager?.ShutdownAll(false);
|
||||
_manager = null;
|
||||
_initializeInvoked = false;
|
||||
}
|
||||
|
||||
|
||||
private void OnApplicationQuit()
|
||||
{
|
||||
Send("2170"); // Выключаем кнопки
|
||||
Send("2130"); // Выключаем шапку
|
||||
|
||||
StopAsyncPolling();
|
||||
ClosePort();
|
||||
_manager?.ShutdownAll(true);
|
||||
_manager = null;
|
||||
_initializeInvoked = false;
|
||||
}
|
||||
|
||||
private void StopAsyncPolling()
|
||||
{
|
||||
pollCancellationTokenSource?.Cancel();
|
||||
pollCancellationTokenSource?.Dispose();
|
||||
pollCancellationTokenSource = null;
|
||||
}
|
||||
|
||||
private async void StartAsyncPolling()
|
||||
{
|
||||
try
|
||||
{
|
||||
GameManager.Logger.Log("Запуск асинхронного опроса порта");
|
||||
|
||||
while (pollCancellationTokenSource != null &&
|
||||
!pollCancellationTokenSource.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(1, pollCancellationTokenSource.Token); // Опрос каждые 1ms для уменьшения задержки
|
||||
|
||||
string comInput = await ReadBufferLineAsync(pollCancellationTokenSource.Token);
|
||||
if (!string.IsNullOrEmpty(comInput))
|
||||
{
|
||||
ProcessComInputImmediate(comInput);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
GameManager.Logger.Log("Асинхронный опрос порта остановлен");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
GameManager.Logger.Log($"Ошибка асинхронного опроса: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessComInputImmediate(string comInput)
|
||||
{
|
||||
// Debug.Log("COM: " + comInput);
|
||||
|
||||
// INPUT обработка
|
||||
if (comInput.StartsWith("INPUT"))
|
||||
{
|
||||
inputToggle = 1; // Обновляем флаг для keep-alive
|
||||
|
||||
// Проверяем, что после "INPUT" есть данные
|
||||
if (comInput.Length <= 5)
|
||||
{
|
||||
GameManager.Logger.Log("INPUT получен, но без данных");
|
||||
return;
|
||||
}
|
||||
|
||||
string rawData = comInput.Substring(5);
|
||||
|
||||
// Проверяем, что rawData не пустой
|
||||
if (string.IsNullOrWhiteSpace(rawData))
|
||||
{
|
||||
GameManager.Logger.Log("INPUT получен с пустым значением");
|
||||
return;
|
||||
}
|
||||
|
||||
// Конвертируем десятичное число в двоичную строку
|
||||
string binaryValue = DecimalToBinary(rawData);
|
||||
|
||||
// Обновляем кэш последнего состояния
|
||||
lastInputBits = binaryValue;
|
||||
|
||||
// Debug.Log($"Decimal: {rawData} → Binary: {binaryValue}");
|
||||
OnInputReceived?.Invoke(binaryValue);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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 единиц по умолчанию
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает последнее кэшированное состояние INPUT.
|
||||
/// Полезно когда нужно узнать состояние кнопок без ожидания нового события.
|
||||
/// </summary>
|
||||
/// <returns>Строка с двоичным представлением состояния кнопок</returns>
|
||||
public static string GetLastInputState() => lastInputBits;
|
||||
|
||||
#region Приватные методы
|
||||
//Ну тут по дебаг логам все должно быть понятно
|
||||
//Если интересно, то h - это хэндл. Что-то на плюсовом, типа ID соединения или что-то. Если 0, то нету соед. если меньше то ошибка, больше 0 - норм.
|
||||
private void initPort()
|
||||
{
|
||||
GameManager.Logger.Log("ComWrapper", "Старт программы");
|
||||
ComNativeWrapper.Serial_CloseAll();
|
||||
var ports = ComNativeWrapper.ListPorts();
|
||||
GameManager.Logger.Log("ComWrapper", "Ports: " + string.Join(", ", ports));
|
||||
if (ports.Length == 0) { GameManager.Logger.Log("Нет портов, завершаю"); return; }
|
||||
|
||||
string port = ports[^1];
|
||||
GameManager.Logger.Log("ComWrapper", "Открываю порт: " + port);
|
||||
|
||||
|
||||
//вот эти настройки лучше не ковырять, но если очень хочется можно поменять readTimeoutMs и writeTimeoutMs
|
||||
//Если read поставить слишком маленьким оно проебует кредиты когда они идут большой пачкой например при оплате через вендисту.
|
||||
//Там в порт спамит "COIN" в больших количествах и процентов 10 из этих строк не успевают прочитаться при таймауте 1мс, соотв. монеты проебуются.
|
||||
//Слишком большой таймаут будет тормозить игру потому что оно все выполняется синхронно и каждый кадр забирает столько мс сколько в переменной указано.
|
||||
h = ComNativeWrapper.Serial_Open(port, 115200, 8, 0, 1, 5, 10);
|
||||
|
||||
|
||||
GameManager.Logger.Log("ComWrapper", $"handle: {h}");
|
||||
if (h <= 0)
|
||||
{
|
||||
GameManager.Logger.Log("ComWrapper", "Open failed: " + ComNativeWrapper.GetLastError());
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
GameManager.Logger.Log("ComWrapper", $"Открыл порт: {port}");
|
||||
}
|
||||
|
||||
// Очищаем буфер порта от старых данных
|
||||
GameManager.Logger.Log("Очистка буфера порта от старых данных...");
|
||||
FlushPortBuffer();
|
||||
|
||||
// Инициализируем lastCoinTime текущим временем чтобы не обработать старые COIN
|
||||
lastCoinTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
GameManager.Logger.Log($"Порт инициализирован, debounce активирован");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Очищает буфер порта от накопившихся старых данных
|
||||
/// </summary>
|
||||
private static void FlushPortBuffer()
|
||||
{
|
||||
lock (bufferLock)
|
||||
{
|
||||
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; // Защита от бесконечного цикла
|
||||
}
|
||||
|
||||
GameManager.Logger.Log($"Очищено: {flushedCount} сообщений");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
GameManager.Logger.Log($"FlushPortBuffer exception: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ClosePort()
|
||||
{
|
||||
Debug.Log("Закрываю все порты");
|
||||
if (h > 0)
|
||||
{
|
||||
ComNativeWrapper.Serial_Close(h);
|
||||
h = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Чтение с порта
|
||||
private static string ReadBufferLine()
|
||||
{
|
||||
lock (bufferLock)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Проверяем валидность handle перед вызовом нативной функции
|
||||
if (h <= 0)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
// Используем локальный буфер для полной изоляции
|
||||
byte[] localBuf = new byte[1024];
|
||||
|
||||
int read = ComNativeWrapper.Serial_ReadLineUTF8_bytes(h, localBuf, localBuf.Length);
|
||||
|
||||
// Проверяем валидность значения read ПЕРЕД использованием
|
||||
if (read <= 0)
|
||||
{
|
||||
if (read < 0)
|
||||
{
|
||||
GameManager.Logger.Log("ComWrapper", $"ReadLine err={read} :: {ComNativeWrapper.GetLastError()}");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Защита от выхода за границы массива
|
||||
if (read > localBuf.Length)
|
||||
{
|
||||
GameManager.Logger.Log($"ReadBufferLine: read ({read}) превышает размер буфера ({localBuf.Length}), обрезаем");
|
||||
read = localBuf.Length;
|
||||
}
|
||||
|
||||
string s = Encoding.UTF8.GetString(localBuf, 0, read);
|
||||
|
||||
// Обрабатываем COIN сразу, но добавляем в очередь для обработки в главном потоке
|
||||
if (s == "COIN")
|
||||
{
|
||||
long currentTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
long timeSinceLastCoin = currentTime - lastCoinTime;
|
||||
|
||||
// Проверяем debounce: если прошло меньше 250ms - это спам от той же монеты
|
||||
if (timeSinceLastCoin < COIN_DEBOUNCE_MS)
|
||||
{
|
||||
GameManager.Logger.Log($"COIN проигнорирован (спам через {timeSinceLastCoin}ms < 250ms)");
|
||||
// НЕ отправляем подтверждение для спама!
|
||||
}
|
||||
else
|
||||
{
|
||||
// Это РЕАЛЬНАЯ новая монета - обрабатываем
|
||||
lastCoinTime = currentTime;
|
||||
|
||||
// Отправляем подтверждение
|
||||
int confirmSent = ComNativeWrapper.Serial_WriteStringUTF8(h, "10", 1);
|
||||
GameManager.Logger.Log($"COIN принят → Подтверждение '10' отправлено (bytes: {confirmSent})");
|
||||
|
||||
// Добавляем в очередь для обработки в главном потоке
|
||||
coinQueue.Enqueue(1);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
GameManager.Logger.Log($"ReadBufferLine exception: {e.Message}");
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Асинхронное чтение с порта. Выполняется в фоновом потоке.
|
||||
/// </summary>
|
||||
private static async Task<string> ReadBufferLineAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return string.Empty;
|
||||
|
||||
return ReadBufferLine();
|
||||
}, cancellationToken);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
GameManager.Logger.Log($"Ошибка асинхронного чтения порта: {e.Message}");
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
//просим прислать состояние цифровых входов. Приходит что то вроде "INPUT111111101"
|
||||
private static void requestInputs() => Send(REQUEST_INPUT);
|
||||
|
||||
/// <summary>
|
||||
/// Асинхронный keep-alive процесс для проверки соединения с платой
|
||||
/// </summary>
|
||||
private async void StartKeepAliveProcess()
|
||||
{
|
||||
try
|
||||
{
|
||||
GameManager.Logger.Log("Запуск асинхронной проверки соединения");
|
||||
|
||||
while (pollCancellationTokenSource != null &&
|
||||
!pollCancellationTokenSource.Token.IsCancellationRequested)
|
||||
{
|
||||
await Task.Delay(2000, pollCancellationTokenSource.Token); // Ждем 2 секунды
|
||||
|
||||
// Запрашиваем INPUT
|
||||
requestInputs();
|
||||
inputToggle = 0;
|
||||
|
||||
await Task.Delay(1000, pollCancellationTokenSource.Token); // Ждем ответа 1 секунду
|
||||
|
||||
// Проверяем, пришел ли ответ
|
||||
if (inputToggle == 0)
|
||||
{
|
||||
GameManager.Logger.Log("Плата не отвечает! Попытка переподключения...");
|
||||
await Task.Run(() =>
|
||||
{
|
||||
ClosePort();
|
||||
initPort();
|
||||
}, pollCancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
await Task.Delay(7000, pollCancellationTokenSource.Token); // Итого ~10 секунд между проверками
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
GameManager.Logger.Log("Асинхронная проверка соединения остановлена");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
GameManager.Logger.Log($"Ошибка проверки соединения: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
@ -68,7 +67,7 @@ namespace RR.Common.Controls
|
||||
/// <para>Если вызвать без <c>.Then()</c> / <c>.Loop()</c> — эффект запустится немедленно как одиночный.</para>
|
||||
/// </summary>
|
||||
/// <param name="mode">Эффект подсветки</param>
|
||||
/// <param name="durationSeconds">Время эффекта (0–99 секунд)</param>
|
||||
/// <param name="durationSeconds">Время эффекта (0–99 секунд, либо <c>-1</c> для команды без <c>YY</c>)</param>
|
||||
public LightSequence SetLight(LightMode mode, int durationSeconds)
|
||||
{
|
||||
StopSequence();
|
||||
@ -92,7 +91,7 @@ namespace RR.Common.Controls
|
||||
public void StopAllEffects()
|
||||
{
|
||||
StopSequence();
|
||||
ComWrapper.Send(EffectPrefix);
|
||||
ComSystemManager.Send(EffectPrefix);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -130,7 +129,7 @@ namespace RR.Common.Controls
|
||||
_blinkingCoroutine = null;
|
||||
}
|
||||
|
||||
ComWrapper.Send(enableHat ? hatOnCommand : hatOffCommand);
|
||||
ComSystemManager.Send(enableHat ? hatOnCommand : hatOffCommand);
|
||||
}
|
||||
|
||||
/// <summary>Возвращает <c>true</c>, если мигание шляпы сейчас активно.</summary>
|
||||
@ -147,7 +146,7 @@ namespace RR.Common.Controls
|
||||
/// </summary>
|
||||
public void SendCommand(LightMode mode, int durationSeconds)
|
||||
{
|
||||
ComWrapper.Send(BuildCommand(mode, durationSeconds));
|
||||
ComSystemManager.Send(BuildCommand(mode, durationSeconds));
|
||||
}
|
||||
|
||||
// ReSharper disable once IteratorNeverReturns — корутина намеренно бесконечная
|
||||
@ -155,7 +154,7 @@ namespace RR.Common.Controls
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
ComWrapper.Send(Random.Range(0f, 1f) < 0.5f ? hatOnCommand : hatOffCommand);
|
||||
ComSystemManager.Send(Random.Range(0f, 1f) < 0.5f ? hatOnCommand : hatOffCommand);
|
||||
|
||||
float wait = blinkIntervalVariance > 0f
|
||||
? Mathf.Max(0.05f, interval + Random.Range(-blinkIntervalVariance, blinkIntervalVariance))
|
||||
@ -171,95 +170,12 @@ namespace RR.Common.Controls
|
||||
return EffectPrefix;
|
||||
|
||||
int xx = (int)mode;
|
||||
if (durationSeconds == -1)
|
||||
return $"{EffectPrefix}{xx:D2}";
|
||||
|
||||
int yy = Mathf.Clamp(durationSeconds, 0, 99);
|
||||
return $"{EffectPrefix}{xx:D2}{yy:D2}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Строитель последовательности световых эффектов.
|
||||
/// Создаётся через <see cref="LightController.SetLight(LightMode,int)"/>.
|
||||
/// <example><code>
|
||||
/// SetLight(LightMode.VerticalWave, 3).Then(LightMode.DiagonalWave, 3).Loop();
|
||||
/// SetLight(LightMode.VerticalWave, 3).Then(LightMode.Off);
|
||||
/// SetLight(LightMode.Rainbow, 5);
|
||||
/// </code></example>
|
||||
/// </summary>
|
||||
public class LightSequence
|
||||
{
|
||||
private readonly struct Step
|
||||
{
|
||||
public readonly LightMode Mode;
|
||||
public readonly int DurationSeconds;
|
||||
public Step(LightMode mode, int durationSeconds) { Mode = mode; DurationSeconds = durationSeconds; }
|
||||
}
|
||||
|
||||
private readonly List<Step> _steps = new();
|
||||
private readonly LightController _controller;
|
||||
private bool _looping;
|
||||
private Coroutine _coroutine;
|
||||
|
||||
internal LightSequence(LightController controller, LightMode firstMode, int firstDuration)
|
||||
{
|
||||
_controller = controller;
|
||||
_steps.Add(new Step(firstMode, firstDuration));
|
||||
}
|
||||
|
||||
/// <summary>Добавляет следующий эффект с временем по умолчанию.</summary>
|
||||
public LightSequence Then(LightMode mode) => Then(mode, _controller.DefaultDuration);
|
||||
|
||||
/// <summary>Добавляет следующий эффект с указанным временем.</summary>
|
||||
public LightSequence Then(LightMode mode, int durationSeconds)
|
||||
{
|
||||
_steps.Add(new Step(mode, durationSeconds));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Зацикливает последовательность. Для остановки — <see cref="LightController.StopSequence"/>.
|
||||
/// </summary>
|
||||
public LightSequence Loop()
|
||||
{
|
||||
_looping = true;
|
||||
Play();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Запускает последовательность один раз.</summary>
|
||||
public LightSequence Play()
|
||||
{
|
||||
Stop();
|
||||
_coroutine = _controller.StartCoroutine(RunSequence());
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Останавливает выполнение последовательности.</summary>
|
||||
public void Stop()
|
||||
{
|
||||
if (_coroutine == null) return;
|
||||
_controller.StopCoroutine(_coroutine);
|
||||
_coroutine = null;
|
||||
}
|
||||
|
||||
/// <summary>Возвращает <c>true</c>, если последовательность сейчас выполняется.</summary>
|
||||
public bool IsPlaying => _coroutine != null;
|
||||
|
||||
// ReSharper disable once IteratorNeverReturns — цикл намеренно бесконечный при _looping=true
|
||||
private IEnumerator RunSequence()
|
||||
{
|
||||
do
|
||||
{
|
||||
foreach (Step step in _steps)
|
||||
{
|
||||
_controller.SendCommand(step.Mode, step.DurationSeconds);
|
||||
|
||||
if (step.Mode != LightMode.Off && step.DurationSeconds > 0)
|
||||
yield return new WaitForSeconds(step.DurationSeconds);
|
||||
}
|
||||
}
|
||||
while (_looping);
|
||||
|
||||
_coroutine = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
93
Assets/Scripts/Controls/Inputs/ComSystem/LightSequence.cs
Normal file
93
Assets/Scripts/Controls/Inputs/ComSystem/LightSequence.cs
Normal file
@ -0,0 +1,93 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace RR.Common.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// Строитель последовательности световых эффектов.
|
||||
/// Создаётся через <see cref="LightController.SetLight(LightMode,int)"/>.
|
||||
/// <example><code>
|
||||
/// SetLight(LightMode.VerticalWave, 3).Then(LightMode.DiagonalWave, 3).Loop();
|
||||
/// SetLight(LightMode.VerticalWave, 3).Then(LightMode.Off);
|
||||
/// SetLight(LightMode.Rainbow, 5);
|
||||
/// </code></example>
|
||||
/// </summary>
|
||||
public class LightSequence
|
||||
{
|
||||
private readonly struct Step
|
||||
{
|
||||
public readonly LightMode Mode;
|
||||
public readonly int DurationSeconds;
|
||||
public Step(LightMode mode, int durationSeconds) { Mode = mode; DurationSeconds = durationSeconds; }
|
||||
}
|
||||
|
||||
private readonly List<Step> _steps = new();
|
||||
private readonly LightController _controller;
|
||||
private bool _looping;
|
||||
private Coroutine _coroutine;
|
||||
|
||||
internal LightSequence(LightController controller, LightMode firstMode, int firstDuration)
|
||||
{
|
||||
_controller = controller;
|
||||
_steps.Add(new Step(firstMode, firstDuration));
|
||||
}
|
||||
|
||||
/// <summary>Добавляет следующий эффект с временем по умолчанию.</summary>
|
||||
public LightSequence Then(LightMode mode) => Then(mode, _controller.DefaultDuration);
|
||||
|
||||
/// <summary>Добавляет следующий эффект с указанным временем.</summary>
|
||||
public LightSequence Then(LightMode mode, int durationSeconds)
|
||||
{
|
||||
_steps.Add(new Step(mode, durationSeconds));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Зацикливает последовательность. Для остановки — <see cref="LightController.StopSequence"/>.
|
||||
/// </summary>
|
||||
public LightSequence Loop()
|
||||
{
|
||||
_looping = true;
|
||||
Play();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Запускает последовательность один раз.</summary>
|
||||
public LightSequence Play()
|
||||
{
|
||||
Stop();
|
||||
_coroutine = _controller.StartCoroutine(RunSequence());
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>Останавливает выполнение последовательности.</summary>
|
||||
public void Stop()
|
||||
{
|
||||
if (_coroutine == null) return;
|
||||
_controller.StopCoroutine(_coroutine);
|
||||
_coroutine = null;
|
||||
}
|
||||
|
||||
/// <summary>Возвращает <c>true</c>, если последовательность сейчас выполняется.</summary>
|
||||
public bool IsPlaying => _coroutine != null;
|
||||
|
||||
// ReSharper disable once IteratorNeverReturns — цикл намеренно бесконечный при _looping=true
|
||||
private IEnumerator RunSequence()
|
||||
{
|
||||
do
|
||||
{
|
||||
foreach (Step step in _steps)
|
||||
{
|
||||
_controller.SendCommand(step.Mode, step.DurationSeconds);
|
||||
|
||||
if (step.Mode != LightMode.Off && step.DurationSeconds > 0)
|
||||
yield return new WaitForSeconds(step.DurationSeconds);
|
||||
}
|
||||
}
|
||||
while (_looping);
|
||||
|
||||
_coroutine = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f2022e7f65aa9734ba4d9349e2fc4bdb
|
||||
315
Assets/Scripts/Controls/Inputs/ComSystem/PrizeStandController.cs
Normal file
315
Assets/Scripts/Controls/Inputs/ComSystem/PrizeStandController.cs
Normal file
@ -0,0 +1,315 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using RR.Common.Controls;
|
||||
using UnityEngine;
|
||||
|
||||
//Зависит от ComNativeWrapper и одноименного плагина
|
||||
//рекомендуется соединение с таймаутом не меньше 10мс. Иначе вероятно busy loop и много алокаций.
|
||||
public static class PrizeStandController
|
||||
{
|
||||
/// <summary>
|
||||
/// По завершении инициализации.
|
||||
/// bool - успех/неуспех.
|
||||
/// string - сообщение в случае неуспеха
|
||||
/// </summary>
|
||||
public static event Action<bool, string> InitializationComplete;
|
||||
/// <summary>
|
||||
/// По завершении операции открытия.
|
||||
/// bool - успех/неуспех.
|
||||
/// string - сообщение в случае неуспеха
|
||||
/// </summary>
|
||||
public static event Action<bool, string> OpeningFinished;
|
||||
|
||||
private const string REQUEST_STAND_STATUS = "25";
|
||||
private const string OPEN_BOX_PREFIX = "2501";
|
||||
private const float STATUS_REQUEST_TIMEOUT = 3;
|
||||
private const float WAIT_OPEN_STATUS_TIMEOUT = 5;
|
||||
|
||||
private static readonly IReadOnlyList<BoxStatus> READY_TO_OPEN_STATUSES = new[] { BoxStatus.OK, BoxStatus.OKplusCard };
|
||||
|
||||
private static int handle = -1;
|
||||
private static int boxesCount;
|
||||
private static readonly SemaphoreSlim _lock = new(1, 1);
|
||||
private static bool initialized = false;
|
||||
|
||||
private static ILogger _logger;
|
||||
|
||||
#region PUBLIC API
|
||||
/// <summary>
|
||||
/// Инициализировать модуль витрины.
|
||||
/// </summary>
|
||||
/// <param name="handleId">Id хэндла полученное после ComNativeWrapper.Serial_Open</param>
|
||||
/// <param name="countOfBoxes">Количество физических коробок витрины</param>
|
||||
/// <param name="logger">(опционально) Логгер</param>
|
||||
public static void Initialize(int handleId, int countOfBoxes, ILogger logger)
|
||||
{
|
||||
bool hasError = false;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
//Проверка хэндла. h<=0 - ошибка открытия порта
|
||||
if (handleId <= 0)
|
||||
{
|
||||
hasError = true;
|
||||
sb.AppendLine($"[Модуль витрины] Переданный handle={handleId}. Ожидалось значение>0.");
|
||||
}
|
||||
else handle = handleId;
|
||||
|
||||
//Проверка количества коробок
|
||||
if (countOfBoxes <= 0)
|
||||
{
|
||||
hasError = true;
|
||||
sb.AppendLine($"[Модуль витрины] Попытка задать количество коробок={countOfBoxes}. Ожидалось значение>0.");
|
||||
}
|
||||
else boxesCount = countOfBoxes;
|
||||
|
||||
//Установка логгера
|
||||
_logger = logger;
|
||||
if (_logger == null)
|
||||
sb.AppendLine($"[Модуль витрины] Получен логгер NULL. Если нужно логгирование, передайте дествующий логгер.");
|
||||
|
||||
//Завершение
|
||||
if (hasError)
|
||||
{
|
||||
initialized = false;
|
||||
handle = -1;
|
||||
boxesCount = 0;
|
||||
|
||||
InitializationComplete?.Invoke(false, sb.ToString());
|
||||
}
|
||||
else
|
||||
{
|
||||
initialized = true;
|
||||
InitializationComplete?.Invoke(true, sb.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Открыть коробку по номеру
|
||||
/// </summary>
|
||||
/// <param name="boxNumber">Номер коробки (считаем с 1)</param>
|
||||
public static async Task OpenBoxAsync(int boxNumber)
|
||||
{
|
||||
if(!await _lock.WaitAsync(0))
|
||||
{
|
||||
OpeningFinished?.Invoke(false, "Выдача уже в процессе");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
//Валидация
|
||||
var validationResult = await CanExecuteOpenAsync(boxNumber);
|
||||
if (!validationResult.OK)
|
||||
{
|
||||
OpeningFinished?.Invoke(false, validationResult.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
SendOpenCommand(boxNumber);
|
||||
|
||||
//Обработка результатов
|
||||
BoxStatus openStatus = await ReadBoxStatus(boxNumber, 1, WAIT_OPEN_STATUS_TIMEOUT);
|
||||
if (openStatus == BoxStatus.OPENED)
|
||||
{
|
||||
OpeningFinished?.Invoke(true, $"Ячейка {boxNumber} успешно открыта");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger?.Log($"[Модуль витрины] Ошибка при открытии ячейки {boxNumber}");
|
||||
OpeningFinished?.Invoke(false, $"Ошибка при открытии ячейки номер {boxNumber}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.Log($"[Модуль витрины] Ошибка при открытии ячейки {boxNumber}: {ex.Message}");
|
||||
OpeningFinished?.Invoke(false, ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region HELPERS
|
||||
private static void SendOpenCommand(int boxNumber)
|
||||
=> ComNativeWrapper.Serial_WriteStringUTF8(handleId: handle,
|
||||
utf8: $"{OPEN_BOX_PREFIX}{boxNumber:D2}", //D2 приводит в формат XX (2 знака, ноль впереди)
|
||||
appendNewline: 1);
|
||||
private static void SendRequestStatusCommand()
|
||||
=> ComNativeWrapper.Serial_WriteStringUTF8(handle, REQUEST_STAND_STATUS, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Проверка может ли модуль произвести выдачу
|
||||
/// </summary>
|
||||
private static async Task<(bool OK, string Message)> CanExecuteOpenAsync(int boxNumber)
|
||||
{
|
||||
bool isOk = true;
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.AppendLine($"[Модуль витрины. Проверка перед открытием]");
|
||||
|
||||
//Проверка инициализации
|
||||
if (!initialized)
|
||||
{
|
||||
isOk = false;
|
||||
sb.AppendLine("- Модуль не инициализирован.");
|
||||
}
|
||||
|
||||
bool boxNumberIsValid = boxNumber > 0 && boxNumber <= boxesCount;
|
||||
if (!boxNumberIsValid)
|
||||
{
|
||||
isOk = false;
|
||||
sb.AppendLine($"- Номер коробки={boxNumber} невалидный. Требуется от 1 до {boxesCount}.");
|
||||
}
|
||||
|
||||
//Не лезем в порт с запросом статуса коробки, если то, что выше дало НеОК
|
||||
if (isOk)
|
||||
{
|
||||
SendRequestStatusCommand();
|
||||
BoxStatus boxStatus = await ReadBoxStatus(boxNumber, boxesCount, STATUS_REQUEST_TIMEOUT);
|
||||
|
||||
if (!READY_TO_OPEN_STATUSES.Contains(boxStatus))
|
||||
{
|
||||
isOk = false;
|
||||
sb.AppendLine($"- Статус коробки {boxNumber}: {boxStatus} не позволяет провести открытие.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine($"- Невозможно проверить статус коробки {boxNumber} из-за ошибок выше.");
|
||||
}
|
||||
|
||||
return (isOk, sb.ToString());
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Читать статус заданной ячейки витрины. Выход когда прочитали заданное количество вхождений, или по таймауту.
|
||||
/// </summary>
|
||||
/// <param name="expectedBoxNumber">Номер ячейки которую мы ждем</param>
|
||||
/// <param name="expectedEntriesCount">Сколько ячеек мы ждем</param>
|
||||
/// <param name="timeout">Таймаут операции(сек.)</param>
|
||||
/// <returns></returns>
|
||||
private static async Task<BoxStatus> ReadBoxStatus(int expectedBoxNumber, int expectedEntriesCount, float timeout)
|
||||
{
|
||||
byte[] buffer = new byte[1024];
|
||||
|
||||
//Изначально предполагаем что статус нужной коробки не пришел
|
||||
BoxStatus status = BoxStatus.NotFound;
|
||||
|
||||
int boxesRecieved = 0;
|
||||
var sw = Stopwatch.StartNew();
|
||||
|
||||
while (sw.Elapsed.TotalSeconds < timeout && boxesRecieved < expectedEntriesCount)
|
||||
{
|
||||
//ждать пока всё не считаем
|
||||
int entryLength = await Task.Run(() => ComNativeWrapper.Serial_ReadLineUTF8_bytes(handle, buffer, buffer.Length));
|
||||
|
||||
if (entryLength == 0) //ничего не пришло
|
||||
{
|
||||
continue;
|
||||
}
|
||||
else if (entryLength < 0) //ошибка
|
||||
{
|
||||
_logger?.Log($"Ошибка при считывании статуса ячеек={entryLength} :: {ComNativeWrapper.GetLastError()}");
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
string entry = Encoding.UTF8.GetString(buffer, 0, entryLength).Trim();
|
||||
var ParseResult = ParseBoxStatus(entry);
|
||||
|
||||
if(ParseResult.BoxNumber == -1)
|
||||
{
|
||||
_logger?.Log($"[Модуль витрины. Ошибка получения статуса ячейки]\n{ParseResult.Message}");
|
||||
}
|
||||
else
|
||||
{
|
||||
boxesRecieved++;
|
||||
if (ParseResult.BoxNumber == expectedBoxNumber)
|
||||
status = ParseResult.Status;
|
||||
}
|
||||
}
|
||||
|
||||
if (status == BoxStatus.NotFound)
|
||||
_logger?.Log($"[Модуль витрины. Не дождался ответа по статусу ячейки]\n" +
|
||||
$"Ожидаемый номер: {expectedBoxNumber}\n" +
|
||||
$"Получено статусов ячеек: {boxesRecieved}/{expectedEntriesCount}");
|
||||
return status;
|
||||
}
|
||||
|
||||
private static (int BoxNumber, BoxStatus Status, string Message) ParseBoxStatus(string entryString)
|
||||
{
|
||||
int number = -1;
|
||||
BoxStatus status = BoxStatus.ParsingError;
|
||||
string message = "";
|
||||
|
||||
|
||||
|
||||
if (entryString.StartsWith("BOX"))
|
||||
{
|
||||
var noPrefixEntry = entryString.Substring(3); //Убираем "BOX"
|
||||
string[] parts = noPrefixEntry.Split('_'); //ожидаем массив [<номер_коробки>, <статус_коробки>]
|
||||
if (parts.Length == 2)
|
||||
{
|
||||
//Парсим номер коробки
|
||||
bool boxNumberParsed = (int.TryParse(parts[0], out int parsedNumber));
|
||||
|
||||
if (boxNumberParsed)
|
||||
{
|
||||
number = parsedNumber;
|
||||
status = MapStatusFromString(parts[1]);
|
||||
}
|
||||
else message = $"Не удалось спарсить номер коробки: {entryString}";
|
||||
}
|
||||
else
|
||||
{
|
||||
message = $"Ошибка парсинга. Часть строки не пришла: {entryString}";
|
||||
}
|
||||
}
|
||||
else message = $"Неожиданный префикс при парсинге ячеек: {entryString}";
|
||||
|
||||
|
||||
return (number, status, message);
|
||||
|
||||
}
|
||||
private static BoxStatus MapStatusFromString(string statusString) => statusString switch
|
||||
{
|
||||
"OK" => BoxStatus.OK,
|
||||
"OK+CARD" => BoxStatus.OKplusCard,
|
||||
"EMPTY" => BoxStatus.Empty,
|
||||
"OPEN" => BoxStatus.Open,
|
||||
|
||||
"OPENED" => BoxStatus.OPENED,
|
||||
"OPEN_ERROR" => BoxStatus.OPEN_ERROR,
|
||||
|
||||
"ERROR" => BoxStatus.Error,
|
||||
|
||||
_ => BoxStatus.ParsingError
|
||||
};
|
||||
#endregion
|
||||
}
|
||||
|
||||
enum BoxStatus
|
||||
{
|
||||
//Статусы ДО открытия
|
||||
OK, //Готов к выдаче
|
||||
OKplusCard, //Готов к выдаче + готов выдать по карте
|
||||
Empty, //Пусто, уже выдали
|
||||
Open, //Дверка УЖЕ открыта. Приз предположительно пустой
|
||||
|
||||
//Статусы ПОСЛЕ открытия
|
||||
OPENED, //Всё хорошо, открылся
|
||||
OPEN_ERROR, //Ошибка открытия
|
||||
|
||||
//Служебные статусы
|
||||
ParsingError, //При парсинге что то пошло не так, или пришла непредусмотренная фигня
|
||||
NotFound, //Коробка с требуемым номером не найдена
|
||||
Error //Общая ошибка (например дали команду на открытие пустого ящика или сломан замок)
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ce33b35c8b4042a5b85c0263727d588b
|
||||
timeCreated: 1771575375
|
||||
@ -1,4 +1,4 @@
|
||||
using System.Collections;
|
||||
using System.Collections;
|
||||
using RR.Common.Managers;
|
||||
using RR.Game2D.Core.Managers;
|
||||
using UnityEngine;
|
||||
@ -102,7 +102,7 @@ namespace RR.Common.Controls
|
||||
|
||||
|
||||
const float timeout = 10; //сек
|
||||
ComWrapper.Send("301");//включаем мотор
|
||||
ComSystemManager.Send("301");//включаем мотор
|
||||
float startTime = Time.time;
|
||||
bool success = false;
|
||||
|
||||
@ -121,7 +121,7 @@ namespace RR.Common.Controls
|
||||
}
|
||||
|
||||
//Выключаем мотор
|
||||
ComWrapper.Send("300");
|
||||
ComSystemManager.Send("300");
|
||||
// DebugWindow.Message("Закончил. Успех: " + success);
|
||||
|
||||
_lastPrizeSuccess = success;
|
||||
@ -146,4 +146,4 @@ namespace RR.Common.Controls
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -285,7 +285,7 @@ namespace RR.Common.Managers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Синхронизирует кредиты в ComWrapper с текущим балансом
|
||||
/// Синхронизирует кредиты в ComSystemManager с текущим балансом
|
||||
/// по формуле: кредиты = баланс / стоимость_импульса (округление вниз)
|
||||
/// </summary>
|
||||
private void SyncCreditsWithBalance()
|
||||
@ -297,13 +297,13 @@ namespace RR.Common.Managers
|
||||
}
|
||||
|
||||
int targetCredits = currentBalance / _impulseCost;
|
||||
int currentCredits = ComWrapper.GetCredits();
|
||||
int currentCredits = ComSystemManager.GetCredits();
|
||||
|
||||
if (targetCredits == currentCredits)
|
||||
return; // Кредиты уже синхронизированы
|
||||
|
||||
// Устанавливаем кредиты напрямую через SetCredits
|
||||
ComWrapper.SetCredits(targetCredits);
|
||||
ComSystemManager.SetCredits(targetCredits);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using RR.Common.Controls;
|
||||
using UnityEngine;
|
||||
@ -32,7 +32,7 @@ namespace RR.Game2D.Core.Managers
|
||||
public InactivityManager Inactivity {get; private set;}
|
||||
|
||||
// Plate control for game
|
||||
public ComWrapper Plate { get; private set; }
|
||||
public ComSystemManager Plate { get; private set; }
|
||||
// Light control for game
|
||||
public LightController Light => Plate != null ? Plate.Light : null;
|
||||
|
||||
@ -160,7 +160,7 @@ namespace RR.Game2D.Core.Managers
|
||||
|
||||
// Apply loaded settings (new system applies automatically in Awake)
|
||||
|
||||
if (Plate == null) Plate = globals.GetComponent<ComWrapper>();
|
||||
if (Plate == null) Plate = globals.GetComponent<ComSystemManager>();
|
||||
|
||||
Payment.OnCreditAdded.AddListener(OnCreditAdded);
|
||||
|
||||
@ -175,7 +175,7 @@ namespace RR.Game2D.Core.Managers
|
||||
private void OnDestroy()
|
||||
{
|
||||
if (Instance == this) Instance = null;
|
||||
Plate.StopBlink(false);
|
||||
Plate?.StopBlink(false);
|
||||
}
|
||||
|
||||
private void OnApplicationQuit()
|
||||
|
||||
@ -8,7 +8,7 @@ namespace RR.Common.Managers
|
||||
public class GlobalsManager : MonoBehaviour
|
||||
{
|
||||
private SettingsManager _settingsManager;
|
||||
private ComWrapper _comWrapper;
|
||||
private ComSystemManager _comSystemManager;
|
||||
private In _in;
|
||||
private PrizeSystem _prizeSystem;
|
||||
private PrizeSystemManager _prizeSystemManager;
|
||||
@ -19,26 +19,26 @@ namespace RR.Common.Managers
|
||||
{
|
||||
GameManager.Logger.Log(name ,"Initializing global objects...");
|
||||
_settingsManager = GetComponent<SettingsManager>();
|
||||
_comWrapper = GetComponent<ComWrapper>();
|
||||
_comSystemManager = GetComponent<ComSystemManager>();
|
||||
_in = GetComponent<In>();
|
||||
_prizeSystem = GetComponent<PrizeSystem>();
|
||||
_prizeSystemManager = GetComponent<PrizeSystemManager>();
|
||||
_paymentManager = GetComponent<PaymentManager>();
|
||||
_adapter = GetComponent<ComInputToInputSystemAdapter>();
|
||||
|
||||
if (_settingsManager == null) _settingsManager = gameObject.AddComponent<SettingsManager>();
|
||||
if (_comWrapper == null) gameObject.AddComponent<ComWrapper>();
|
||||
if (_in == null) gameObject.AddComponent<In>();
|
||||
if (_prizeSystem == null) gameObject.AddComponent<PrizeSystem>();
|
||||
if (_prizeSystemManager == null) gameObject.AddComponent<PrizeSystemManager>();
|
||||
if (_paymentManager == null) gameObject.AddComponent<PaymentManager>();
|
||||
if (_adapter == null) gameObject.AddComponent<ComInputToInputSystemAdapter>();
|
||||
if (_settingsManager == null) _settingsManager = gameObject.AddComponent<SettingsManager>();
|
||||
if (_comSystemManager == null) _comSystemManager = gameObject.AddComponent<ComSystemManager>();
|
||||
if (_in == null) _in = gameObject.AddComponent<In>();
|
||||
if (_prizeSystem == null) _prizeSystem = gameObject.AddComponent<PrizeSystem>();
|
||||
if (_prizeSystemManager == null) _prizeSystemManager = gameObject.AddComponent<PrizeSystemManager>();
|
||||
if (_paymentManager == null) _paymentManager = gameObject.AddComponent<PaymentManager>();
|
||||
if (_adapter == null) _adapter = gameObject.AddComponent<ComInputToInputSystemAdapter>();
|
||||
|
||||
_settingsManager.Initialize();
|
||||
GameManager.Logger.Log(name, "SettingsManager successfully initialized.");
|
||||
|
||||
_comWrapper.Initialize();
|
||||
GameManager.Logger.Log(name, "ComWrapper successfully initialized.");
|
||||
_comSystemManager.Initialize();
|
||||
GameManager.Logger.Log(name, "ComSystemManager successfully initialized.");
|
||||
|
||||
_in.Initialize();
|
||||
GameManager.Logger.Log(name, "In.cs successfully initialized.");
|
||||
@ -56,4 +56,4 @@ namespace RR.Common.Managers
|
||||
GameManager.Logger.Log(name, "InputAdapter successfully initialized.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,10 +100,10 @@ namespace RR.Common.Managers
|
||||
|
||||
private void SubscribeToInputEvents()
|
||||
{
|
||||
// Подписываемся на события ComWrapper (кнопки автомата)
|
||||
if (ComWrapper.OnInputReceived != null)
|
||||
// Подписываемся на события ComSystemManager (кнопки автомата)
|
||||
if (ComSystemManager.OnInputReceived != null)
|
||||
{
|
||||
ComWrapper.OnInputReceived.AddListener(OnComInput);
|
||||
ComSystemManager.OnInputReceived.AddListener(OnComInput);
|
||||
}
|
||||
|
||||
// InputSystem.onAnyButtonPress - это событие, которое срабатывает каждый раз
|
||||
@ -112,9 +112,9 @@ namespace RR.Common.Managers
|
||||
|
||||
private void UnsubscribeFromInputEvents()
|
||||
{
|
||||
if (ComWrapper.OnInputReceived != null)
|
||||
if (ComSystemManager.OnInputReceived != null)
|
||||
{
|
||||
ComWrapper.OnInputReceived.RemoveListener(OnComInput);
|
||||
ComSystemManager.OnInputReceived.RemoveListener(OnComInput);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ using UnityEngine.InputSystem;
|
||||
namespace RR.Common.Managers
|
||||
{
|
||||
/// <summary>
|
||||
/// Менеджер системы оплаты. Управляет кредитами и интегрируется с ComWrapper.
|
||||
/// Менеджер системы оплаты. Управляет кредитами и интегрируется с ComSystemManager.
|
||||
/// </summary>
|
||||
public class PaymentManager : MonoBehaviour
|
||||
{
|
||||
@ -54,9 +54,9 @@ namespace RR.Common.Managers
|
||||
|
||||
CreditsCount = PlayerPrefs.GetInt("CoinsCount", 0);
|
||||
|
||||
// Подписка на события ComWrapper
|
||||
ComWrapper.OnCreditAdded?.AddListener(HandleCreditAdded);
|
||||
ComWrapper.OnCreditRemoved?.AddListener(HandleCreditRemoved);
|
||||
// Подписка на события ComSystemManager
|
||||
ComSystemManager.OnCreditAdded?.AddListener(HandleCreditAdded);
|
||||
ComSystemManager.OnCreditRemoved?.AddListener(HandleCreditRemoved);
|
||||
|
||||
// Привязка действия добавления кредита (для тестирования)
|
||||
if (addCreditAction != null)
|
||||
@ -83,9 +83,9 @@ namespace RR.Common.Managers
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
// Отписка от событий ComWrapper
|
||||
ComWrapper.OnCreditAdded?.RemoveListener(HandleCreditAdded);
|
||||
ComWrapper.OnCreditRemoved?.RemoveListener(HandleCreditRemoved);
|
||||
// Отписка от событий ComSystemManager
|
||||
ComSystemManager.OnCreditAdded?.RemoveListener(HandleCreditAdded);
|
||||
ComSystemManager.OnCreditRemoved?.RemoveListener(HandleCreditRemoved);
|
||||
|
||||
// Отключение действия добавления кредита
|
||||
if (addCreditAction == null) return;
|
||||
@ -103,7 +103,7 @@ namespace RR.Common.Managers
|
||||
#region Private Methods
|
||||
|
||||
/// <summary>
|
||||
/// Обработчик добавления кредита из ComWrapper
|
||||
/// Обработчик добавления кредита из ComSystemManager
|
||||
/// </summary>
|
||||
/// <param name="newCreditsCount">Новое количество кредитов</param>
|
||||
private void HandleCreditAdded(int newCreditsCount)
|
||||
@ -115,7 +115,7 @@ namespace RR.Common.Managers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Обработчик удаления кредита из ComWrapper
|
||||
/// Обработчик удаления кредита из ComSystemManager
|
||||
/// </summary>
|
||||
/// <param name="newCreditsCount">Новое количество кредитов</param>
|
||||
private void HandleCreditRemoved(int newCreditsCount)
|
||||
@ -155,7 +155,7 @@ namespace RR.Common.Managers
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Выполняет списание платы за запуск игры через ComWrapper.Pay()
|
||||
/// Выполняет списание платы за запуск игры через ComSystemManager.Pay()
|
||||
/// </summary>
|
||||
/// <returns>true если оплата прошла успешно</returns>
|
||||
public bool ProcessPayment(int amount = 1)
|
||||
@ -166,7 +166,7 @@ namespace RR.Common.Managers
|
||||
return true;
|
||||
}
|
||||
|
||||
bool paymentSuccess = ComWrapper.Pay(amount);
|
||||
bool paymentSuccess = ComSystemManager.Pay(amount);
|
||||
|
||||
Debug.Log(paymentSuccess ? "Оплата успешна" : "Оплата не прошла - недостаточно кредитов", this);
|
||||
|
||||
@ -181,7 +181,7 @@ namespace RR.Common.Managers
|
||||
{
|
||||
for (int i = 0; i < amount; i++)
|
||||
{
|
||||
ComWrapper.AddCredit();
|
||||
ComSystemManager.AddCredit();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"com.github-glitchenzo.nugetforunity": "https://github.com/GlitchEnzo/NuGetForUnity.git?path=/src/NuGetForUnity",
|
||||
"com.quaza.unitymcp": "https://github.com/quazaai/UnityMCPIntegration.git",
|
||||
"com.unity.ai.inference": "2.5.0",
|
||||
"com.unity.cinemachine": "3.1.5",
|
||||
"com.unity.collab-proxy": "2.11.3",
|
||||
|
||||
@ -7,6 +7,13 @@
|
||||
"dependencies": {},
|
||||
"hash": "34432703a177c97631a4eb1d908f086037c67969"
|
||||
},
|
||||
"com.quaza.unitymcp": {
|
||||
"version": "https://github.com/quazaai/UnityMCPIntegration.git",
|
||||
"depth": 0,
|
||||
"source": "git",
|
||||
"dependencies": {},
|
||||
"hash": "d1515b326e2fc14b39de5a08534159ba29963c7d"
|
||||
},
|
||||
"com.unity.2d.animation": {
|
||||
"version": "13.0.4",
|
||||
"depth": 1,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user