using System; using System.IO; using System.Linq; using System.Threading; using BriarQueen.Framework.Managers.IO; using Cysharp.Threading.Tasks; using PrimeTween; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; using VContainer; namespace BriarQueen.UI.Menus { /// /// New Save modal window: /// - Opens over SelectSaveWindow /// - User enters a save name /// - Create -> SaveManager.CreateNewSaveGame(name) (and typically sets CurrentSave) /// - Then immediately LoadGameData(name) (optional but robust), and raises OnSaveCreated /// public class NewSaveWindow : MonoBehaviour { [Header("Root")] [SerializeField] private CanvasGroup _canvasGroup; [Header("Input")] [SerializeField] private TMP_InputField _nameInput; [SerializeField] private Button _createButton; [SerializeField] private Button _cancelButton; [Header("Error UI")] [SerializeField] private GameObject _errorBox; [SerializeField] private TextMeshProUGUI _errorText; [Header("Validation")] [SerializeField] private int _minNameLength = 1; [SerializeField] private int _maxNameLength = 24; [SerializeField] private bool _trimWhitespace = true; [Header("Tween Settings")] [SerializeField] private TweenSettings _tweenSettings = new() { duration = 0.25f, ease = Ease.OutQuad, useUnscaledTime = true }; private CancellationTokenSource _cts; private bool _isBusy; private bool _isOpen; private SaveManager _saveManager; private Sequence _seq; private bool _tutorialsEnabled; private void Awake() { if (_createButton != null) _createButton.onClick.AddListener(OnCreateClicked); if (_cancelButton != null) _cancelButton.onClick.AddListener(Close); HideError(); CloseImmediate(); } private void OnDestroy() { if (_createButton != null) _createButton.onClick.RemoveListener(OnCreateClicked); if (_cancelButton != null) _cancelButton.onClick.RemoveListener(Close); StopTween(); } public event Action OnCloseWindow; public event Action OnSaveCreated; [Inject] public void Construct(SaveManager saveManager) { _saveManager = saveManager; } public void Open() { if (_isOpen) return; Debug.Log($"Opening {nameof(NewSaveWindow)}"); OpenInternal().Forget(); } public void Close() { if (!_isOpen || _isBusy) return; CloseInternal().Forget(); } public void CloseImmediate() { _isOpen = false; _isBusy = false; if (_nameInput != null) _nameInput.text = string.Empty; if (_canvasGroup != null) { _canvasGroup.alpha = 0f; _canvasGroup.interactable = false; _canvasGroup.blocksRaycasts = false; } gameObject.SetActive(false); HideError(); } private async UniTask OpenInternal() { if (_canvasGroup == null) { gameObject.SetActive(true); _isOpen = true; return; } Debug.Log("Opening Internal..."); Debug.Log($"{gameObject} is {gameObject.activeSelf}"); ResetCtsAndCancelRunning(); gameObject.SetActive(true); Debug.Log($"{gameObject} is now {gameObject.activeSelf}"); _canvasGroup.alpha = 0f; _canvasGroup.interactable = false; _canvasGroup.blocksRaycasts = false; _isOpen = true; _isBusy = true; if (_nameInput != null) _nameInput.text = string.Empty; HideError(); Debug.Log("Opening - Creating Sequence"); _seq = Sequence.Create(useUnscaledTime: true) .Group(Tween.Alpha(_canvasGroup, new TweenSettings { startValue = 0f, endValue = 1f, settings = _tweenSettings })); try { Debug.Log("Opening - Sequence Running."); await _seq.ToUniTask(cancellationToken: _cts.Token); } catch (OperationCanceledException) { return; } catch (Exception e) { Debug.Log($"Opening - Sequence Error: {e.Message}"); } finally { Debug.Log("Opening - Sequence over."); _seq = default; } _canvasGroup.alpha = 1f; _canvasGroup.interactable = true; _canvasGroup.blocksRaycasts = true; _isBusy = false; FocusInput(); } private async UniTask CloseInternal() { if (_canvasGroup == null) { CloseImmediate(); OnCloseWindow?.Invoke(); return; } ResetCtsAndCancelRunning(); _isBusy = true; _canvasGroup.interactable = false; _canvasGroup.blocksRaycasts = false; _seq = Sequence.Create(useUnscaledTime: true) .Group(Tween.Alpha(_canvasGroup, new TweenSettings { startValue = _canvasGroup.alpha, endValue = 0f, settings = _tweenSettings })); try { await _seq.ToUniTask(cancellationToken: _cts.Token); } catch (OperationCanceledException) { return; } finally { _seq = default; } _canvasGroup.alpha = 0f; _isOpen = false; _isBusy = false; gameObject.SetActive(false); OnCloseWindow?.Invoke(); } private void FocusInput() { if (_nameInput == null) return; _nameInput.ActivateInputField(); if (EventSystem.current != null) EventSystem.current.SetSelectedGameObject(_nameInput.gameObject); } private void OnCreateClicked() { if (_isBusy) return; CreateSave().Forget(); } private async UniTask CreateSave() { HideError(); if (_saveManager == null) { ShowError("Save system not available."); return; } var raw = _nameInput != null ? _nameInput.text : string.Empty; var name = _trimWhitespace ? (raw ?? string.Empty).Trim() : raw ?? string.Empty; if (string.IsNullOrWhiteSpace(name)) { ShowError("Please enter a save name."); return; } if (name.Length < _minNameLength) { ShowError($"Save name must be at least {_minNameLength} character(s)."); return; } if (_maxNameLength > 0 && name.Length > _maxNameLength) { ShowError($"Save name must be {_maxNameLength} characters or fewer."); return; } if (ContainsIllegalFileNameChars(name, out var illegalChars)) { ShowError(illegalChars.Length == 1 ? $"That name contains an illegal character: '{illegalChars[0]}'." : $"That name contains illegal characters: {string.Join(" ", illegalChars.Select(c => $"'{c}'"))}."); return; } if (IsWindowsReservedFileName(name)) { ShowError("That name is reserved by the operating system. Please choose a different name."); return; } if (_saveManager.DoesSaveExist(name)) { ShowError("A save with that name already exists."); return; } _isBusy = true; SetButtonsInteractable(false); try { await _saveManager.CreateNewSaveGame(name); // Tell SelectSaveWindow to start game. OnSaveCreated?.Invoke(name); // Close ourselves immediately (caller will close SelectSaveWindow) CloseImmediate(); } catch (Exception) { ShowError("Failed to create save. Please try again."); } finally { _isBusy = false; SetButtonsInteractable(true); } } private void SetButtonsInteractable(bool interactable) { if (_createButton != null) _createButton.interactable = interactable; if (_cancelButton != null) _cancelButton.interactable = interactable; if (_nameInput != null) _nameInput.interactable = interactable; } private void ShowError(string message) { if (_errorText != null) _errorText.text = message; if (_errorBox != null) _errorBox.SetActive(true); } private void HideError() { if (_errorBox != null) _errorBox.SetActive(false); if (_errorText != null) _errorText.text = string.Empty; } private static bool ContainsIllegalFileNameChars(string name, out char[] illegalChars) { var invalid = Path.GetInvalidFileNameChars(); illegalChars = name.Where(c => invalid.Contains(c)).Distinct().ToArray(); return illegalChars.Length > 0; } private static bool IsWindowsReservedFileName(string name) { var trimmed = (name ?? string.Empty).Trim().TrimEnd('.', ' '); if (string.IsNullOrEmpty(trimmed)) return true; var upper = trimmed.ToUpperInvariant(); if (upper == "CON" || upper == "PRN" || upper == "AUX" || upper == "NUL") return true; if (upper.Length == 4) { if (upper.StartsWith("COM") && char.IsDigit(upper[3]) && upper[3] != '0') return true; if (upper.StartsWith("LPT") && char.IsDigit(upper[3]) && upper[3] != '0') return true; } return false; } private void ResetCtsAndCancelRunning() { if (_seq.isAlive) { _seq.Stop(); _seq = default; } if (_cts != null) { try { _cts.Cancel(); } catch { } _cts.Dispose(); _cts = null; } _cts = new CancellationTokenSource(); } private void StopTween() { if (_seq.isAlive) _seq.Stop(); _seq = default; if (_cts != null) { try { _cts.Cancel(); } catch { } _cts.Dispose(); _cts = null; } } } }