Danil Iarosh a40b761c81
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
feat(prizes, ui): add configurable level prize flow and launcher assets
- добавлена новая система призов:
  PrizeBase, PrizeType, PrizeDatabase, LevelPrizeConfig и реализации
  для hopper, QR-кода, отсутствия приза и стойки с призом
- добавлена конфигурация призов уровней через `prizes.json`
  и связанная логика загрузки/обработки в менеджерах игры
- расширен UI главного меню и настроек уровней:
  добавлены LevelSettingsModal, LevelPrizeSettingsModal,
  QRCodeModal и PrizeStandModal
- обновлена навигация модальных окон и связанный UI-код:
  ModalManager, MainMenu, RulesModal, PauseMenu,
  PrizeErrorDisplay, UIRowPointer и WinBannerController
- доработаны настройки и генерация UI-элементов:
  обновлены SettingsDatabase, SettingsUIGenerator,
  SettingUIItem, ValueSelectorRow и editor drawer для настроек
- обновлены и добавлены UI-prefab'ы для кнопок, строк настроек,
  панели групп и кнопки настроек уровня
- обновлены сцены `MainMenu` и `Level UI` под новый флоу
  настройки уровней, выбора призов и отображения модалок
- добавлены файлы лаунчера в `StreamingAssets`,
  включая `RRLauncher.exe`, служебные meta-файлы и документацию
- улучшено логирование через FileLogger
  и удален устаревший код лицензирования
- удалены больше неиспользуемые файлы:
  `Licensing.cs`, `SteppedSlider.cs` и recovery-сцены
2026-03-16 08:15:36 +03:00

380 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 System;
using System.IO;
using System.Text;
using UnityEngine;
using Object = UnityEngine.Object;
namespace RR.Common.Utility
{
/// <summary>
/// Логгер для записи сообщений в файл. Совместим с CompositeLogger.
/// </summary>
public class FileLogger : ILogger, IDisposable
{
public ILogHandler logHandler { get; set; }
public LogType filterLogType { get; set; }
public bool logEnabled { get; set; } = true;
private readonly string _filePath;
private readonly object _lock = new object();
private StreamWriter _writer;
private bool _disposed;
private bool _footerWritten;
/// <summary>
/// Создает файловый логгер с указанным путём к файлу.
/// </summary>
/// <param name="filePath">Полный путь к файлу лога. Если null, используется Application.persistentDataPath/game.log</param>
/// <param name="append">Если true, добавляет к существующему файлу, иначе перезаписывает</param>
public FileLogger(string filePath = null, bool append = true)
{
_filePath = filePath ?? Path.Combine(Application.persistentDataPath, "game.log");
filterLogType = LogType.Log;
try
{
// Создаем директорию если её нет
string directory = Path.GetDirectoryName(_filePath);
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
{
Directory.CreateDirectory(directory);
}
// Открываем файл для записи
_writer = new StreamWriter(_filePath, append, Encoding.UTF8)
{
AutoFlush = true // Автоматически сбрасывать буфер для надёжности
};
// Записываем заголовок
WriteHeader();
// Подписываемся на завершение приложения для гарантированной записи
Application.quitting += OnApplicationQuitting;
}
catch (Exception ex)
{
Debug.LogError($"[FileLogger] Не удалось открыть файл лога: {_filePath}\n{ex}");
}
}
private void WriteHeader()
{
if (_writer != null)
{
_writer.WriteLine("================================================================================");
_writer.WriteLine($"Новая сессия логирования начата: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
_writer.WriteLine($"Unity версия: {Application.unityVersion}");
_writer.WriteLine($"Платформа: {Application.platform}");
_writer.WriteLine($"Путь к логу: {_filePath}");
_writer.WriteLine("================================================================================");
_writer.WriteLine();
}
}
public bool IsLogTypeAllowed(LogType logType)
{
return logEnabled && logType <= filterLogType;
}
private void WriteToFile(LogType logType, string message)
{
if (!logEnabled || _disposed || _writer == null)
return;
if (!IsLogTypeAllowed(logType))
return;
lock (_lock)
{
try
{
string timestamp = DateTime.Now.ToString("HH:mm:ss.fff");
string logTypeStr = GetLogTypeString(logType);
_writer.WriteLine($"[{timestamp}] [{logTypeStr}] {message}");
}
catch (Exception ex)
{
Debug.LogError($"[FileLogger] Ошибка записи в файл: {ex.Message}");
}
}
}
private string GetLogTypeString(LogType logType)
{
return logType switch
{
LogType.Error => "ERROR",
LogType.Assert => "ASSERT",
LogType.Warning => "WARNING",
LogType.Log => "INFO",
LogType.Exception => "EXCEPTION",
_ => "UNKNOWN"
};
}
private string FormatMessage(object message, Object context = null)
{
string msg = message?.ToString() ?? "null";
if (context != null)
{
msg += $" (Context: {context.name})";
}
return msg;
}
private string FormatMessage(string tag, object message, Object context = null)
{
string msg = $"[{tag}] {message?.ToString() ?? "null"}";
if (context != null)
{
msg += $" (Context: {context.name})";
}
return msg;
}
#region ILogger Implementation
public void Log(LogType logType, object message)
{
WriteToFile(logType, FormatMessage(message));
}
public void Log(LogType logType, object message, Object context)
{
WriteToFile(logType, FormatMessage(message, context));
}
public void Log(LogType logType, string tag, object message)
{
WriteToFile(logType, FormatMessage(tag, message));
}
public void Log(LogType logType, string tag, object message, Object context)
{
WriteToFile(logType, FormatMessage(tag, message, context));
}
public void Log(object message)
{
WriteToFile(LogType.Log, FormatMessage(message));
}
public void Log(string tag, object message)
{
WriteToFile(LogType.Log, FormatMessage(tag, message));
}
public void Log(string tag, object message, Object context)
{
WriteToFile(LogType.Log, FormatMessage(tag, message, context));
}
public void LogWarning(string tag, object message)
{
WriteToFile(LogType.Warning, FormatMessage(tag, message));
}
public void LogWarning(string tag, object message, Object context)
{
WriteToFile(LogType.Warning, FormatMessage(tag, message, context));
}
public void LogError(string tag, object message)
{
WriteToFile(LogType.Error, FormatMessage(tag, message));
}
public void LogError(string tag, object message, Object context)
{
WriteToFile(LogType.Error, FormatMessage(tag, message, context));
}
public void LogFormat(LogType logType, string format, params object[] args)
{
try
{
string message = string.Format(format, args);
WriteToFile(logType, message);
}
catch (FormatException ex)
{
WriteToFile(LogType.Error, $"Ошибка форматирования: {ex.Message}");
}
}
public void LogFormat(LogType logType, Object context, string format, params object[] args)
{
try
{
string message = string.Format(format, args);
WriteToFile(logType, FormatMessage(message, context));
}
catch (FormatException ex)
{
WriteToFile(LogType.Error, $"Ошибка форматирования: {ex.Message}");
}
}
public void LogException(Exception exception)
{
if (exception == null)
return;
WriteToFile(LogType.Exception, $"{exception.GetType().Name}: {exception.Message}");
WriteToFile(LogType.Exception, $"StackTrace:\n{exception.StackTrace}");
if (exception.InnerException != null)
{
WriteToFile(LogType.Exception,
$"Inner Exception: {exception.InnerException.GetType().Name}: {exception.InnerException.Message}");
}
}
public void LogException(Exception exception, Object context)
{
if (exception == null)
return;
string contextInfo = context != null ? $" (Context: {context.name})" : "";
WriteToFile(LogType.Exception, $"{exception.GetType().Name}: {exception.Message}{contextInfo}");
WriteToFile(LogType.Exception, $"StackTrace:\n{exception.StackTrace}");
if (exception.InnerException != null)
{
WriteToFile(LogType.Exception,
$"Inner Exception: {exception.InnerException.GetType().Name}: {exception.InnerException.Message}");
}
}
#endregion
private void OnApplicationQuitting()
{
WriteFooter();
Dispose();
}
private void WriteFooter()
{
if (_footerWritten || _disposed || _writer == null) return;
lock (_lock)
{
if (_footerWritten) return;
_footerWritten = true;
try
{
_writer.WriteLine();
_writer.WriteLine("================================================================================");
_writer.WriteLine($"Сессия логирования завершена: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
_writer.WriteLine("================================================================================");
_writer.Flush();
}
catch (ObjectDisposedException) { }
catch (Exception ex)
{
Debug.LogError($"[FileLogger] Ошибка при записи footer: {ex.Message}");
}
}
}
/// <summary>
/// Принудительно сбрасывает буфер на диск
/// </summary>
public void Flush()
{
lock (_lock)
{
try
{
_writer?.Flush();
}
catch (Exception ex)
{
Debug.LogError($"[FileLogger] Ошибка при сбросе буфера: {ex.Message}");
}
}
}
/// <summary>
/// Очищает файл лога
/// </summary>
public void Clear()
{
lock (_lock)
{
try
{
_writer?.Close();
File.WriteAllText(_filePath, string.Empty);
_writer = new StreamWriter(_filePath, true, Encoding.UTF8) { AutoFlush = true };
WriteHeader();
}
catch (Exception ex)
{
Debug.LogError($"[FileLogger] Ошибка при очистке файла: {ex.Message}");
}
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (_disposed)
return;
lock (_lock)
{
if (_disposed)
return;
try
{
// Гарантированно записываем footer
WriteFooter();
if (disposing)
{
Application.quitting -= OnApplicationQuitting;
}
if (_writer != null)
{
_writer.Dispose();
_writer = null;
}
}
catch (ObjectDisposedException)
{
// Поток уже закрыт — игнорируем.
}
catch (Exception ex)
{
Debug.LogError($"[FileLogger] Ошибка при закрытии файла: {ex.Message}");
}
finally
{
_disposed = true;
}
}
}
~FileLogger()
{
Dispose(false);
}
}
}