606 lines
22 KiB
C#
606 lines
22 KiB
C#
using System;
|
||
using System.Collections;
|
||
using System.Text;
|
||
using UnityEngine;
|
||
using UnityEngine.Events;
|
||
|
||
//async
|
||
using System.Threading.Tasks;
|
||
using System.Threading;
|
||
|
||
|
||
|
||
/// <summary>
|
||
/// Основной класс-обертка для работы с COM-портом и периферийной платой.
|
||
/// Управляет всей коммуникацией с аппаратным обеспечением: прием монет, чтение кнопок, управление подсветкой, отправка сигналов.
|
||
/// </summary>
|
||
public class ComWrapper : MonoBehaviour
|
||
{
|
||
//Настраиваем из инспектора
|
||
[SerializeField] bool saveMoneyBetweenSessions;
|
||
[SerializeField] bool enablePrizeChecking = false; // Если хопер с проверкой наличия приза ставим true
|
||
[SerializeField] bool keepAlive = true;
|
||
|
||
public static UnityEvent onPulseRecieved;
|
||
public static UnityEvent<int> onCoinAdded;
|
||
public static UnityEvent<int> onCoinRemoved;
|
||
public static UnityEvent<string> onInputRecieved;
|
||
|
||
#region Приватные поля
|
||
private const string REQUEST_INPUT = "91";
|
||
private const string DEFAULT_INPUT = "INPUT1111111111111111";
|
||
|
||
private static int h;//идентификатор хэндла
|
||
private static byte[] buf = new byte[1024]; //Байт-буфер куда будут приходить данные с порта
|
||
|
||
public static int Balance;
|
||
|
||
bool boardEncrypted = false;
|
||
string encString;
|
||
string confirmString;
|
||
Coroutine keepAliveCoroutine;
|
||
|
||
//async
|
||
private CancellationTokenSource pollCancellationTokenSource;
|
||
private bool isPolling = false;
|
||
|
||
|
||
//сделать запрос инпутов. а если его нет то просим ребут? делаем если по другим факторам не пройдёт.
|
||
|
||
//private string inputString = DEFAULT_INPUT;
|
||
#endregion
|
||
|
||
//===========================start: PUBLIC API===========================//
|
||
|
||
/// <summary>
|
||
/// Инициализирует систему работы с COM-портом.
|
||
/// Открывает соединение, настраивает события и запускает периодические запросы состояния.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// Обязателен к вызову при старте приложения. Выполняет:
|
||
/// - Создание UnityEvent'ов для уведомлений
|
||
/// - Загрузку количества кредитов из PlayerPrefs
|
||
/// - Открытие COM-порта
|
||
/// - Запуск периодического опроса пинов (если включена проверка призов)
|
||
/// </remarks>
|
||
public void Initialize()
|
||
{
|
||
//Создаем ивенты
|
||
onCoinAdded = new UnityEvent<int>();
|
||
onCoinRemoved = new UnityEvent<int>();
|
||
onInputRecieved = new UnityEvent<string>();
|
||
onPulseRecieved = new UnityEvent();
|
||
|
||
//Начальная настройка
|
||
if (!saveMoneyBetweenSessions) PlayerPrefs.SetInt("CoinsCount", 0);
|
||
Balance = PlayerPrefs.GetInt("CoinsCount", 0);
|
||
|
||
onInputRecieved?.AddListener(InputRecievedCather);
|
||
StartCoroutine(SetupBoard());
|
||
|
||
|
||
}
|
||
|
||
/// <summary>
|
||
/// Добавляет один кредит в систему и сохраняет обновленное количество.
|
||
/// Вызывается автоматически событием при получении сигнала "COIN" из COM-порта.
|
||
/// </summary>
|
||
/// <remarks>
|
||
/// Увеличивает счетчик кредитов и вызывает событие onCoinAdded для уведомления подписчиков.
|
||
/// Автоматически сохраняет состояние в PlayerPrefs.
|
||
/// </remarks>
|
||
public static void AddCredit()
|
||
{
|
||
int recieved = PlayerPrefs.GetInt("ValutaPerImpulse", 10);
|
||
|
||
Balance = Balance + recieved;
|
||
PlayerPrefs.SetInt("CoinsCount", Balance);
|
||
|
||
//Счетчики выручки
|
||
PlayerPrefs.SetInt(PrizeSystem.key_starts, PlayerPrefs.GetInt(PrizeSystem.key_starts) + recieved);
|
||
PlayerPrefs.SetInt(PrizeSystem.key_startsNul, PlayerPrefs.GetInt(PrizeSystem.key_startsNul) + recieved);
|
||
|
||
if(SettingsManager.GetString("BaseBotDifficulty", "Фиксированная") == "Повышенная")
|
||
{
|
||
int nowPulses = PlayerPrefs.GetInt("PulsesCounter", 0) + 1;
|
||
PlayerPrefs.SetInt("PulsesCounter", nowPulses);
|
||
}
|
||
|
||
|
||
onPulseRecieved?.Invoke();
|
||
onCoinAdded?.Invoke(Balance);
|
||
}
|
||
|
||
/// <summary>
|
||
/// Отправляет произвольную строковую команду на периферийную плату через COM-порт.
|
||
/// </summary>
|
||
/// <param name="message">Команда для отправки</param>
|
||
/// <remarks>
|
||
/// Используется для отправки пользовательских команд на плату.
|
||
/// Команда должна быть в формате, ожидаемом прошивкой платы.
|
||
/// В случае ошибки отправки выводит предупреждение в DebugWindow.
|
||
/// </remarks>
|
||
public static void Send(string message)
|
||
{
|
||
try
|
||
{
|
||
int w = ComNativeWrapper.Serial_WriteStringUTF8(h, message, 1); //w-количество записаных символов в порт. можно обработать
|
||
//Debug.Log("Wrote: " + w);
|
||
|
||
}
|
||
catch { Debug.LogError("Ошибка отправки строкового сигнала."); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Отправляет команду управления подсветкой на периферийную плату.
|
||
/// </summary>
|
||
/// <param name="lightMode">Режим подсветки из перечисления LightMode</param>
|
||
/// <remarks>
|
||
/// Формирует команду вида "80XXX" где XXX - код режима подсветки.
|
||
/// Для добавления новых режимов необходимо обновить вложенный метод getLightString + добавить режим в перечисление.
|
||
/// </remarks>
|
||
public static void Send(LightMode lightMode)
|
||
{
|
||
try
|
||
{
|
||
string signal = LightUtility.GetLightString(lightMode);
|
||
Send(signal);
|
||
}
|
||
catch { Debug.LogError("Ошибка отправки светового сигнала."); }
|
||
}
|
||
|
||
/// <summary>
|
||
/// Выполняет списание платы за запуск игры.
|
||
/// Проверяет наличие доступных кредитов или режим бесплатной игры.
|
||
/// При успешном списании увеличивает счетчики запусков.
|
||
/// </summary>
|
||
/// <returns>
|
||
/// true - если списание прошло успешно (бесплатный режим или есть кредиты),
|
||
/// false - если кредитов недостаточно и платный режим
|
||
/// </returns>
|
||
/// <remarks>
|
||
/// В случае успеха:
|
||
/// - Увеличивает общие счетчики запусков (key_starts и key_startsNul)
|
||
/// - При платном режиме уменьшает количество кредитов на 1
|
||
/// - Вызывает событие onCoinRemoved при списании кредита
|
||
/// </remarks>
|
||
public static bool Pay(int amount)
|
||
{
|
||
//блок проверки оплаты
|
||
if (SettingsManager.GetBool("Freeplay", false))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
if (Balance >= amount)
|
||
{
|
||
Balance -= amount;
|
||
PlayerPrefs.SetInt("CoinsCount", Balance);
|
||
|
||
onCoinRemoved?.Invoke(Balance);
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
|
||
|
||
//===========================end: PUBLIC API===========================//
|
||
|
||
|
||
|
||
|
||
//private void Update() => pollPort(); //async пока выключил это
|
||
private void OnDisable()
|
||
{
|
||
StopAsyncPolling();//async
|
||
ClosePort();
|
||
}
|
||
private void OnApplicationQuit()
|
||
{
|
||
StopAsyncPolling();//async
|
||
ClosePort();
|
||
}
|
||
|
||
private void StopAsyncPolling()
|
||
{
|
||
pollCancellationTokenSource?.Cancel();
|
||
pollCancellationTokenSource?.Dispose();
|
||
pollCancellationTokenSource = null;
|
||
}
|
||
private async void StartAsyncPolling()
|
||
{
|
||
try
|
||
{
|
||
Logger.Instance.Log("Запуск асинхронного опроса порта");
|
||
|
||
while (pollCancellationTokenSource != null &&
|
||
!pollCancellationTokenSource.Token.IsCancellationRequested)
|
||
{
|
||
await Task.Delay(10, pollCancellationTokenSource.Token);
|
||
|
||
string comInput = await ReadBufferLineAsync(pollCancellationTokenSource.Token);
|
||
if (!string.IsNullOrEmpty(comInput))
|
||
{
|
||
// Прямой вызов - мы уже в главном потоке после await
|
||
ProcessComInputImmediate(comInput);
|
||
}
|
||
}
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
Logger.Instance.Log("Асинхронный опрос порта остановлен");
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Logger.Instance.Log($"Ошибка асинхронного опроса: {e.Message}");
|
||
}
|
||
}
|
||
private void ProcessComInputImmediate(string comInput)
|
||
{
|
||
Debug.LogError("COM: " + comInput);
|
||
|
||
if (comInput == "COIN") AddCredit();
|
||
if (comInput.StartsWith("INPUT")) onInputRecieved?.Invoke(comInput.Substring(5));
|
||
if (comInput.StartsWith("ENC:")) encString = comInput;
|
||
if (comInput.StartsWith("ACTIVATED:")) confirmString = comInput;
|
||
}
|
||
private IEnumerator KeepPortAliveProcess()
|
||
{
|
||
|
||
Logger.Instance.Log("Подключаю проверку порта.");
|
||
|
||
while (true)
|
||
{
|
||
requestInputs();
|
||
yield return new WaitForSeconds(2f);
|
||
|
||
//нет инпута
|
||
if (GetInputToggle() == 0)
|
||
{
|
||
Logger.Instance.Log("Плата отвалилась. Запускаю переподключение.");
|
||
initPort();
|
||
}
|
||
//есть инпут
|
||
else
|
||
{
|
||
ToggleInputRecieved(0);
|
||
}
|
||
|
||
yield return new WaitForSeconds(10f);
|
||
}
|
||
|
||
}
|
||
|
||
void ToggleInputRecieved(int value) => PlayerPrefs.SetInt("inputToggle", value);
|
||
int GetInputToggle() => PlayerPrefs.GetInt("inputToggle", 0);
|
||
|
||
void InputRecievedCather(string input)
|
||
{
|
||
if (!string.IsNullOrEmpty(input))
|
||
{
|
||
//пришел инпут
|
||
ToggleInputRecieved(1);
|
||
}
|
||
|
||
}
|
||
|
||
#region Приватные методы
|
||
//Ну тут по дебаг логам все должно быть понятно
|
||
//Если интересно, то h - это хэндл. Что то на плюсовом, типа ID соединения или чтото. Если 0 то нету соед. если меньше то ошибка, больше 0 - норм.
|
||
private void initPort()
|
||
{
|
||
Logger.Instance.Log("Инициализация порта");
|
||
ComNativeWrapper.Serial_CloseAll();
|
||
//ComNativeWrapper.Serial_Close(h);
|
||
|
||
var ports = ComNativeWrapper.ListPorts();
|
||
Debug.Log("Ports: " + string.Join(", ", ports));
|
||
if (ports.Length == 0) { Debug.LogError("Нет портов, завершаю"); return; }
|
||
|
||
string port = ports[^1];
|
||
Logger.Instance.Log("Открываю порт: " + port);
|
||
|
||
|
||
//вот эти настройки лучше не ковырять, но если очень хочется можно поменять readTimeoutMs и writeTimeoutMs
|
||
//Если read поставить слишком маленьким оно проебует кредиты когда они идут большой пачкой например при оплате через вендисту.
|
||
//Там в порт спамит "COIN" в больших количествах и процентов 10 из этих строк не успевают прочитаться при таймауте 1мс, соотв. монеты проебуются.
|
||
//Слишком большой таймаут будет тормозить игру потому что оно все выполняется синхронно и каждый кадр забирает столько мс сколько в переменной указано.
|
||
h = ComNativeWrapper.Serial_Open(port, 115200, 8, 0, 1, 4, 10);
|
||
|
||
|
||
Logger.Instance.Log($"handle: {h}");
|
||
if (h <= 0)
|
||
{
|
||
Logger.Instance.Log("Open failed: " + ComNativeWrapper.GetLastError());
|
||
return;
|
||
}
|
||
else
|
||
{
|
||
Logger.Instance.Log($"Открыл порт: {port}");
|
||
}
|
||
}
|
||
|
||
private static void ClosePort()
|
||
{
|
||
Logger.Instance.Log($"Отпускаю порт handle:{h}");
|
||
if (h > 0)
|
||
{
|
||
ComNativeWrapper.Serial_Close(h);
|
||
h = 0;
|
||
}
|
||
}
|
||
|
||
|
||
//Тут чтение, не трогаем(опционально)
|
||
private static string ReadBufferLine()
|
||
{
|
||
|
||
int read = ComNativeWrapper.Serial_ReadLineUTF8_bytes(h, buf, buf.Length);
|
||
if (read > 0)
|
||
{
|
||
var s = Encoding.UTF8.GetString(buf, 0, read);
|
||
//DebugWindow.Message($"ReadLine len={read} data=[{s}]");
|
||
return s;
|
||
}
|
||
else if (read < 0)
|
||
{
|
||
//DebugWindow.Message($"ReadLine err={read} :: {ComNativeWrapper.GetLastError()}");
|
||
}
|
||
return "";
|
||
}
|
||
|
||
//async
|
||
private static async Task<string> ReadBufferLineAsync(CancellationToken cancellationToken = default)
|
||
{
|
||
try
|
||
{
|
||
return await Task.Run(() =>
|
||
{
|
||
// Проверяем отмену внутри Task.Run
|
||
if (cancellationToken.IsCancellationRequested)
|
||
return string.Empty;
|
||
|
||
int read = ComNativeWrapper.Serial_ReadLineUTF8_bytes(h, buf, buf.Length);
|
||
if (read > 0)
|
||
{
|
||
return Encoding.UTF8.GetString(buf, 0, read);
|
||
}
|
||
return string.Empty;
|
||
}, cancellationToken);
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
Logger.Instance.Log("Чтение порта отменено");
|
||
return string.Empty;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Logger.Instance.Log($"Ошибка асинхронного чтения: {e.Message}");
|
||
return string.Empty;
|
||
}
|
||
}
|
||
|
||
|
||
//просим прислать состояние цифровых входов. Приходит что то вроде "INPUT111111101"
|
||
private void requestInputs() => Send(REQUEST_INPUT);
|
||
|
||
private void pollPort()
|
||
{
|
||
//Тут все просто, читаем что нам приходит с порта и сравниваем.
|
||
string comInput = ReadBufferLine();
|
||
if (comInput != "")
|
||
{
|
||
Debug.Log("COM: " + comInput);
|
||
|
||
if (comInput == "COIN") AddCredit();
|
||
if (comInput.StartsWith("INPUT")) onInputRecieved?.Invoke(comInput.Substring(5)); //инвокаю сразу без префикса "INPUT"
|
||
if (comInput.StartsWith("ENC:")) encString = comInput;
|
||
if (comInput.StartsWith("ACTIVATED:")) confirmString = comInput;
|
||
}
|
||
}
|
||
|
||
|
||
|
||
private IEnumerator SetupBoard()
|
||
{
|
||
if (keepAliveCoroutine != null) StopCoroutine(keepAliveCoroutine);
|
||
|
||
initPort();
|
||
boardEncrypted = true; //предполагаем что зашифрована
|
||
|
||
//пробуем получить инпуты. Вдруг плата осталась расшифрована
|
||
ToggleInputRecieved(0);
|
||
requestInputs();
|
||
float timer = 1;
|
||
while (GetInputToggle() == 0 && timer > 0)
|
||
{
|
||
pollPort();
|
||
timer -= Time.deltaTime;
|
||
yield return null;
|
||
}
|
||
//Есть инпут. незашифровано
|
||
if (GetInputToggle() == 1)
|
||
{
|
||
boardEncrypted = false;
|
||
}
|
||
|
||
|
||
if (boardEncrypted)
|
||
{
|
||
Logger.Instance.Log("Шифрованная плата");
|
||
//запрос
|
||
encString = "";
|
||
Send("666");
|
||
|
||
//ждем пока придет ENC:
|
||
timer = 1;
|
||
while (string.IsNullOrEmpty(encString) && timer > 0)
|
||
{
|
||
pollPort();
|
||
timer -= Time.deltaTime;
|
||
yield return null;
|
||
}
|
||
//получили ENC: строку или вышли по таймеру
|
||
|
||
//Строка пришла
|
||
if (!string.IsNullOrEmpty(encString))
|
||
{
|
||
Logger.Instance.Log($"Расшифровываю {encString}");
|
||
|
||
string decr = BoardUtility.DecryptSerial(ExtractData(encString));
|
||
|
||
//подтверждение
|
||
confirmString = "";
|
||
Send("777" + decr);
|
||
}
|
||
else
|
||
{
|
||
Logger.Instance.Log($"Зашифрованная строка не пришла"); //отвал игры пускать
|
||
(new GameObject()).AddComponent(typeof(VfxPrefab));
|
||
}
|
||
|
||
|
||
//ждем подтверждение от платы
|
||
timer = 2;
|
||
while (string.IsNullOrEmpty(confirmString) && timer > 0)
|
||
{
|
||
pollPort();
|
||
timer -= Time.deltaTime;
|
||
yield return null;
|
||
}
|
||
if (string.IsNullOrEmpty(confirmString))
|
||
{
|
||
Application.Quit();
|
||
}
|
||
else
|
||
{
|
||
if (confirmString.IndexOf("OK") != -1)
|
||
{
|
||
Logger.Instance.Log("Активировано.");
|
||
}
|
||
else
|
||
{
|
||
Application.Quit();
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
Logger.Instance.Log("НЕшифрованная плата");
|
||
}
|
||
|
||
//async
|
||
//Каждые 5 сек запрашиваем инпуты у платы. Надо потому что при заходе в игру с пинов еще не пришла инфа
|
||
/* if (keepAlive)
|
||
keepAliveCoroutine = StartCoroutine(KeepPortAliveProcess());*/
|
||
|
||
//async
|
||
pollCancellationTokenSource = new CancellationTokenSource();
|
||
StartAsyncPolling();
|
||
StartKeepAliveProcess();
|
||
|
||
|
||
}
|
||
private async void StartKeepAliveProcess()
|
||
{
|
||
if (keepAlive)
|
||
{
|
||
await StartKeepAliveProcessAsync(pollCancellationTokenSource.Token);
|
||
}
|
||
}
|
||
private async Task StartKeepAliveProcessAsync(CancellationToken cancellationToken)
|
||
{
|
||
try
|
||
{
|
||
Logger.Instance.Log("Запуск асинхронной проверки порта");
|
||
|
||
while (!cancellationToken.IsCancellationRequested)
|
||
{
|
||
await SendRequestInputsAsync(cancellationToken);
|
||
await Task.Delay(2000, cancellationToken); // 2 секунды
|
||
|
||
if (!await CheckPortConnectionAsync(cancellationToken))
|
||
{
|
||
Logger.Instance.Log("Плата отвалилась. Запускаю переподключение.");
|
||
await ReinitializePortAsync(cancellationToken);
|
||
}
|
||
else
|
||
{
|
||
ToggleInputRecieved(0);
|
||
}
|
||
|
||
await Task.Delay(10000, cancellationToken); // 10 секунд
|
||
}
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
Logger.Instance.Log("Асинхронная проверка порта остановлена");
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Logger.Instance.Log($"Ошибка проверки порта: {e.Message}");
|
||
}
|
||
}
|
||
private async Task SendRequestInputsAsync(CancellationToken cancellationToken)
|
||
{
|
||
try
|
||
{
|
||
requestInputs();
|
||
await Task.CompletedTask;
|
||
}
|
||
catch (Exception e)
|
||
{
|
||
Logger.Instance.Log($"Ошибка отправки запроса инпутов: {e.Message}");
|
||
}
|
||
}
|
||
|
||
private async Task<bool> CheckPortConnectionAsync(CancellationToken cancellationToken)
|
||
{
|
||
// Простая проверка, не требует Task.Run
|
||
return await Task.FromResult(GetInputToggle() != 0);
|
||
}
|
||
|
||
private async Task ReinitializePortAsync(CancellationToken cancellationToken)
|
||
{
|
||
await Task.Run(() => initPort(), cancellationToken);
|
||
}
|
||
|
||
public static string ExtractData(string input)
|
||
{
|
||
const string encPrefix = "ENC:";
|
||
|
||
int encIndex = input.IndexOf(encPrefix);
|
||
|
||
if (encIndex == -1)
|
||
{
|
||
Logger.Instance.Log($"Неверный формат шифрованной строки {input}");
|
||
throw new ArgumentException("Invalid input format");
|
||
}
|
||
|
||
int dataStart = encIndex + encPrefix.Length;
|
||
|
||
|
||
return input.Substring(4);
|
||
}
|
||
#endregion
|
||
|
||
|
||
|
||
}
|
||
public enum LightMode
|
||
{
|
||
Idle1,
|
||
Idle2,
|
||
Orange,
|
||
Blue,
|
||
Win,
|
||
Lose,
|
||
Prize,
|
||
Toaster,
|
||
Tama,
|
||
Dog,
|
||
Sam,
|
||
Mistress
|
||
} |