First commit for private source control. Older commits available on Github.
This commit is contained in:
425
Assets/Scripts/UI/Menus/NewSaveWindow.cs
Normal file
425
Assets/Scripts/UI/Menus/NewSaveWindow.cs
Normal file
@@ -0,0 +1,425 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user