using System.Text;
using UnityEngine;
using UnityEngine.Events;
///
/// Основной класс-обертка для работы с COM-портом и периферийной платой.
/// Управляет всей коммуникацией с аппаратным обеспечением: прием монет, чтение кнопок, управление подсветкой, отправка сигналов.
///
public class ComWrapper : MonoBehaviour
{
//Настраиваем из инспектора
[SerializeField] bool saveMoneyBetweenSessions;
[SerializeField] bool enablePrizeChecking = false; // Если хопер с проверкой наличия приза ставим true
public static UnityEvent onCoinAdded;
public static UnityEvent onCoinRemoved;
public static UnityEvent onInputRecieved;
public static System.Action OnInitialize;
#region Приватные поля
private const string REQUEST_INPUT = "91";
private const string LIGHTSIGNAL_PREFIX = "80";
private static int handle1; //идентификатор хэндла платы 1
private static int handle2; //идентификатор хэндла платы 2
private static byte[] buf = new byte[1024]; //Байт-буфер куда будут приходить данные с порта
// StringBuilder для эффективного формирования строки INPUT без лишних аллокаций
private static System.Text.StringBuilder inputBuilder = new System.Text.StringBuilder(32);
// Кэш последних состояний INPUT для каждой платы (16 бит от каждой платы)
// ВАЖНО: 0 = кнопка нажата, 1 = кнопка отпущена, поэтому по умолчанию все единицы
// Каждая плата присылает 16 бит, но мы берём:
// - от платы 1: первые 8 бит (биты 0-7)
// - от платы 2: последние 8 бит (биты 8-15)
private static string lastInputBits1 = "1111111111111111"; // последние 16 бит от платы 1 (все отпущены)
private static string lastInputBits2 = "1111111111111111"; // последние 16 бит от платы 2 (все отпущены)
private static int CreditsPlayer1;
private static int CreditsPlayer2;
#endregion
//===========================start: PUBLIC API===========================//
///
/// Инициализирует систему работы с COM-портом.
/// Открывает соединение, настраивает события и запускает периодические запросы состояния.
///
///
/// Обязателен к вызову при старте приложения. Выполняет:
/// - Создание UnityEvent'ов для уведомлений
/// - Загрузку количества кредитов из PlayerPrefs
/// - Открытие COM-порта
/// - Запуск периодического опроса пинов (если включена проверка призов)
///
public void Initialize()
{
//Создаем ивенты
onCoinAdded = new UnityEvent();
onCoinRemoved = new UnityEvent();
onInputRecieved = new();
//Начальная настройка
if (!saveMoneyBetweenSessions)
{
PlayerPrefs.SetInt("Player1_Coins", 0);
PlayerPrefs.SetInt("Player2_Coins", 0);
}
CreditsPlayer1 = PlayerPrefs.GetInt("Player1_Coins", 0);
CreditsPlayer2 = PlayerPrefs.GetInt("Player2_Coins", 0);
initPort(); //вот это важный метод который собственно порт открывает.
//Каждые 5 сек запрашиваем инпуты у платы. Надо потому что при заходе в игру с пинов еще не пришла инфа
if (enablePrizeChecking) InvokeRepeating("requestInputs", 0, 5);
OnInitialize.Invoke();
}
///
/// Добавляет один кредит в систему и сохраняет обновленное количество.
/// Вызывается автоматически событием при получении сигнала "COIN" из COM-порта.
///
///
/// Увеличивает счетчик кредитов и вызывает событие onCoinAdded для уведомления подписчиков.
/// Автоматически сохраняет состояние в PlayerPrefs.
///
public static void AddCredit(int coins = 1, int playerId = 1)
{
if (playerId == 1)
{
CreditsPlayer1 += coins;
PlayerPrefs.SetInt("Player1_Coins", CreditsPlayer1);
onCoinAdded?.Invoke(playerId, CreditsPlayer1); // передаём номер игрока!
}
else if (playerId == 2)
{
CreditsPlayer2 += coins;
PlayerPrefs.SetInt("Player2_Coins", CreditsPlayer2);
onCoinAdded?.Invoke(playerId, CreditsPlayer2); // передаём номер игрока!
}
PlayerPrefs.SetInt(PrizeSystem.key_starts, PlayerPrefs.GetInt(PrizeSystem.key_starts) + 1);
PlayerPrefs.SetInt(PrizeSystem.key_startsNul, PlayerPrefs.GetInt(PrizeSystem.key_startsNul) + 1);
}
///
/// Отправляет произвольную строковую команду на периферийную плату через COM-порт.
///
/// Команда для отправки
///
/// Используется для отправки пользовательских команд на плату.
/// Команда должна быть в формате, ожидаемом прошивкой платы.
/// В случае ошибки отправки выводит предупреждение в DebugWindow.
///
public static void Send(string message)
{
try
{
int write1 = ComNativeWrapper.Serial_WriteStringUTF8(handle1, message, 1); //w-количество записаных символов в порт. можно обработать
int write2 = ComNativeWrapper.Serial_WriteStringUTF8(handle2, message, 1); //w-количество записаных символов в порт. можно обработать
//Debug.Log("Wrote: " + w);
}
catch { Debug.Log("Ошибка отправки строкового сигнала."); }
}
///
/// Отправляет команду управления подсветкой на периферийную плату.
///
/// Режим подсветки из перечисления LightMode
///
/// Формирует команду вида "80XXX" где XXX - код режима подсветки.
/// Для добавления новых режимов необходимо обновить вложенный метод getLightString + добавить режим в перечисление.
///
public static void Send(LightMode lightMode)
{
try
{
string signal = LIGHTSIGNAL_PREFIX + getLightString(lightMode);
Send(signal);
//ВОТЭТОТ МЕТОД ПОД СВОИ ПОДСВЕТКИ НАДО ПОДГОНЯТЬ
string getLightString(LightMode lightMode) =>
lightMode switch
{
LightMode.Idle => "106",
LightMode.Orange => "107",
LightMode.Blue => "108",
LightMode.Win => "109",
LightMode.Lose => "110",
LightMode.Prize => "111",
_ => "106"
};
}
catch { Debug.Log("Ошибка отправки светового сигнала."); }
}
///
/// Выполняет списание платы за запуск игры.
/// Проверяет наличие доступных кредитов или режим бесплатной игры.
/// При успешном списании увеличивает счетчики запусков.
///
///
/// true - если списание прошло успешно (бесплатный режим или есть кредиты),
/// false - если кредитов недостаточно и платный режим
///
///
/// В случае успеха:
/// - Увеличивает общие счетчики запусков (key_starts и key_startsNul)
/// - При платном режиме уменьшает количество кредитов на 1
/// - Вызывает событие onCoinRemoved при списании кредита
///
public static bool Pay(int coins = 1, int playerId = 1)
{
if (SettingsManager.instance.GetSettingValue("Бесплатная игра") == "Вкл") return true;
ref int credits = ref (playerId == 1 ? ref CreditsPlayer1 : ref CreditsPlayer2);
string key = playerId == 1 ? "Player1_Coins" : "Player2_Coins";
if (credits > 0)
{
credits -= coins;
PlayerPrefs.SetInt(key, credits);
onCoinRemoved?.Invoke(playerId, credits);
return true;
}
return false;
}
///
/// Получаем баланс игрока по id
///
///
public static int GetPlayerCredits(int playerId = 1)
{
return playerId == 1 ? CreditsPlayer1 : CreditsPlayer2;
}
//===========================end: PUBLIC API===========================//
private void Update() => pollPort();
private void OnDisable() => ClosePort();
private void OnApplicationQuit() => ClosePort();
#region Приватные методы
//Ну тут по дебаг логам все должно быть понятно
//Если интересно, то h - это хэндл. Что то на плюсовом, типа ID соединения или чтото. Если 0 то нету соед. если меньше то ошибка, больше 0 - норм.
private void initPort()
{
Debug.Log("Старт программы");
ComNativeWrapper.Serial_CloseAll();
var ports = ComNativeWrapper.ListPorts();
Debug.Log("Ports: " + string.Join(", ", ports));
if (ports.Length == 0) { Debug.Log("Нет портов, завершаю"); return; }
// НОВАЯ ПРОВЕРКА: нужно минимум 2 порта
if (ports.Length < 2)
{
Debug.Log($"ОШИБКА: Найден только {ports.Length} порт, а нужно минимум 2 для работы с двумя платами!");
return;
}
string port1 = ports[^1];
string port2 = ports[^2];
Debug.Log("Открываю первый с конца порт: " + port1);
Debug.Log("Открываю второй с конца порт: " + port2);
//вот эти настройки лучше не ковырять, но если очень хочется можно поменять readTimeoutMs и writeTimeoutMs
//Если read поставить слишком маленьким оно проебует кредиты когда они идут большой пачкой например при оплате через вендисту.
//Там в порт спамит "COIN" в больших количествах и процентов 10 из этих строк не успевают прочитаться при таймауте 1мс, соотв. монеты проебуются.
//Слишком большой таймаут будет тормозить игру потому что оно все выполняется синхронно и каждый кадр забирает столько мс сколько в переменной указано.
handle1 = ComNativeWrapper.Serial_Open(port1, 115200, 8, 0, 1, 5, 10);
handle2 = ComNativeWrapper.Serial_Open(port2, 115200, 8, 0, 1, 5, 10);
Debug.Log($"handle: {handle1}");
if (handle1 <= 0)
{
Debug.Log("Open failed: " + ComNativeWrapper.GetLastError());
return;
}
Debug.Log($"handle: {handle2}");
if (handle2 <= 0)
{
Debug.Log("Open failed: " + ComNativeWrapper.GetLastError());
return;
}
}
private static void ClosePort()
{
if (handle1 > 0)
{
ComNativeWrapper.Serial_Close(handle1);
handle1 = 0;
}
if (handle2 > 0)
{
ComNativeWrapper.Serial_Close(handle2);
handle2 = 0;
}
}
//Чтение с ДВУХ плат и объединение битов INPUT с использованием кэша
//COIN обрабатывается сразу, чтобы не потерять монеты с обеих плат
private static string ReadBufferLine()
{
string result1 = "";
string result2 = "";
bool hasNewInput1 = false;
bool hasNewInput2 = false;
// Читаем с первой платы (биты 0-7)
int read1 = ComNativeWrapper.Serial_ReadLineUTF8_bytes(handle1, buf, buf.Length);
if (read1 > 0)
{
result1 = Encoding.UTF8.GetString(buf, 0, read1);
// Обрабатываем COIN сразу, чтобы не потерять
if (result1 == "COIN")
{
AddCredit(1, 1); // 1 монета, игрок 1
Debug.Log("COM1: COIN (Игрок 1)");
}
// Если пришёл INPUT - обновляем кэш
else if (result1.StartsWith("INPUT"))
{
lastInputBits1 = result1.Substring(5); // сохраняем биты 0-7
hasNewInput1 = true;
}
}
// Читаем со второй платы (биты 8-15)
int read2 = ComNativeWrapper.Serial_ReadLineUTF8_bytes(handle2, buf, buf.Length);
if (read2 > 0)
{
result2 = Encoding.UTF8.GetString(buf, 0, read2);
// Обрабатываем COIN сразу, чтобы не потерять
if (result2 == "COIN")
{
AddCredit(1, 2); // 1 монета, игрок 2
Debug.Log("COM2: COIN (Игрок 2)");
}
// Если пришёл INPUT - обновляем кэш
else if (result2.StartsWith("INPUT"))
{
lastInputBits2 = result2.Substring(5); // сохраняем биты 8-15
hasNewInput2 = true;
}
}
// Если хотя бы с одной платы пришёл INPUT - объединяем с кэшем
if (hasNewInput1 || hasNewInput2)
{
// Берём нужные биты из каждой платы:
// - от платы 1: первые 8 символов (биты 0-7)
// - от платы 2: последние 8 символов (биты 8-15)
string bits0_7 = lastInputBits1.Substring(0, 8); // первые 8 бит от платы 1
string bits8_15 = lastInputBits2.Substring(8, 8); // последние 8 бит от платы 2
// Формируем строку эффективно через StringBuilder
inputBuilder.Clear();
inputBuilder.Append("INPUT");
inputBuilder.Append(bits0_7);
inputBuilder.Append(bits8_15);
string combined = inputBuilder.ToString();
if (hasNewInput1 && hasNewInput2)
{
Debug.Log($"Обе платы ответили: [{bits0_7}]+[{bits8_15}]=[{combined}]");
}
else if (hasNewInput1)
{
Debug.Log($"Плата1 новая, плата2 кэш: [{bits0_7}]+[{bits8_15}]=[{combined}]");
}
else
{
Debug.Log($"Плата1 кэш, плата2 новая: [{bits0_7}]+[{bits8_15}]=[{combined}]");
}
return combined;
}
return "";
}
//просим прислать состояние цифровых входов. Приходит что то вроде "INPUT111111101"
private void requestInputs() => Send(REQUEST_INPUT);
private void pollPort()
{
//Тут все просто, читаем что нам приходит с порта и сравниваем.
string comInput = ReadBufferLine();
if (comInput != "")
{
Debug.Log("COM: " + comInput);
// COIN уже обработан в ReadBufferLine(), здесь только INPUT
if (comInput.StartsWith("INPUT")) onInputRecieved?.Invoke(comInput.Substring(5)); //инвокаю сразу без префикса "INPUT"
}
}
#endregion
}
public enum LightMode
{
Idle,
Orange,
Blue,
Win,
Lose,
Prize,
}