Add new metadata files and update input handling for improved COM communication
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
This commit is contained in:
parent
698e02d2c6
commit
37e29ce3ab
@ -322,6 +322,9 @@ MonoBehaviour:
|
||||
highQualityFiltering:
|
||||
m_OverrideState: 1
|
||||
m_Value: 0
|
||||
filter:
|
||||
m_OverrideState: 1
|
||||
m_Value: 0
|
||||
downscale:
|
||||
m_OverrideState: 1
|
||||
m_Value: 0
|
||||
|
||||
@ -175,10 +175,6 @@ MonoBehaviour:
|
||||
actionBindings: []
|
||||
pinBindings:
|
||||
- pinIndex: 0
|
||||
target: 1
|
||||
button: 0
|
||||
dpadDirection: 0
|
||||
- pinIndex: 3
|
||||
target: 1
|
||||
button: 2
|
||||
dpadDirection: 2
|
||||
@ -186,19 +182,23 @@ MonoBehaviour:
|
||||
target: 1
|
||||
button: 3
|
||||
dpadDirection: 3
|
||||
- pinIndex: 2
|
||||
- pinIndex: 4
|
||||
target: 1
|
||||
button: 0
|
||||
dpadDirection: 0
|
||||
- pinIndex: 5
|
||||
target: 1
|
||||
button: 1
|
||||
dpadDirection: 1
|
||||
- pinIndex: 4
|
||||
target: 0
|
||||
button: 11
|
||||
dpadDirection: 0
|
||||
- pinIndex: 10
|
||||
- pinIndex: 2
|
||||
target: 0
|
||||
button: 10
|
||||
dpadDirection: 1
|
||||
- pinIndex: 12
|
||||
- pinIndex: 3
|
||||
target: 0
|
||||
button: 11
|
||||
dpadDirection: 0
|
||||
- pinIndex: 6
|
||||
target: 0
|
||||
button: 4
|
||||
dpadDirection: 1
|
||||
|
||||
@ -4874,7 +4874,6 @@ GameObject:
|
||||
- component: {fileID: 2082762286}
|
||||
- component: {fileID: 2082762288}
|
||||
- component: {fileID: 2082762287}
|
||||
- component: {fileID: 2082762289}
|
||||
m_Layer: 5
|
||||
m_Name: Banner
|
||||
m_TagString: Untagged
|
||||
@ -4941,21 +4940,6 @@ CanvasRenderer:
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2082762285}
|
||||
m_CullTransparentMesh: 1
|
||||
--- !u!114 &2082762289
|
||||
MonoBehaviour:
|
||||
m_ObjectHideFlags: 0
|
||||
m_CorrespondingSourceObject: {fileID: 0}
|
||||
m_PrefabInstance: {fileID: 0}
|
||||
m_PrefabAsset: {fileID: 0}
|
||||
m_GameObject: {fileID: 2082762285}
|
||||
m_Enabled: 1
|
||||
m_EditorHideFlags: 0
|
||||
m_Script: {fileID: 11500000, guid: c815924369a0a64458e3c85a564de8b8, type: 3}
|
||||
m_Name:
|
||||
m_EditorClassIdentifier: Assembly-CSharp::RR.Common.UI.ModalFocusTrap
|
||||
modalRoot: {fileID: 2082762285}
|
||||
firstSelected: {fileID: 1065887711}
|
||||
enforce: 1
|
||||
--- !u!1660057539 &9223372036854775807
|
||||
SceneRoots:
|
||||
m_ObjectHideFlags: 0
|
||||
|
||||
@ -1486,7 +1486,7 @@ GameObject:
|
||||
m_Icon: {fileID: 0}
|
||||
m_NavMeshLayer: 0
|
||||
m_StaticEditorFlags: 0
|
||||
m_IsActive: 1
|
||||
m_IsActive: 0
|
||||
--- !u!224 &254276106
|
||||
RectTransform:
|
||||
m_ObjectHideFlags: 0
|
||||
@ -1502,10 +1502,10 @@ RectTransform:
|
||||
- {fileID: 112784686}
|
||||
m_Father: {fileID: 1042850270}
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 0}
|
||||
m_AnchorMax: {x: 0, y: 0}
|
||||
m_AnchoredPosition: {x: 0, y: 0}
|
||||
m_SizeDelta: {x: 0, y: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 0, y: 1}
|
||||
m_AnchoredPosition: {x: 50, y: -162.5}
|
||||
m_SizeDelta: {x: 400, y: 175}
|
||||
m_Pivot: {x: 0.5, y: 0.5}
|
||||
--- !u!114 &254276107
|
||||
MonoBehaviour:
|
||||
@ -1807,7 +1807,7 @@ MonoBehaviour:
|
||||
m_HandleRect: {fileID: 434895585}
|
||||
m_Direction: 2
|
||||
m_Value: 0
|
||||
m_Size: 1
|
||||
m_Size: 0.9858968
|
||||
m_NumberOfSteps: 0
|
||||
m_OnValueChanged:
|
||||
m_PersistentCalls:
|
||||
@ -11914,7 +11914,7 @@ RectTransform:
|
||||
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
|
||||
m_AnchorMin: {x: 0, y: 1}
|
||||
m_AnchorMax: {x: 1, y: 1}
|
||||
m_AnchoredPosition: {x: 0, y: 0.0009278054}
|
||||
m_AnchoredPosition: {x: 0, y: -0.000037980128}
|
||||
m_SizeDelta: {x: 0, y: 300}
|
||||
m_Pivot: {x: 0, y: 1}
|
||||
--- !u!114 &1792218764
|
||||
|
||||
@ -236,6 +236,72 @@ namespace RR.Common.Controls
|
||||
TryAddActionBinding(PlayerAction.action, BindingTarget.Button, GamepadButton.South, DpadDirection.Up);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Синхронизирует pinBindings с ButtonPinMapping.
|
||||
/// Вызывать из контекстного меню в инспекторе.
|
||||
/// </summary>
|
||||
[ContextMenu("Синхронизировать pinBindings с ButtonPinMapping")]
|
||||
private void SyncPinBindingsWithButtonPinMapping()
|
||||
{
|
||||
pinBindings.Clear();
|
||||
|
||||
// Left
|
||||
pinBindings.Add(new PinBinding
|
||||
{
|
||||
pinIndex = (int)ButtonPinMapping.left1,
|
||||
target = BindingTarget.Dpad,
|
||||
button = GamepadButton.DpadLeft,
|
||||
dpadDirection = DpadDirection.Left
|
||||
});
|
||||
|
||||
// Right
|
||||
pinBindings.Add(new PinBinding
|
||||
{
|
||||
pinIndex = (int)ButtonPinMapping.right1,
|
||||
target = BindingTarget.Dpad,
|
||||
button = GamepadButton.DpadRight,
|
||||
dpadDirection = DpadDirection.Right
|
||||
});
|
||||
|
||||
// Up
|
||||
pinBindings.Add(new PinBinding
|
||||
{
|
||||
pinIndex = (int)ButtonPinMapping.up1,
|
||||
target = BindingTarget.Dpad,
|
||||
button = GamepadButton.DpadUp,
|
||||
dpadDirection = DpadDirection.Up
|
||||
});
|
||||
|
||||
// Down
|
||||
pinBindings.Add(new PinBinding
|
||||
{
|
||||
pinIndex = (int)ButtonPinMapping.down1,
|
||||
target = BindingTarget.Dpad,
|
||||
button = GamepadButton.DpadDown,
|
||||
dpadDirection = DpadDirection.Down
|
||||
});
|
||||
|
||||
// Action
|
||||
pinBindings.Add(new PinBinding
|
||||
{
|
||||
pinIndex = (int)ButtonPinMapping.action1,
|
||||
target = BindingTarget.Button,
|
||||
button = GamepadButton.South,
|
||||
dpadDirection = DpadDirection.Up
|
||||
});
|
||||
|
||||
// Start
|
||||
pinBindings.Add(new PinBinding
|
||||
{
|
||||
pinIndex = (int)ButtonPinMapping.startOnePlayer,
|
||||
target = BindingTarget.Button,
|
||||
button = GamepadButton.Start,
|
||||
dpadDirection = DpadDirection.Up
|
||||
});
|
||||
|
||||
Debug.Log($"[ComInputAdapter] pinBindings синхронизированы с ButtonPinMapping. Всего: {pinBindings.Count}");
|
||||
}
|
||||
|
||||
private void TryAddActionBinding(
|
||||
PlayerAction action,
|
||||
BindingTarget target,
|
||||
|
||||
@ -6,34 +6,36 @@
|
||||
/// </summary>
|
||||
public enum ButtonPinMapping
|
||||
{
|
||||
/// <summary>Кнопка движения вверх для первого игрока</summary>
|
||||
up1 = 0,
|
||||
/// <summary>Кнопка движения влево для первого игрока</summary>
|
||||
left1 = 0,
|
||||
/// <summary>Кнопка движения вправо для первого игрока</summary>
|
||||
right1 = 1,
|
||||
/// <summary>Кнопка движения вверх для первого игрока</summary>
|
||||
up1 = 3,
|
||||
/// <summary>Кнопка движения вниз для первого игрока</summary>
|
||||
down1 = 2,
|
||||
/// <summary>Кнопка движения влево для первого игрока</summary>
|
||||
left1 = 3,
|
||||
down1 = 5,
|
||||
|
||||
/// <summary>Запуск игры для одного игрока</summary>
|
||||
startOnePlayer = 2,
|
||||
/// <summary>Действие/огонь для первого игрока</summary>
|
||||
action1 = 4,
|
||||
|
||||
/// <summary>Кнопка движения вверх для второго игрока</summary>
|
||||
up2 = 5,
|
||||
/// <summary>Кнопка движения вправо для второго игрока</summary>
|
||||
right2 = 6,
|
||||
/// <summary>Кнопка движения вниз для второго игрока</summary>
|
||||
down2 = 7,
|
||||
/// <summary>Кнопка движения влево для второго игрока</summary>
|
||||
left2 = 8,
|
||||
/// <summary>Действие/огонь для второго игрока</summary>
|
||||
action2 = 9,
|
||||
|
||||
/// <summary>Запуск игры для одного игрока</summary>
|
||||
startOnePlayer = 10,
|
||||
/// <summary>Запуск игры для двух игроков</summary>
|
||||
startTwoPlayers = 11,
|
||||
/// <summary>Секретная/служебная кнопка для захода в настройки</summary>
|
||||
secret = 12,
|
||||
secret = 6,
|
||||
|
||||
// НЕИСПОЛЬЗУЕМЫЕ ПИНЫ
|
||||
/// <summary>Кнопка движения влево для второго игрока</summary>
|
||||
left2 = 7,
|
||||
/// <summary>Кнопка движения вправо для второго игрока</summary>
|
||||
right2 = 8,
|
||||
/// <summary>Кнопка движения вверх для второго игрока</summary>
|
||||
up2 = 10,
|
||||
/// <summary>Кнопка движения вниз для второго игрока</summary>
|
||||
down2 = 12,
|
||||
|
||||
/// <summary>Запуск игры для двух игроков</summary>
|
||||
startTwoPlayers = 9,
|
||||
/// <summary>Действие/огонь для второго игрока</summary>
|
||||
action2 = 11,
|
||||
|
||||
/// <summary>Дополнительная/резервная кнопка</summary>
|
||||
extra = 13,
|
||||
|
||||
@ -7,9 +7,9 @@ namespace RR.Common.Controls
|
||||
public class COMInput : InputSource
|
||||
{
|
||||
//Количество = цифровые + 2 датчика.
|
||||
//На момент написания количество цифровых входов 7*N.
|
||||
//На момент написания количество цифровых входов соответствует разрядности платы
|
||||
//Настроить количество пинов под свою настройку платы
|
||||
private const int PIN_COUNT = 16;
|
||||
private const int PIN_COUNT = 26;
|
||||
|
||||
private const char POSITIVE = '0';
|
||||
private const char NEGATIVE = '1';
|
||||
@ -204,7 +204,11 @@ namespace RR.Common.Controls
|
||||
{
|
||||
// Будем принимать в нативном формате by Pasha. 0 - нажато 1 - не нажато (чтобы преобразовывать циклом)
|
||||
// В параметрприходит строка вида "1111011111011111"
|
||||
for (int i = 0; i < input.Length; i++)
|
||||
|
||||
// Ограничиваем количество обрабатываемых символов размером массива
|
||||
int maxIndex = Math.Min(input.Length, PIN_COUNT);
|
||||
|
||||
for (int i = 0; i < maxIndex; i++)
|
||||
{
|
||||
// Получаем новое состояние пина
|
||||
bool newState = input[i] == POSITIVE;
|
||||
@ -227,7 +231,10 @@ namespace RR.Common.Controls
|
||||
buttonState[i] = newState;
|
||||
|
||||
}
|
||||
if (input[^1] == POSITIVE) PlayerPrefs.SetInt("PrizeGivenBuffer", 1); //пишем в буфер чтобы между кадрами и порядком вызова не проебать
|
||||
|
||||
// Проверяем последний пин только если он в пределах массива
|
||||
if (input.Length > 0 && input.Length <= PIN_COUNT && input[^1] == POSITIVE)
|
||||
PlayerPrefs.SetInt("PrizeGivenBuffer", 1); //пишем в буфер чтобы между кадрами и порядком вызова не проебать
|
||||
|
||||
// DebugWindow.Message("Инпуты: " + string.Join("", buttonState));//можно вырезать
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
using System.Collections;
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using RR.Game2D.Core.Managers;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
@ -21,33 +23,48 @@ namespace RR.Common.Controls
|
||||
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";
|
||||
private const string DEFAULT_INPUT = "INPUT1111111111111111";
|
||||
private const string LIGHTSIGNAL_PREFIX = "80";
|
||||
|
||||
private static int h; // идентификатор хэндла
|
||||
private static byte[] buf = new byte[1024]; // Байт-буфер куда будут приходить данные с порта
|
||||
private static readonly object bufferLock = new object(); // Блокировка для синхронизации доступа к COM-порту
|
||||
|
||||
private static int CreditsCount;
|
||||
|
||||
private bool boardEncrypted = false;
|
||||
// private string inputString = DEFAULT_INPUT;
|
||||
// Асинхронная работа
|
||||
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 бит по умолчанию
|
||||
#endregion
|
||||
|
||||
//===========================start: PUBLIC API===========================//
|
||||
|
||||
/// <summary>
|
||||
/// Инициализирует систему работы с COM-портом.
|
||||
/// Открывает соединение, настраивает события и запускает периодические запросы состояния.
|
||||
/// Открывает соединение, настраивает события и запускает асинхронные запросы состояния.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Обязателен к вызову при старте приложения. Выполняет:
|
||||
/// - Создание UnityEvent'ов для уведомлений
|
||||
/// - Загрузку количества кредитов из PlayerPrefs
|
||||
/// - Открытие COM-порта
|
||||
/// - Запуск периодического опроса пинов (если включена проверка призов)
|
||||
/// - Запуск асинхронного опроса пинов (если включена проверка призов)
|
||||
/// </remarks>
|
||||
public void Initialize()
|
||||
{
|
||||
@ -60,7 +77,19 @@ namespace RR.Common.Controls
|
||||
if (!saveMoneyBetweenSessions) PlayerPrefs.SetInt("CoinsCount", 0);
|
||||
CreditsCount = PlayerPrefs.GetInt("CoinsCount", 0);
|
||||
|
||||
StartCoroutine(SetupBoard());
|
||||
initPort();
|
||||
|
||||
// Запускаем асинхронный опрос порта
|
||||
pollCancellationTokenSource = new CancellationTokenSource();
|
||||
StartAsyncPolling();
|
||||
|
||||
// Запускаем keep-alive механизм если включен
|
||||
if (keepAlive || enablePrizeChecking)
|
||||
{
|
||||
StartKeepAliveProcess();
|
||||
}
|
||||
|
||||
OnInitialize?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -76,6 +105,9 @@ namespace RR.Common.Controls
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -85,19 +117,22 @@ namespace RR.Common.Controls
|
||||
/// <remarks>
|
||||
/// Используется для отправки пользовательских команд на плату.
|
||||
/// Команда должна быть в формате, ожидаемом прошивкой платы.
|
||||
/// В случае ошибки отправки выводит предупреждение в DebugWindow.
|
||||
/// </remarks>
|
||||
public static void Send(string message)
|
||||
{
|
||||
try
|
||||
{
|
||||
int w = ComNativeWrapper.Serial_WriteStringUTF8(h, message, 1); //w-количество записаных символов в порт. можно обработать
|
||||
//GameManager.Logger.Log("Wrote: " + w);
|
||||
|
||||
if (h <= 0)
|
||||
{
|
||||
GameManager.Logger.Log("ComWrapper: Попытка отправки при закрытом порте");
|
||||
return;
|
||||
}
|
||||
|
||||
ComNativeWrapper.Serial_WriteStringUTF8(h, message, 1);
|
||||
}
|
||||
catch
|
||||
catch (Exception e)
|
||||
{
|
||||
// DebugWindow.Message("Ошибка отправки строкового сигнала.");
|
||||
GameManager.Logger.Log($"Ошибка отправки сигнала: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -185,52 +220,151 @@ namespace RR.Common.Controls
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получаем текущий баланс кредитов
|
||||
/// </summary>
|
||||
public static int GetCredits() => CreditsCount;
|
||||
|
||||
//===========================end: PUBLIC API===========================//
|
||||
|
||||
private void Update() => pollPort();
|
||||
private void OnDisable() => ClosePort();
|
||||
private void OnApplicationQuit() => ClosePort();
|
||||
|
||||
private IEnumerator KeepPortAliveProcess()
|
||||
private void Update()
|
||||
{
|
||||
OnInputReceived?.AddListener(InputReceivedCather);
|
||||
GameManager.Logger.Log("Подключаю проверку порта.");
|
||||
|
||||
while (true)
|
||||
// Обрабатываем COIN из очереди в главном потоке
|
||||
int coinsProcessed = 0;
|
||||
|
||||
while (coinQueue.TryDequeue(out _))
|
||||
{
|
||||
requestInputs();
|
||||
yield return new WaitForSeconds(5f);
|
||||
AddCredit();
|
||||
coinsProcessed++;
|
||||
}
|
||||
|
||||
if (coinsProcessed > 0)
|
||||
{
|
||||
GameManager.Logger.Log($"Обработано монет: {coinsProcessed}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
StopAsyncPolling();
|
||||
ClosePort();
|
||||
}
|
||||
|
||||
private void OnApplicationQuit()
|
||||
{
|
||||
StopAsyncPolling();
|
||||
ClosePort();
|
||||
}
|
||||
|
||||
//нет инпута
|
||||
if (GetInputToggle() == 0)
|
||||
private void StopAsyncPolling()
|
||||
{
|
||||
pollCancellationTokenSource?.Cancel();
|
||||
pollCancellationTokenSource?.Dispose();
|
||||
pollCancellationTokenSource = null;
|
||||
}
|
||||
|
||||
private async void StartAsyncPolling()
|
||||
{
|
||||
try
|
||||
{
|
||||
GameManager.Logger.Log("Запуск асинхронного опроса порта");
|
||||
|
||||
while (pollCancellationTokenSource != null &&
|
||||
!pollCancellationTokenSource.Token.IsCancellationRequested)
|
||||
{
|
||||
GameManager.Logger.Log("Плата отвалилась. Запускаю переподключение.");
|
||||
initPort();
|
||||
|
||||
await Task.Delay(10, pollCancellationTokenSource.Token); // Опрос каждые 10ms
|
||||
|
||||
string comInput = await ReadBufferLineAsync(pollCancellationTokenSource.Token);
|
||||
if (!string.IsNullOrEmpty(comInput))
|
||||
{
|
||||
ProcessComInputImmediate(comInput);
|
||||
}
|
||||
}
|
||||
//есть инпут
|
||||
else
|
||||
{
|
||||
ToggleInputReceived(0);
|
||||
}
|
||||
|
||||
yield return new WaitForSeconds(5);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
GameManager.Logger.Log("Асинхронный опрос порта остановлен");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
GameManager.Logger.Log($"Ошибка асинхронного опроса: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void ToggleInputReceived(int value) => PlayerPrefs.SetInt("inputToggle", value);
|
||||
private static int GetInputToggle() => PlayerPrefs.GetInt("inputToggle", 0);
|
||||
|
||||
private static void InputReceivedCather(string input)
|
||||
private void ProcessComInputImmediate(string comInput)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(input))
|
||||
GameManager.Logger.Log("COM: " + comInput);
|
||||
|
||||
// INPUT обработка
|
||||
if (comInput.StartsWith("INPUT"))
|
||||
{
|
||||
//пришел инпут
|
||||
ToggleInputReceived(1);
|
||||
inputToggle = 1; // Обновляем флаг для keep-alive
|
||||
|
||||
// Проверяем, что после "INPUT" есть данные
|
||||
if (comInput.Length <= 5)
|
||||
{
|
||||
GameManager.Logger.Log("INPUT получен, но без данных");
|
||||
return;
|
||||
}
|
||||
|
||||
string rawData = comInput.Substring(5);
|
||||
|
||||
// Пр<D09F><D180>веряем, что rawData не пустой
|
||||
if (string.IsNullOrWhiteSpace(rawData))
|
||||
{
|
||||
GameManager.Logger.Log("INPUT получен с пустым значением");
|
||||
return;
|
||||
}
|
||||
|
||||
// Конвертируем десятичное число в двоичную строку
|
||||
string binaryValue = DecimalToBinary(rawData);
|
||||
|
||||
// Обновляем кэш последнего состояния
|
||||
lastInputBits = binaryValue;
|
||||
|
||||
GameManager.Logger.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);
|
||||
}
|
||||
|
||||
GameManager.Logger.Log($"Не удалось распарсить значение INPUT: {decimalString}");
|
||||
return "11111111111111111111111111"; // 26 единиц по умолчанию
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
GameManager.Logger.Log($"Ошибка преобразования в двоичную систему: {e.Message}");
|
||||
return "11111111111111111111111111"; // 26 единиц по умолчанию
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Получает пос<D0BE><D181>еднее кэшированное состояние INPUT.
|
||||
/// Полезно когда нужно узнать состояние кнопок без ожидания нового события.
|
||||
/// </summary>
|
||||
/// <returns>Строка с двоичным представлением состояния кнопок</returns>
|
||||
public static string GetLastInputState() => lastInputBits;
|
||||
|
||||
#region Приватные методы
|
||||
//Ну тут по дебаг логам все должно быть понятно
|
||||
@ -264,6 +398,51 @@ namespace RR.Common.Controls
|
||||
{
|
||||
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()
|
||||
@ -277,52 +456,151 @@ namespace RR.Common.Controls
|
||||
}
|
||||
|
||||
|
||||
//Тут чтение, не трогаем(опционально)
|
||||
//Чтение с порта
|
||||
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 read = ComNativeWrapper.Serial_ReadLineUTF8_bytes(h, buf, buf.Length);
|
||||
if (read > 0)
|
||||
{
|
||||
string s = Encoding.UTF8.GetString(buf, 0, read);
|
||||
//GameManager.Logger.Log($"ReadLine len={read} data=[{s}]");
|
||||
return s;
|
||||
// Отправляем подтверждение
|
||||
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 "";
|
||||
}
|
||||
}
|
||||
else if (read < 0)
|
||||
{
|
||||
GameManager.Logger.Log("ComWrapper", $"ReadLine err={read} :: {ComNativeWrapper.GetLastError()}");
|
||||
}
|
||||
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);
|
||||
|
||||
private static void pollPort()
|
||||
/// <summary>
|
||||
/// Асинхронный keep-alive процесс для проверки соединения с платой
|
||||
/// </summary>
|
||||
private async void StartKeepAliveProcess()
|
||||
{
|
||||
//Тут все просто, читаем что нам приходит с порта и сравниваем.
|
||||
string comInput = ReadBufferLine();
|
||||
if (comInput != "")
|
||||
try
|
||||
{
|
||||
// GameManager.Logger.Log("ComWrapper", "COM: " + comInput);
|
||||
|
||||
if (comInput == "COIN") AddCredit();
|
||||
if (comInput.StartsWith("INPUT")) OnInputReceived?.Invoke(comInput.Substring(5)); //инвокаю сразу без префикса "INPUT"
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerator SetupBoard()
|
||||
{
|
||||
initPort();
|
||||
requestInputs();
|
||||
//проверяем вдруг не зашифрованная версия
|
||||
|
||||
|
||||
|
||||
|
||||
//Каждые 5 сек запрашиваем инпуты у платы. Надо потому что при заходе в игру с пинов еще не пришла инфа
|
||||
if (keepAlive) StartCoroutine(KeepPortAliveProcess());
|
||||
|
||||
yield return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
@ -342,4 +620,5 @@ namespace RR.Common.Controls
|
||||
Sam,
|
||||
Mistress
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
8
Assets/_Recovery.meta
Normal file
8
Assets/_Recovery.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3c1afb5231ad35646a78a949f3a90ca7
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
44861
Assets/_Recovery/0.unity
Normal file
44861
Assets/_Recovery/0.unity
Normal file
File diff suppressed because it is too large
Load Diff
7
Assets/_Recovery/0.unity.meta
Normal file
7
Assets/_Recovery/0.unity.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 92bdad2e73fe34a4eab7de385fd6c412
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Loading…
x
Reference in New Issue
Block a user