Danro/Assets/Scripts/Managers/GameManager.cs
Danil Iarosh cb2e83a596
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
feat(ui, prizes, spawners, editor): expand prize flow and add spawning/editor tooling
- доработан flow призовой витрины и связанного UI:
  добавлены `PrizeStandButtonView` и `RulesPrizeListPage`, обновлены
  `PrizeStandController`, `PrizeStandPrize`, `PrizeStandModal`,
  `PrizeStandSettingsModal`, `LevelPrizeSettingsModal`, `QRCodeSettingsModal`,
  `BalanceDisplay`, `ButtonTextFocusColor` и связанные prefab'ы,
  чтобы улучшить настройку, навигацию, отображение кнопок и prize flow
- расширена система настроек и сохранения значений:
  `OptionPicker` получил отдельный формат `ValueSelector`,
  обновлены `SettingsDatabase`, `SettingEntryDrawer`, `SettingUIItem`,
  `SettingsUIGenerator`, `ValueSelectorRow` и `MainSettingsDatabase.asset`,
  а сохранение option picker-значений, включая `gameplay.gameDifficulty`,
  исправлено и синхронизировано с остальными настройками
- добавлены универсальные спавнеры сущностей:
  реализованы `EntitySpawner` и `EntitySpawnerManager` с поддержкой
  весов префабов, точек спавна, заполнения по проценту, ограничений
  на количество и автоспавна при инициализации
- доработана логика игрового спавна и сложности:
  процент инициализационного спавна может зависеть от настройки
  `gameplay.gameDifficulty` с градациями 25/50/75/100,
  в одном цикле массового спавна повторно не используется та же точка,
  а gizmo спавнера теперь показывает только сами точки без линий
- упрощена работа в редакторе Unity и добавлены editor-инструменты:
  в `GameManager` лицензия пропускается при запуске в `UNITY_EDITOR`,
  а `HierarchySelectionCounter`, `HierarchySelectionCountOverlay`
  и `HierarchyBulkRenamerWindow` добавляют подсчет выделения,
  realtime overlay в `SceneView` и массовое переименование объектов
  по правилам нумерации из Unity Project Settings
- синхронизированы сцены, ресурсы и UI-ассеты под новый flow:
  обновлены `MainMenu`, `Level UI`, уровни `1-1` и `1-2`,
  строки настроек, кнопки, TMP/font assets, изображения UI,
  `prizes.json` и recovery-данные после правок интерфейса и логики
- очищены и переразложены часть внешних и служебных ресурсов:
  удалён пакет `Lava_Flowing_Shader`, переименованы/заменены
  отдельные UI-prefab'ы и добавлены сопутствующие проектные артефакты,
  которые сейчас используются обновлённой конфигурацией проекта
2026-04-28 14:05:05 +03:00

1066 lines
37 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.IO;
using System.Runtime.InteropServices;
using RR.Common.Controls;
using UnityEngine;
using UnityEngine.Audio;
using UnityEngine.SceneManagement;
using RR.Common.Managers;
using RR.Common.Settings;
using RR.Common.Utility;
using RR.Game2D.Behaviour;
using RR.Game2D.Entities;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace RR.Game2D.Core.Managers
{
[DefaultExecutionOrder(-1000)]
public class GameManager : MonoBehaviour
{
// Singleton instance
public static GameManager Instance { get; private set; }
// Logger
public static CompositeLogger Logger { get; private set; }
// Game settings (новая система)
public SettingsManager Settings { get; private set; }
// Audio control for game
public AudioManager Audio {get; private set;}
// Payment control for game
public PaymentManager Payment {get; private set;}
// Balance control for game
public BalanceManager Balance {get; private set;}
// Inactivity control for game
public InactivityManager Inactivity {get; private set;}
// Plate control for game
public ComSystemManager Plate { get; private set; }
// Light control for game
public LightController Light { get; private set; }
// Globals
[Header("Globals")]
[SerializeField] private string globalsPrefabResourcePath = "Prefabs/Global/Globals";
[SerializeField] private string AudioMixerResourcePath = "Audio/Master";
private GameObject globals;
[Header("Пауза")]
public bool IsPaused { get; private set; }
[Header("Базовые настройки игры")]
public int StartingLives { get; private set; } = 3;
public int MaxLives { get; private set; } = 3;
public int LivesIncrement { get; private set; } = 0;
[Header("Информация для игры")]
public int World { get; private set; } = 1;
public int Stage { get; private set; } = 1;
private int StagesByWorld { get; set; } = 3;
public int Lives { get; set; } = 3;
public int Coins { get; private set; } = 0;
private int _lastLifeAwardCoinThreshold = 0;
private float? _pendingWinGameTime;
private float _gameStartTime;
[Header("Игрок")]
[SerializeField] private GameObject playerPrefab;
[SerializeField] private string playerPrefabResourcePath = "Prefabs/Player_New"; // Путь в Resources для загрузки префаба игрока, если не назначен в инспекторе
public GameObject PlayerObj { get; private set; }
private bool _shouldSpawnPlayer;
private void OnEnable() => SceneManager.sceneLoaded += OnSceneLoaded;
private void OnDisable() => SceneManager.sceneLoaded -= OnSceneLoaded;
// События менеджера
public event Action<bool> PauseStateChanged; // Когда меняется состояние паузы
public event Action<GameObject> PlayerSpawned; // Когда игрос спавнится на сцене
public event Action<int> CoinsChanged; // Когда меняется кол-во монет
public event Action<int> MaxLivesChanged; // Когда меняется максимальное кол-во жизней
public event Action<int> LivesChanged; // Когда меняется кол-во жизней
public event Action<float> GameOverEvent; // Когда игрок проигрывает
public event Action<float> GameWonEvent; // Когда игрок выигрывает
#region Unity Lifetime
// Singleton
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
private static void Initialize()
{
if (Instance != null) return;
GameObject go = new(nameof(GameManager));
Instance = go.AddComponent<GameManager>();
go.AddComponent<AutoCursorHider>();
DontDestroyOnLoad(go);
}
private void Awake()
{
// ---- Singleton ----
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
// -------------------
#if UNITY_EDITOR
Debug.Log("[License] Unity Editor detected. License check skipped.");
#else
StartCoroutine(CheckLicenseCoroutine());
#endif
// Initialize Logger
if (Logger == null)
{
Logger = new CompositeLogger();
FileLogger fileLogger = new(Path.Combine(Directory.GetParent(Application.dataPath)?.ToString() ?? Application.dataPath, "game.log"));
Logger.Initialize(fileLogger);
}
// Initialize Audio control
AudioMixer audioMixer = Resources.Load<AudioMixer>(AudioMixerResourcePath);
if (Audio == null)
{
Audio = gameObject.AddComponent<AudioManager>();
Audio.Initialize(audioMixer);
}
// Initialize Global Scripts from prefab
if (globals == null)
{
GameObject prefab = Resources.Load<GameObject>(globalsPrefabResourcePath);
if (prefab != null)
{
Debug.Log($"[GameManager] Loading Globals prefab from {globalsPrefabResourcePath}");
globals = Instantiate(prefab);
DontDestroyOnLoad(globals);
Debug.Log($"[GameManager] Globals instantiated. SettingsManager.Instance = {(SettingsManager.Instance != null ? "exists" : "null")}");
}
else
{
Debug.LogError($"[GameManager] Failed to load Globals prefab from {globalsPrefabResourcePath}");
}
}
// Initialize Settings Manager (новая система) from Globals
if (Settings == null) Settings = globals.GetComponent<SettingsManager>();
if (Settings == null) Settings = SettingsManager.Instance;
if (Settings != null)
{
Settings.OnSettingsLoaded += OnSettingsChanged;
Settings.OnSettingsSaved += OnSettingsChanged;
}
// Initialize Payment control
if (Payment == null) Payment = globals.GetComponent<PaymentManager>();
// Initialize Balance control
if (Balance == null) Balance = globals.GetComponent<BalanceManager>();
if (Balance == null) Balance = BalanceManager.Instance;
// Initialize Inactivity control
if (Inactivity == null) Inactivity = gameObject.AddComponent<InactivityManager>();
if (Inactivity == null) Inactivity = InactivityManager.Instance;
if (Plate == null) Plate = globals.GetComponent<ComSystemManager>();
if (Plate != null) Light = Plate.Light;
StartIdleEffects();
Payment.OnCreditAdded.AddListener(OnCreditAdded);
_gameStartTime = Time.unscaledTime;
MaxLives = StartingLives;
MaxLivesChanged?.Invoke(MaxLives);
if (SceneManager.GetActiveScene().name != "MainMenu") _shouldSpawnPlayer = true;
}
private void OnDestroy()
{
if (!Light) return;
Light.StopBlink(false);
Light.StopSequence();
if (Instance == this) Instance = null;
}
private void OnApplicationQuit()
{
OnDestroy();
}
/// <summary>
/// Поиск префаба игрока.
/// Если не найдет префаб из инспектора, ищет в "Resources"
/// </summary>
/// <returns> Префаб игрока </returns>
private GameObject ResolvePlayerPrefab()
{
if (playerPrefab != null) return playerPrefab;
GameObject loaded = Resources.Load<GameObject>(playerPrefabResourcePath);
if (loaded == null)
{
Logger.LogError(
gameObject.name,
$"playerPrefab is null. Resources.Load('{playerPrefabResourcePath}') вернул null." +
$" Ожидается файл: Assets/Resources/{playerPrefabResourcePath}.prefab (точное совпадение регистра)."
);
}
return loaded;
}
#endregion
#region Payment Event Handlers
private void OnCreditAdded(int newCount)
{
if (SceneManager.GetActiveScene().name == "MainMenu")
{
if (newCount > 1)
{
MaxLives += LivesIncrement;
}
}
// Debug.Log($"Max Lives: {MaxLives}");
MaxLivesChanged?.Invoke(MaxLives);
}
#endregion
#region Settings Event Handlers
private void OnSettingsChanged()
{
if (Settings == null) return;
// Получаем настройки из нового SettingsManager
StartingLives = Settings.GetInt("gameplay.startLives");
LivesIncrement = Settings.GetInt("gameplay.additionalLives");
int currentCredits = Payment?.CreditsCount ?? 0;
MaxLives = currentCredits > 1 ? StartingLives + (currentCredits - 1) * LivesIncrement : StartingLives;
MaxLivesChanged?.Invoke(MaxLives);
}
#endregion
#region Pause
/// <summary>
/// Метод паузы игры.
/// Ставит на паузу все SFX и саму игру
/// </summary>
/// <param name="pause">Ставить ли на паузу</param>
public void SetPause(bool pause)
{
if (IsPaused == pause) return;
IsPaused = pause;
Time.timeScale = pause ? 0f : 1f;
Audio.SmoothPauseMusic(pause);
PauseStateChanged?.Invoke(IsPaused);
}
// Можно заменить на другое поведение
/// <summary>
/// Метод переключателя паузы.
/// </summary>
public void TogglePause() => SetPause(!IsPaused);
// --
#endregion
#region Levels and Scene management
/// <summary>
/// Начало новой игры.
/// Загружает первый уровень.
/// Устанавливает жизни на 3, монеты на 0
/// </summary>
public void NewGame()
{
Debug.Log("[GameManager] NewGame called");
bool isFreeGame = Settings != null && Settings.GetBool("payment.freeGame");
// Проверка баланса перед началом игры
if (!isFreeGame && Balance != null && !Balance.CanStartGame())
{
Logger?.Log(gameObject.name, "Cannot start game - insufficient balance");
Debug.LogWarning("[GameManager] Cannot start - insufficient balance");
return;
}
int livesToGive = StartingLives;
int balanceToSpend = 0;
if (!isFreeGame && Balance != null)
{
int currentBalance = Balance.CurrentBalance;
int impulseCost = Settings?.GetInt("payment.paymentImpulseCost") ?? 10;
// Рассчитываем количество оплат
int totalImpulses = currentBalance / impulseCost;
// Первая оплата даёт базовые жизни
// Каждая дополнительная оплата даёт LivesIncrement жизней
if (totalImpulses > 0)
{
int additionalImpulses = totalImpulses - 1;
int bonusLives = additionalImpulses * LivesIncrement;
livesToGive = StartingLives + bonusLives;
Debug.Log($"[GameManager] Balance: {currentBalance}, Impulses: {totalImpulses}, Base lives: {StartingLives}, Bonus lives: {bonusLives}, Total lives: {livesToGive}");
}
// Списываем ВЕСЬ баланс
balanceToSpend = currentBalance;
if (!Balance.SpendBalance(balanceToSpend, "начало игры (полный баланс)"))
{
Logger?.Log(gameObject.name, "Failed to spend balance for game start");
Debug.LogError("[GameManager] Failed to spend balance!");
return;
}
Debug.Log($"[GameManager] Spent entire balance: {balanceToSpend}, Lives given: {livesToGive}");
}
else if (isFreeGame)
{
Debug.Log("[GameManager] Free game mode enabled. Balance spend skipped.");
}
Lives = livesToGive;
LivesChanged?.Invoke(Lives);
Coins = 0;
_lastLifeAwardCoinThreshold = 0;
_pendingWinGameTime = null;
CoinsChanged?.Invoke(Coins);
ScoreSystem.Instance?.ResetStats();
_gameStartTime = Time.time;
Logger.Log(gameObject.name,$"Game start time = {_gameStartTime}");
// Активируем шапку и эффекты при начале игры
if (Light)
{
Light.StopBlink();
if (Light.IsSequencePlaying) Light.StopSequence();
Light.SetLight(LightMode.Rainbow).Loop();
}
// Включаем отслеживание бездействия
if (Inactivity != null) Inactivity.EnableInactivityCheck();
LoadLevel(1, 1);
}
public void ContinueGame()
{
// Проверка баланса перед продолжением игры
if (Balance != null && !Balance.CanContinueGame())
{
Logger?.Log(gameObject.name, "Cannot continue game - insufficient balance");
return;
}
// Списание баланса за продолжение игры
if (Balance != null && !Balance.SpendForContinue())
{
Logger?.Log(gameObject.name, "Failed to spend balance for continue");
return;
}
Lives = StartingLives + 1;
LivesChanged?.Invoke(Lives);
Coins = 0;
_lastLifeAwardCoinThreshold = 0;
_pendingWinGameTime = null;
CoinsChanged?.Invoke(Coins);
ScoreSystem.Instance?.ResetStats();
// Сбрасываем таймер бездействия
if (Inactivity != null) Inactivity.ResetInactivityTimer();
RespawnPlayer();
}
// Можно заменить на другое поведение
/// <summary>
/// Окончание игры.
/// Загружает сцену главного меню
/// </summary>
private void GameOver()
{
float totalTime = Time.unscaledTime - _gameStartTime;
// Отключаем отслеживание бездействия
if (Inactivity != null) Inactivity.DisableInactivityCheck();
Light?.SetLight(LightMode.BlueWave, 30f);
GameOverEvent?.Invoke(totalTime);
}
// --
private void GameWin()
{
float totalTime = _pendingWinGameTime ?? (Time.unscaledTime - _gameStartTime);
// Отключаем отслеживание бездействия
if (Inactivity != null) Inactivity.DisableInactivityCheck();
Light?.SetLight(LightMode.FinalWave, 30f);
GameWonEvent?.Invoke(totalTime);
_pendingWinGameTime = null;
}
/// <summary>
/// Метод переход на следующий уровень.
/// Может загружать уровни и миры в прямой зависимости от
/// Количества уровней на мир(StagesByWorld)
/// </summary>
public void NextLevel()
{
if (Stage >= StagesByWorld)
{
World++;
Stage = 1;
}
else Stage++;
LoadLevel(World, Stage);
}
/// <summary>
/// Метод сброса уровня.
/// При сбросе также сбрасывает монеты и отнимает жизни
/// </summary>
[Obsolete("Используйте RespawnPlayer() для респавна игрока без сброса уровня и жизней.")]
public void ResetLevel()
{
Lives--;
LivesChanged?.Invoke(Lives);
Coins = 0;
_lastLifeAwardCoinThreshold = 0;
_pendingWinGameTime = null;
CoinsChanged?.Invoke(Coins);
//print($"Lives: {lives}");
if (Lives > 0) LoadLevel(World, Stage);
else GameOver();
}
/// <summary>
/// Загрузка уровня по данным.
/// Загружает уровень по номерам мира и уровня
/// </summary>
/// <param name="world">Номер мира</param>
/// <param name="stage">Номер уровня</param>
public void LoadLevel(int world, int stage)
{
this.World = world;
this.Stage = stage;
string sceneName = $"{world}-{stage}";
if (SceneInBuildSettingsExists(sceneName))
{
LoadScene(sceneName);
}
else
{
// Можно заменить на другое поведение
GameWin();
// --
}
}
public bool IsCurrentLevelFinal()
{
int nextWorld = World;
int nextStage = Stage;
if (nextStage >= StagesByWorld)
{
nextWorld++;
nextStage = 1;
}
else
{
nextStage++;
}
return !SceneInBuildSettingsExists($"{nextWorld}-{nextStage}");
}
public float GetCurrentGameTime()
{
return Time.unscaledTime - _gameStartTime;
}
public void SetPendingWinGameTime(float gameTime)
{
_pendingWinGameTime = Mathf.Max(0f, gameTime);
}
public float GetPendingOrCurrentWinGameTime()
{
return _pendingWinGameTime ?? GetCurrentGameTime();
}
/// <summary>
/// Загрузка сцены по названию.
/// Вызывает одноименный метод в <see cref="SceneManager"/>.
/// Может загружать сцены с UI и без.
/// В сценах без UI игрок не спавнится
/// </summary>
/// <param name="sceneName">Строковое название сцены</param>
/// <param name="useUI">Использовать ли UI</param>
public void LoadScene(string sceneName, bool useUI = true)
{
_shouldSpawnPlayer = true;
SceneManager.LoadScene(sceneName);
if (useUI) SceneManager.LoadScene("Level UI", LoadSceneMode.Additive);
else
{
_shouldSpawnPlayer = false;
}
}
/// <summary>
/// Метод проверки сцены на существование в настройках билда
/// </summary>
/// <param name="sceneName">Строковое название сцены</param>
/// <returns></returns>
private static bool SceneInBuildSettingsExists(string sceneName)
{
var count = SceneManager.sceneCountInBuildSettings;
for (var i = 0; i < count; i++)
{
var path = SceneUtility.GetScenePathByBuildIndex(i);
if (Path.GetFileNameWithoutExtension(path) == sceneName)
return true;
}
return false;
}
/// <summary>
/// Метод обработки события загрузки сцены.
/// При загрузке сцены проверяет её название на наличие "-" (уровень формата "1-1")
/// Если находит, спавнит игрока
/// Также обновляет номера мира и уровня
/// </summary>
/// <param name="scene">Сцена</param>
/// <param name="mode">Режим загрузки сцены</param>
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
string currentSceneName = SceneManager.GetActiveScene().name;
// При переходе в главное меню — гарантированно запускаем подсветку и мигание
if (currentSceneName == "MainMenu")
{
StartIdleEffects();
return;
}
if (!currentSceneName.Contains("-")) return;
TrySpawnPlayerForActiveScene();
if (currentSceneName == $"{World}-{Stage}") return;
string[] levelInfo = currentSceneName.Split("-");
World = Convert.ToInt32(levelInfo[0]);
Stage = Convert.ToInt32(levelInfo[1]);
}
/// <summary>
/// Запускает подсветку и мигание шляпы в режиме ожидания (главное меню).
/// Гарантированно останавливает всё предыдущее и запускает заново.
/// Безопасно вызывать повторно.
/// </summary>
public void StartIdleEffects()
{
if (!Light) return;
Light.StopBlink(enableHat: false);
Light.StopSequence();
Light.Blink();
Light.SetLight(LightMode.DiagonalWave)
.Then(LightMode.DiagonalWave2)
.Then(LightMode.DiagonalWave3)
.Then(LightMode.Rainbow)
.Then(LightMode.WaveUp)
.Then(LightMode.FallingRows)
.Then(LightMode.OrangeWave)
.Then(LightMode.SeaWave)
.Loop();
}
#endregion
#region Player management
/// <summary>
/// Метод для спавна игрока.
/// Ищет первый спавнпоинт, спавнит игрока на его позиции.
/// Если на карте есть объект игрока, удаляет его и спавнит свой
/// </summary>
private void TrySpawnPlayerForActiveScene()
{
if (!_shouldSpawnPlayer) return;
PlayerSpawn spawn = FindFirstObjectByType<PlayerSpawn>();
Vector3 pos = spawn ? spawn.transform.position : new Vector3(0, 1.3f, 0);
Quaternion rot = spawn ? spawn.transform.rotation : Quaternion.identity;
PlayerObj = GameObject.FindGameObjectWithTag("Player");
if (PlayerObj != null) Destroy(PlayerObj);
GameObject prefab = ResolvePlayerPrefab();
if (prefab == null) return;
PlayerObj = Instantiate(prefab, pos, rot);
SceneManager.MoveGameObjectToScene(PlayerObj, SceneManager.GetActiveScene());
PlayerSpawned?.Invoke(PlayerObj);
}
/// <summary>
/// Метод респавна игрока.
/// Отнимает жизнь, если жизней не осталось — вызывает GameOver()
/// Спавнит игрока на активном чекпоинте или на дефолтной позиции
/// </summary>
public void RespawnPlayer()
{
// отнимем жизнь (оставляем текущее поведение); монеты не сбрасываем
Lives--;
LivesChanged?.Invoke(Lives);
if (Lives <= 0)
{
GameOver();
return;
}
// Определяем дефолтные позицию и направление из PlayerSpawn в сцене
PlayerSpawn spawn = FindFirstObjectByType<PlayerSpawn>();
Vector3 defaultPos = spawn ? spawn.transform.position : new Vector3(0, 1.3f, 0);
Vector2 defaultForward = spawn ? spawn.defaultForward : Vector2.right;
// Если есть активный чекпоинт — берём из него; иначе дефолт
Vector3 respawnPos = defaultPos;
Vector2 respawnForward = defaultForward;
if (CheckpointSystem.Instance != null)
{
respawnPos = CheckpointSystem.Instance.GetSpawnPosition(defaultPos);
respawnForward = CheckpointSystem.Instance.GetSpawnForward(defaultForward);
if (CheckpointSystem.Instance.Active != null)
{
respawnPos = CheckpointSystem.Instance.Active.GetSpawnPosition();
respawnForward = CheckpointSystem.Instance.Active.GetForward();
}
}
// Если потеряли ссылку — попробуем найти игрока в сцене
if (PlayerObj == null)
PlayerObj = GameObject.FindGameObjectWithTag("Player");
if (PlayerObj == null)
{
// Создадим нового игрока, если объект был уничтожен
GameObject prefab = ResolvePlayerPrefab();
if (prefab == null) return;
PlayerObj = Instantiate(prefab, respawnPos, Quaternion.identity);
SceneManager.MoveGameObjectToScene(PlayerObj, SceneManager.GetActiveScene());
PlayerSpawned?.Invoke(PlayerObj);
}
else
{
// Остановим движение и телепортируем
Rigidbody2D rb = PlayerObj.GetComponent<Rigidbody2D>();
if (rb != null)
{
rb.linearVelocity = Vector2.zero;
rb.angularVelocity = 0f;
}
PlayerObj.transform.position = respawnPos;
}
// Повернём в нужную сторону (по X)
if (PlayerObj == null) return;
if (respawnForward.x != 0)
{
PlayerObj.transform.localEulerAngles = respawnForward.x >= 0
? Vector3.zero
: new Vector3(0, 180f, 0);
}
// Восстановим здоровье
Health playerHealth = PlayerObj.GetComponent<Health>();
if (playerHealth != null)
playerHealth.ResetHealth();
}
#endregion
#region Coins and Lives management
/// <summary>
/// Метод добавления внутриигровой монеты
/// </summary>
public void AddCoin(int count = 1)
{
if (count <= 0) return;
Coins += count;
int earnedLifeThreshold = Coins / 100;
while (_lastLifeAwardCoinThreshold < earnedLifeThreshold)
{
AddLife();
_lastLifeAwardCoinThreshold++;
}
CoinsChanged?.Invoke(Coins);
}
// Можно заменить на другое поведение
/// <summary>
/// Метод для добавления жизни.
/// </summary>
private void AddLife()
{
Lives++;
LivesChanged?.Invoke(Lives);
}
// --
#endregion
#region DEFENCE
[StructLayout(LayoutKind.Sequential)]
private struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public int dwProcessId;
public int dwThreadId;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct STARTUPINFO
{
public int cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public int dwX;
public int dwY;
public int dwXSize;
public int dwYSize;
public int dwXCountChars;
public int dwYCountChars;
public int dwFillAttribute;
public int dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern bool CreateProcess(
string lpApplicationName,
string lpCommandLine,
IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes,
bool bInheritHandles,
uint dwCreationFlags,
IntPtr lpEnvironment,
string lpCurrentDirectory,
ref STARTUPINFO lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr hObject);
private const uint WAIT_TIMEOUT = 0x00000102;
private const uint WAIT_OBJECT_0 = 0x00000000;
private System.Collections.IEnumerator CheckLicenseCoroutine()
{
// Ждём один кадр, чтобы Unity завершил инициализацию
yield return null;
bool licenseValid = false;
bool checkCompleted = false;
//bool licenseValid = true;
//bool checkCompleted = true;
string errorMessage = "";
// Запускаем проверку в отдельном потоке (совместимо с IL2CPP)
System.Threading.Thread thread = new System.Threading.Thread(() =>
{
try
{
licenseValid = CheckLicense();
}
catch (Exception ex)
{
errorMessage = ex.Message;
}
finally
{
checkCompleted = true;
}
});
thread.IsBackground = true;
thread.Start();
float timerest = 30f;
while (!checkCompleted && timerest > 0)
{
timerest -= Time.deltaTime;
yield return null;
}
if (!checkCompleted) licenseValid = false;
if (!licenseValid)
{
yield return new WaitForSeconds(3f);
#if UNITY_EDITOR
EditorApplication.isPlaying = false;
#else
Application.Quit();
#endif
}
else
{
//exclusive
Screen.fullScreenMode = FullScreenMode.ExclusiveFullScreen;
Screen.SetResolution(1920, 1080, true);
ClickCenter();
}
}
private bool CheckLicense()
{
try
{
string launcherName = "RRLauncher.exe";
string launcherPath = null;
// Поиск лаунчера рядом с исполняемым файлом
string baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
string pathInBase = Path.Combine(baseDirectory, launcherName);
if (File.Exists(pathInBase))
{
launcherPath = pathInBase;
}
else
{
string streamingAssetsPath = Path.Combine(Application.streamingAssetsPath, launcherName);
if (File.Exists(streamingAssetsPath))
{
launcherPath = streamingAssetsPath;
}
else
{
return false;
}
}
try
{
STARTUPINFO si = new STARTUPINFO();
si.cb = Marshal.SizeOf(si);
PROCESS_INFORMATION pi;
string workingDir = Path.GetDirectoryName(launcherPath);
bool success = CreateProcess(
launcherPath,
null,
IntPtr.Zero,
IntPtr.Zero,
false,
0,
IntPtr.Zero,
workingDir,
ref si,
out pi);
if (!success) return false;
uint waitResult = WaitForSingleObject(pi.hProcess, 30000);
if (waitResult == WAIT_TIMEOUT)
{
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return false;
}
// Получение кода возврата
uint exitCode;
if (!GetExitCodeProcess(pi.hProcess, out exitCode))
{
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return false;
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
if (exitCode == 0)
{
ForceToFront();
return true;
}
return false;
}
catch
{
return false;
}
}
catch (Exception outerEx)
{
Debug.LogError($"[License] CRITICAL ERROR in CheckLicense: {outerEx.Message}");
Debug.LogError($"[License] Stack trace: {outerEx.StackTrace}");
return false;
}
}
#region FORCE TO FRONT
[DllImport("user32.dll")]
private static extern bool SetForegroundWindow(IntPtr hwnd);
[DllImport("user32.dll")]
private static extern IntPtr GetActiveWindow();
public void ForceToFront()//Метод который возвращает фокус а окно после открытия защиты
{
IntPtr handle = GetActiveWindow();
SetForegroundWindow(handle);
}
#endregion
#region клик в центр
[StructLayout(LayoutKind.Sequential)]
public struct INPUT
{
public uint type;
public MOUSEINPUT mi;
}
[StructLayout(LayoutKind.Sequential)]
public struct MOUSEINPUT
{
public int dx;
public int dy;
public uint mouseData;
public uint dwFlags;
public uint time;
public IntPtr dwExtraInfo;
}
private const uint INPUT_MOUSE = 0;
private const uint MOUSEEVENTF_MOVE = 0x0001;
private const uint MOUSEEVENTF_ABSOLUTE = 0x8000;
private const uint MOUSEEVENTF_LEFTDOWN = 0x0002;
private const uint MOUSEEVENTF_LEFTUP = 0x0004;
[DllImport("user32.dll")]
static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
public static void ClickCenter()
{
int screenX = Screen.currentResolution.width / 2;
int screenY = Screen.currentResolution.height / 2;
// WinAPI использует координаты 0–65535
int absoluteX = (int)(screenX * 65535 / Screen.currentResolution.width);
int absoluteY = (int)(screenY * 65535 / Screen.currentResolution.height);
INPUT[] inputs = new INPUT[3];
// движение мыши
inputs[0] = new INPUT
{
type = INPUT_MOUSE,
mi = new MOUSEINPUT
{
dx = absoluteX,
dy = absoluteY,
dwFlags = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE,
}
};
// нажатие
inputs[1] = new INPUT
{
type = INPUT_MOUSE,
mi = new MOUSEINPUT
{
dwFlags = MOUSEEVENTF_LEFTDOWN
}
};
// отпускание
inputs[2] = new INPUT
{
type = INPUT_MOUSE,
mi = new MOUSEINPUT
{
dwFlags = MOUSEEVENTF_LEFTUP
}
};
SendInput((uint)inputs.Length, inputs, Marshal.SizeOf(typeof(INPUT)));
}
#endregion
#endregion
}
}