using System.IO;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace DanroJump.Bootstrap
{
///
/// Первая точка входа приложения: фиксирует портретное соотношение сторон и загружает главное меню.
///
[DefaultExecutionOrder(-10000)]
public sealed class BootstrapEntryPoint : MonoBehaviour
{
private const float AspectTolerance = 0.0025f;
private static BootstrapEntryPoint instance;
[SerializeField] private string mainMenuSceneName = "MainMenu";
[SerializeField] private bool forceBuildAspectRatio = true;
[SerializeField] [Min(1)] private int targetAspectWidth = 9;
[SerializeField] [Min(1)] private int targetAspectHeight = 16;
[SerializeField] [Min(1)] private int preferredPortraitHeight = 1920;
[SerializeField] private bool clampToDisplaySize = true;
[SerializeField] private FullScreenMode buildFullScreenMode = FullScreenMode.Windowed;
[SerializeField] [Min(1)] private int startupEnforcementFrames = 8;
[SerializeField] [Min(0.1f)] private float aspectCheckInterval = 0.5f;
private float nextAspectCheckTime;
///
/// Создает singleton bootstrap-объект и применяет разрешение билда до загрузки меню.
///
private void Awake()
{
if (instance != null && instance != this)
{
Destroy(gameObject);
return;
}
instance = this;
if (Application.isPlaying)
{
DontDestroyOnLoad(gameObject);
}
// В редакторе не вмешиваемся в Game View, а в билде принудительно держим 9:16.
if (!Application.isEditor && forceBuildAspectRatio)
{
ApplyBuildAspectResolution();
EnforceBuildAspectOnStartupAsync(this.GetCancellationTokenOnDestroy()).Forget(Debug.LogException);
}
}
///
/// Периодически возвращает разрешение к целевому aspect ratio, если окно было изменено.
///
private void Update()
{
if (Application.isEditor || !forceBuildAspectRatio || Time.unscaledTime < nextAspectCheckTime)
{
return;
}
nextAspectCheckTime = Time.unscaledTime + aspectCheckInterval;
if (!HasTargetAspectRatio(Screen.width, Screen.height))
{
ApplyBuildAspectResolution();
}
}
///
/// Загружает сцену главного меню после bootstrap-настройки.
///
private void Start()
{
LoadMainMenuAsync(this.GetCancellationTokenOnDestroy()).Forget(Debug.LogException);
}
private async UniTask LoadMainMenuAsync(CancellationToken cancellationToken)
{
try
{
if (StartupProtectionController.HasInstance)
{
StartupProtectionState protectionState =
await StartupProtectionController.WaitUntilFinishedAsync(cancellationToken);
if (protectionState == StartupProtectionState.Failed)
{
Debug.LogError("[BootstrapEntryPoint] Startup protection failed. Main menu loading was blocked.", this);
return;
}
}
else
{
Debug.LogWarning("[BootstrapEntryPoint] Startup protection controller is missing in Bootstrap scene.", this);
}
StartupWindowFocusController startupWindowFocus =
FindAnyObjectByType();
if (startupWindowFocus != null)
{
await startupWindowFocus.RunStartupFocusAsync(cancellationToken);
}
if (string.IsNullOrWhiteSpace(mainMenuSceneName))
{
Debug.LogError("[BootstrapEntryPoint] Main menu scene name is empty.", this);
return;
}
if (!SceneExistsInBuildSettings(mainMenuSceneName))
{
Debug.LogError($"[BootstrapEntryPoint] Scene '{mainMenuSceneName}' is not registered in Build Settings.", this);
return;
}
AsyncOperation operation = SceneManager.LoadSceneAsync(mainMenuSceneName, LoadSceneMode.Single);
if (operation == null)
{
Debug.LogError($"[BootstrapEntryPoint] Failed to start loading scene '{mainMenuSceneName}'.", this);
return;
}
while (!operation.isDone)
{
await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken);
}
}
catch (System.OperationCanceledException)
{
}
}
///
/// Очищает singleton-ссылку при выходе из Play Mode или уничтожении bootstrap-объекта.
///
private void OnDestroy()
{
if (instance == this)
{
instance = null;
}
}
///
/// Несколько кадров подряд повторяет установку разрешения, пока Unity завершает старт окна.
///
private async UniTask EnforceBuildAspectOnStartupAsync(CancellationToken cancellationToken)
{
try
{
for (int i = 0; i < startupEnforcementFrames; i++)
{
if (!HasTargetAspectRatio(Screen.width, Screen.height))
{
ApplyBuildAspectResolution();
}
await UniTask.Yield(PlayerLoopTiming.Update, cancellationToken);
}
}
catch (System.OperationCanceledException)
{
}
}
///
/// Применяет рассчитанное разрешение к окну игры.
///
private void ApplyBuildAspectResolution()
{
Vector2Int resolution = CalculateTargetResolution();
FullScreenMode mode = Debug.isDebugBuild ? buildFullScreenMode : FullScreenMode.FullScreenWindow;
Screen.SetResolution(resolution.x, resolution.y, mode);
}
///
/// Рассчитывает ближайшее разрешение с целевым aspect ratio, сохраняя пользовательский размер окна.
///
private Vector2Int CalculateTargetResolution()
{
var targetAspect = GetTargetAspect();
var currentWidth = Mathf.Max(1, Screen.width);
var currentHeight = Mathf.Max(1, Screen.height);
var widthByCurrentHeight = Mathf.Max(1, Mathf.RoundToInt(currentHeight * targetAspect));
var heightByCurrentWidth = Mathf.Max(1, Mathf.RoundToInt(currentWidth / targetAspect));
var keepHeightDelta = Mathf.Abs(widthByCurrentHeight - currentWidth);
var keepWidthDelta = Mathf.Abs(heightByCurrentWidth - currentHeight);
var width = currentWidth;
var height = currentHeight;
if (currentWidth <= 1 || currentHeight <= 1)
{
height = Mathf.Max(1, preferredPortraitHeight);
width = Mathf.Max(1, Mathf.RoundToInt(height * targetAspect));
}
else if (keepHeightDelta <= keepWidthDelta)
{
width = widthByCurrentHeight;
}
else
{
height = heightByCurrentWidth;
}
if (clampToDisplaySize && Display.main != null)
{
int displayWidth = Display.main.systemWidth;
int displayHeight = Display.main.systemHeight;
if (displayHeight > 0 && height > displayHeight)
{
height = displayHeight;
width = Mathf.Max(1, Mathf.RoundToInt(height * targetAspect));
}
if (displayWidth > 0 && width > displayWidth)
{
width = displayWidth;
height = Mathf.Max(1, Mathf.RoundToInt(width / targetAspect));
}
}
return new Vector2Int(width, height);
}
///
/// Проверяет, совпадает ли текущее разрешение с целевым соотношением сторон.
///
private bool HasTargetAspectRatio(int width, int height)
{
if (width <= 0 || height <= 0)
{
return false;
}
return Mathf.Abs(width / (float)height - GetTargetAspect()) <= AspectTolerance;
}
///
/// Возвращает целевой aspect ratio из настроек ширины и высоты.
///
private float GetTargetAspect()
{
return targetAspectWidth / (float)targetAspectHeight;
}
///
/// Проверяет, добавлена ли сцена в Build Settings.
///
private static bool SceneExistsInBuildSettings(string sceneName)
{
for (int i = 0; i < SceneManager.sceneCountInBuildSettings; i++)
{
string scenePath = SceneUtility.GetScenePathByBuildIndex(i);
string buildSceneName = Path.GetFileNameWithoutExtension(scenePath);
if (buildSceneName == sceneName)
return true;
}
return false;
}
}
}