Danro/Assets/Scripts/Managers/BalanceManager.cs
Danil Iarosh e9d5562684 Fix infinite recursion crash in BalanceManager on payment pulse
Исправлена бесконечная рекурсия (StackOverflowException) при синхронизации баланса с кредитами платы.

Детали изменений:
- В класс BalanceManager добавлен флаг-предохранитель _isSyncingCredits.
- Обработчик OnImpulsePaid теперь игнорирует события начисления кредитов, вызванные синхронизацией.
- В методе SyncCreditsWithBalance вызов ComSystemManager.SetCredits обернут в блок try-finally для безопасного управления флагом _isSyncingCredits.
2026-06-30 11:11:11 +03:00

342 lines
12 KiB
C#
Raw Permalink 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 RR.Common.Controls;
using RR.Common.Settings;
using UnityEngine;
using UnityEngine.Events;
namespace RR.Common.Managers
{
/// <summary>
/// Менеджер игрового баланса. Управляет внутриигровой валютой,
/// которая начисляется за импульсы оплаты и списывается за начало/продолжение игры.
/// </summary>
public class BalanceManager : MonoBehaviour
{
public static BalanceManager Instance { get; private set; }
[Header("Текущий баланс")]
[SerializeField] private int currentBalance;
[Header("Настройки сохранения")]
[SerializeField] private bool saveBalanceBetweenSessions;
// События для уведомления UI и других систем
public UnityEvent<int> OnBalanceChanged;
public UnityEvent<int, int> OnBalanceAdded; // (добавлено, новый баланс)
public UnityEvent<int, int> OnBalanceSpent; // (потрачено, новый баланс)
/// <summary>
/// Текущий баланс игрока
/// </summary>
public int CurrentBalance => currentBalance;
private const string BALANCE_SAVE_KEY = "PlayerBalance";
// Кешированные значения из настроек
private int _impulseCost = 10;
private int _startGameCost = 10;
private int _continueGameCost = 5;
private bool _freeGame;
private bool _isSyncingCredits;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
Initialize();
}
private void Initialize()
{
// Инициализация событий
OnBalanceChanged ??= new UnityEvent<int>();
OnBalanceChanged ??= new UnityEvent<int>();
OnBalanceAdded ??= new UnityEvent<int, int>();
OnBalanceSpent ??= new UnityEvent<int, int>();
// Загрузка баланса из сохранений
if (saveBalanceBetweenSessions)
{
currentBalance = PlayerPrefs.GetInt(BALANCE_SAVE_KEY, 0);
}
else
{
currentBalance = 0;
PlayerPrefs.SetInt(BALANCE_SAVE_KEY, 0);
}
// Подписка на события настроек для кеширования значений
var settingsManager = SettingsManager.Instance;
if (settingsManager != null)
{
settingsManager.OnSettingsLoaded += CacheSettingsValues;
settingsManager.OnSettingsSaved += CacheSettingsValues;
CacheSettingsValues(); // Загрузка при инициализации
}
// Подписка на события оплаты
if (PaymentManager.Instance != null)
{
PaymentManager.Instance.OnCreditAdded.AddListener(OnImpulsePaid);
}
Debug.Log($"[BalanceManager] Инициализирован. Баланс: {currentBalance}");
}
private void OnDestroy()
{
if (PaymentManager.Instance != null)
{
PaymentManager.Instance.OnCreditAdded.RemoveListener(OnImpulsePaid);
}
var settingsManager = SettingsManager.Instance;
if (settingsManager != null)
{
settingsManager.OnSettingsLoaded -= CacheSettingsValues;
settingsManager.OnSettingsSaved -= CacheSettingsValues;
}
}
/// <summary>
/// Кеширует значения из настроек
/// </summary>
private void CacheSettingsValues()
{
var settings = SettingsManager.Instance;
if (settings == null)
{
Debug.LogError("[BalanceManager] SettingsManager is null, cannot cache settings!");
return;
}
_impulseCost = settings.GetInt("payment.paymentImpulseCost");
_startGameCost = settings.GetInt("payment.startGameCost");
_continueGameCost = settings.GetInt("payment.continueGameCost");
_freeGame = settings.GetBool("payment.freeGame");
// Валидация значений
if (_impulseCost <= 0)
{
Debug.LogError($"[BalanceManager] Invalid payment.paymentImpulseCost: {_impulseCost}");
_impulseCost = 10;
}
if (_startGameCost <= 0)
{
Debug.LogError($"[BalanceManager] Invalid payment.startGameCost: {_startGameCost}");
_startGameCost = 10;
}
if (_continueGameCost <= 0)
{
Debug.LogError($"[BalanceManager] Invalid payment.continueGameCost: {_continueGameCost}");
_continueGameCost = 5;
}
}
/// <summary>
/// Обработчик оплаты импульса - начисляет баланс
/// </summary>
private void OnImpulsePaid(int creditsCount)
{
if (_isSyncingCredits)
return;
// creditsCount - это количество добавленных кредитов (всегда 1 для одного импульса)
// Начисляем стоимость импульса
AddBalance(_impulseCost);
}
/// <summary>
/// Добавляет баланс игроку
/// </summary>
/// <param name="amount">Количество добавляемого баланса</param>
public void AddBalance(int amount)
{
if (amount <= 0)
{
Debug.LogWarning($"[BalanceManager] Trying to add invalid amount: {amount}");
return;
}
currentBalance += amount;
// Сохранение если включено
if (saveBalanceBetweenSessions)
{
PlayerPrefs.SetInt(BALANCE_SAVE_KEY, currentBalance);
PlayerPrefs.Save();
}
// Синхронизация кредитов с балансом
SyncCreditsWithBalance();
// Уведомление подписчиков
OnBalanceAdded?.Invoke(amount, currentBalance);
OnBalanceChanged?.Invoke(currentBalance);
}
/// <summary>
/// Проверяет, достаточно ли баланса для начала игры
/// </summary>
public bool CanStartGame()
{
// Проверка бесплатной игры
if (_freeGame)
return true;
return currentBalance >= _startGameCost;
}
/// <summary>
/// Проверяет, достаточно ли баланса для продолжения игры
/// </summary>
public bool CanContinueGame()
{
// Проверка бесплатной игры
if (_freeGame)
return true;
return currentBalance >= _continueGameCost;
}
/// <summary>
/// Списывает баланс за начало игры
/// </summary>
/// <returns>true если списание успешно</returns>
public bool SpendForStart()
{
// Проверка бесплатной игры
if (_freeGame)
return true;
return SpendBalance(_startGameCost, "начало игры");
}
/// <summary>
/// Списывает баланс за продолжение игры
/// </summary>
/// <returns>true если списание успешно</returns>
public bool SpendForContinue()
{
// Проверка бесплатной игры
if (_freeGame)
return true;
return SpendBalance(_continueGameCost, "продолжение игры");
}
/// <summary>
/// Списывает указанное количество баланса
/// </summary>
/// <param name="amount">Количество списываемого баланса</param>
/// <param name="reason">Причина списания (для логирования)</param>
/// <returns>true если списание успешно</returns>
public bool SpendBalance(int amount, string reason = "")
{
if (amount <= 0)
{
Debug.LogWarning($"[BalanceManager] Trying to spend invalid amount: {amount}");
return false;
}
if (currentBalance < amount)
{
return false;
}
currentBalance -= amount;
// Сохранение если включено
if (saveBalanceBetweenSessions)
{
PlayerPrefs.SetInt(BALANCE_SAVE_KEY, currentBalance);
PlayerPrefs.Save();
}
// Синхронизация кредитов с балансом
SyncCreditsWithBalance();
// Уведомление подписчиков
OnBalanceSpent?.Invoke(amount, currentBalance);
OnBalanceChanged?.Invoke(currentBalance);
return true;
}
/// <summary>
/// Сбрасывает баланс до нуля
/// </summary>
public void ResetBalance()
{
currentBalance = 0;
if (saveBalanceBetweenSessions)
{
PlayerPrefs.SetInt(BALANCE_SAVE_KEY, 0);
PlayerPrefs.Save();
}
// Синхронизация кредитов с балансом
SyncCreditsWithBalance();
OnBalanceChanged?.Invoke(currentBalance);
}
/// <summary>
/// Синхронизирует кредиты в ComSystemManager с текущим балансом
/// по формуле: кредиты = баланс / стоимость_импульса (округление вниз)
/// </summary>
private void SyncCreditsWithBalance()
{
if (_impulseCost <= 0)
{
Debug.LogError("[BalanceManager] Cannot sync credits - invalid impulse cost");
return;
}
int targetCredits = currentBalance / _impulseCost;
int currentCredits = ComSystemManager.GetCredits();
if (targetCredits == currentCredits)
return; // Кредиты уже синхронизированы
_isSyncingCredits = true;
try
{
// Устанавливаем кредиты напрямую через SetCredits
ComSystemManager.SetCredits(targetCredits);
}
finally
{
_isSyncingCredits = false;
}
}
/// <summary>
/// Устанавливает баланс напрямую (для тестирования/отладки)
/// </summary>
public void SetBalance(int amount)
{
currentBalance = Mathf.Max(0, amount);
if (saveBalanceBetweenSessions)
{
PlayerPrefs.SetInt(BALANCE_SAVE_KEY, currentBalance);
PlayerPrefs.Save();
}
// Синхронизация кредитов с балансом
SyncCreditsWithBalance();
OnBalanceChanged?.Invoke(currentBalance);
}
}
}