Files
A-Fairytale-Gone-Bad-Briar-…/Assets/Scripts/UI/Menus/NewSaveWindow.cs

425 lines
12 KiB
C#

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
{
/// <summary>
/// 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
/// </summary>
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<string> 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<float>
{
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<float>
{
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;
}
}
}
}