UI/PauseMenu.razor
@using Sandbox.UI
@namespace sGBA
@inherits PanelComponent
<root class="@(IsVisible ? "visible" : "") @(_showConfirmDialog ? "confirm-open" : "") @(_showSavingDialog ? "saving-open" : "") @(InNetworkedSession ? "networked" : "")">
<div class="overlay">
<div class="menu-container">
<div class="menu-left @(_inSlotPanel ? "in-slots" : "")">
@for (int i = 0; i < ActiveMenuItems.Length; i++)
{
var idx = i;
<div class="menu-item @(idx == _selectedIndex ? "selected" : "")" onmouseenter=@(() => SelectItem(idx)) onclick=@(() => ActivateItemWithMouse(idx))>
@ActiveMenuItems[idx]
</div>
}
</div>
<div class="menu-divider @(HasSlotPanel ? "visible" : "")"></div>
<div class="menu-right @(HasSlotPanel ? "visible" : "")">
@for (int i = 1; i <= GbaSerialize.SlotCount; i++)
{
var slot = i;
<div class="slot @(slot == _highlightedSlot && HasSlotPanel ? "highlighted" : "")" onmouseenter=@(() => SelectSlotWithMouse(slot)) onclick=@(() => ActivateSlotWithMouse(slot))>
<div class="slot-preview">
@if (SlotTextures[slot - 1] != null)
{
<Image Texture=@SlotTextures[slot - 1] />
}
else
{
<span>#pause.slot.empty</span>
}
</div>
<div class="slot-info">
<div class="slot-label"><span>#pause.slot.label</span><span>@slot</span></div>
@if (SlotTimestamps[slot - 1] != null)
{
<div class="slot-timestamp">@SlotTimestamps[slot - 1].Value.ToString("dd/MM/yyyy - HH:mm")</div>
}
</div>
</div>
}
</div>
</div>
</div>
<div class="dialog-overlay @(_showConfirmDialog ? "visible" : "")">
<div class="confirm-box">
<div class="dialog-message">#pause.confirm.message</div>
<div class="confirm-buttons">
<div class="confirm-btn @(_confirmSelection == 0 ? "selected" : "")" onmouseenter=@(() => SelectConfirmWithMouse(0)) onclick=@(() => ConfirmOverwriteWithMouse())>#pause.confirm.yes</div>
<div class="confirm-btn @(_confirmSelection == 1 ? "selected" : "")" onmouseenter=@(() => SelectConfirmWithMouse(1)) onclick=@(() => CancelOverwriteWithMouse())>#pause.confirm.no</div>
</div>
</div>
</div>
<div class="dialog-overlay @(_showSavingDialog ? "visible" : "")">
<div class="dialog-box">
<div class="dialog-message">#pause.saving</div>
</div>
</div>
</root>
@code
{
private const int MenuContinue = 0;
private const int MenuLoad = 1;
private const int MenuCreate = 2;
private const int MenuControls = 3;
private const int MenuReset = 4;
private const int MenuGoToHome = 5;
private readonly string[] MenuItems =
[
"#pause.continue",
"#pause.load",
"#pause.create",
"#pause.controls",
"#pause.reset",
"#pause.home"
];
private static readonly string[] ClientMenuItems =
[
"#pause.continue",
"#pause.controls",
"#pause.home"
];
private static bool InNetworkedSession => NetworkManager.Current?.IsActive == true;
private string[] ActiveMenuItems => InNetworkedSession ? ClientMenuItems : MenuItems;
public bool IsVisible { get; private set; }
private int _selectedIndex;
private string SelectedKey => _selectedIndex >= 0 && _selectedIndex < ActiveMenuItems.Length ? ActiveMenuItems[_selectedIndex] : null;
private bool HasSlotPanel => SelectedKey is "#pause.load" or "#pause.create";
private bool IsLoadMode => SelectedKey == "#pause.load";
private bool _inSlotPanel;
private int _highlightedSlot = 1;
private readonly Texture[] SlotTextures = new Texture[GbaSerialize.SlotCount];
private readonly bool[] SlotOccupied = new bool[GbaSerialize.SlotCount];
private readonly DateTime?[] SlotTimestamps = new DateTime?[GbaSerialize.SlotCount];
private int _slotVersion;
private bool _showConfirmDialog;
private int _confirmSelection;
private int _pendingOverwriteSlot;
private bool _showSavingDialog;
private readonly FocusInput _input = new();
protected override void OnUpdate()
{
if (!EmulatorComponent.Current.IsValid() || !EmulatorComponent.Current.IsReady)
return;
if (Input.EscapePressed)
{
Input.EscapePressed = false;
if (IsVisible && !new Game.Overlay().IsOpen)
Resume();
else if (!IsVisible)
Pause();
return;
}
if (!IsVisible) return;
if (new Game.Overlay().IsOpen) return;
if (_showConfirmDialog)
{
HandleConfirmInput();
return;
}
if (_showSavingDialog) return;
var nav = _input.TickRepeating();
if (nav.Up) NavigateUp();
if (nav.Down) NavigateDown();
if (nav.Right)
{
if (!_inSlotPanel && HasSlotPanel)
EnterSlotPanel();
}
if (nav.Left)
{
if (_inSlotPanel)
ExitSlotPanel();
}
if (Input.Pressed("GBA_A"))
{
SetGamepadMode();
Sound.Play("ui.button.press");
if (_inSlotPanel)
ActivateSlot(_highlightedSlot);
else if (HasSlotPanel)
EnterSlotPanel();
else
ActivateItem(_selectedIndex);
}
if (Input.Pressed("GBA_B"))
{
SetGamepadMode();
Sound.Play("ui.button.press");
if (_inSlotPanel)
ExitSlotPanel();
else
Resume();
}
}
private void SetGamepadMode() => _input.ForceGamepadMode();
private void NavigateUp()
{
SetGamepadMode();
Sound.Play("ui.button.over");
if (_inSlotPanel)
_highlightedSlot = _highlightedSlot <= 1 ? GbaSerialize.SlotCount : _highlightedSlot - 1;
else
_selectedIndex = _selectedIndex <= 0 ? ActiveMenuItems.Length - 1 : _selectedIndex - 1;
}
private void NavigateDown()
{
SetGamepadMode();
Sound.Play("ui.button.over");
if (_inSlotPanel)
_highlightedSlot = _highlightedSlot >= GbaSerialize.SlotCount ? 1 : _highlightedSlot + 1;
else
_selectedIndex = _selectedIndex >= ActiveMenuItems.Length - 1 ? 0 : _selectedIndex + 1;
}
private void EnterSlotPanel()
{
SetGamepadMode();
Sound.Play("ui.button.press");
_inSlotPanel = true;
_highlightedSlot = 1;
}
private void ExitSlotPanel()
{
SetGamepadMode();
Sound.Play("ui.button.press");
_inSlotPanel = false;
_selectedIndex = MenuContinue;
}
private void Pause()
{
IsVisible = true;
_selectedIndex = 0;
_inSlotPanel = false;
_highlightedSlot = 1;
_input.Begin(useGamepad: true);
Sound.Play("ui.popup.message.open");
EmulatorComponent.Current.SetPaused(true);
RefreshSlotPreviews();
}
private void Resume()
{
IsVisible = false;
_selectedIndex = MenuContinue;
_inSlotPanel = false;
_input.End();
Mouse.Visibility = MouseVisibility.Hidden;
Sound.Play("ui.popup.message.close");
EmulatorComponent.Current.SetPaused(false);
}
private void SelectItem(int index)
{
_input.ForceMouseMode();
_selectedIndex = index;
_inSlotPanel = false;
if (HasSlotPanel)
_highlightedSlot = 1;
}
private void SelectSlotWithMouse(int slot)
{
_input.ForceMouseMode();
_highlightedSlot = slot;
}
private void ActivateSlotWithMouse(int slot)
{
_input.ForceMouseMode();
ActivateSlot(slot);
}
private void SelectConfirmWithMouse(int index)
{
_input.ForceMouseMode();
_confirmSelection = index;
}
private void ConfirmOverwriteWithMouse()
{
_input.ForceMouseMode();
ConfirmOverwrite();
}
private void CancelOverwriteWithMouse()
{
_input.ForceMouseMode();
CancelOverwrite();
}
private void ActivateItem(int index)
{
if (index < 0 || index >= ActiveMenuItems.Length) return;
switch (ActiveMenuItems[index])
{
case "#pause.continue":
Resume();
break;
case "#pause.load":
case "#pause.create":
SelectItem(index);
_inSlotPanel = true;
break;
case "#pause.controls":
Game.Overlay.ShowBinds();
break;
case "#pause.reset":
EmulatorComponent.Current.ResetEmulator();
Resume();
break;
case "#pause.home":
IsVisible = false;
_selectedIndex = MenuContinue;
_inSlotPanel = false;
Mouse.Visibility = MouseVisibility.Hidden;
Sound.Play("ui.popup.message.close");
NetworkManager.Current?.Leave();
HomeScreen.Current?.Show();
break;
}
}
private void ActivateItemWithMouse(int index)
{
_input.ForceMouseMode();
ActivateItem(index);
}
private void ActivateSlot(int slot)
{
if (IsLoadMode)
{
if (!SlotOccupied[slot - 1])
return;
EmulatorComponent.Current.LoadSuspendPoint(slot);
Resume();
}
else
{
if (SlotOccupied[slot - 1])
{
_pendingOverwriteSlot = slot;
_confirmSelection = 0;
_showConfirmDialog = true;
Sound.Play("ui.popup.message.open");
return;
}
DoSave(slot);
}
}
private void HandleConfirmInput()
{
var nav = _input.TickRepeating();
if (nav.Up || nav.Down)
{
SetGamepadMode();
Sound.Play("ui.button.over");
_confirmSelection = _confirmSelection == 0 ? 1 : 0;
}
if (Input.Pressed("GBA_A"))
{
SetGamepadMode();
Sound.Play("ui.button.press");
if (_confirmSelection == 0)
ConfirmOverwrite();
else
CancelOverwrite();
}
if (Input.Pressed("GBA_B"))
{
SetGamepadMode();
Sound.Play("ui.button.press");
CancelOverwrite();
}
}
private void ConfirmOverwrite()
{
_showConfirmDialog = false;
DoSave(_pendingOverwriteSlot);
}
private void CancelOverwrite()
{
_showConfirmDialog = false;
Sound.Play("ui.popup.message.close");
}
private async void DoSave(int slot)
{
_showSavingDialog = true;
await Task.Delay(100);
EmulatorComponent.Current.CreateSuspendPoint(slot);
RefreshSlotPreviews();
await Task.Delay(500);
_showSavingDialog = false;
}
private void RefreshSlotPreviews()
{
var emu = EmulatorComponent.Current;
if (!emu.IsValid()) return;
for (int i = 0; i < GbaSerialize.SlotCount; i++)
{
var path = emu.GetStatePath(i + 1);
if (!FileSystem.Data.FileExists(path))
{
ClearSlot(i);
continue;
}
var data = FileSystem.Data.ReadAllBytes(path).ToArray();
SlotOccupied[i] = true;
SlotTimestamps[i] = GbaSerialize.ReadTimestamp(data);
var screenshot = GbaSerialize.ReadScreenshot(data);
if (screenshot == null) continue;
if (SlotTextures[i] == null)
{
SlotTextures[i] = Texture.Create(GbaConstants.ScreenWidth, GbaConstants.ScreenHeight, ImageFormat.RGBA8888)
.WithDynamicUsage()
.WithName($"suspend-preview-{i}")
.Finish();
}
SlotTextures[i].Update(screenshot, 0, 0, GbaConstants.ScreenWidth, GbaConstants.ScreenHeight);
}
_slotVersion++;
}
private void ClearSlot(int index)
{
SlotOccupied[index] = false;
SlotTimestamps[index] = null;
SlotTextures[index]?.Dispose();
SlotTextures[index] = null;
}
protected override int BuildHash()
{
return HashCode.Combine(
IsVisible, _selectedIndex, _inSlotPanel, _highlightedSlot,
_input.UseGamepad, _slotVersion, _showConfirmDialog,
HashCode.Combine(_confirmSelection, _showSavingDialog, InNetworkedSession)
);
}
protected override void OnDestroy()
{
for (int i = 0; i < SlotTextures.Length; i++)
{
SlotTextures[i]?.Dispose();
SlotTextures[i] = null;
}
}
}