425 lines
12 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|
|
} |