MatchResultScreen becomes a self-contained modal (IsOpen/Open/Close, _panelRoot, pause handling); resolution auto-open moves there from the session browser. OverviewMenu integrates it into the Esc chain and hides the Match Results button until results exist (same pattern as the role-scoped mode buttons). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
309 lines
12 KiB
C#
309 lines
12 KiB
C#
using System.Collections.Generic;
|
|
using TMPro;
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
using UnityEngine.UI;
|
|
|
|
/// <summary>
|
|
/// uGUI binding for <see cref="OnlineSessionManager"/>: Host / Refresh / row select / Join / Disconnect
|
|
/// plus a status line, mirroring the manager through its <see cref="OnlineSessionManager.Changed"/> /
|
|
/// <see cref="OnlineSessionManager.SessionsChanged"/> events; the session logic stays on the manager.
|
|
///
|
|
/// The panel is always shown in Main (not connected); once a connection is up it hides, and in the
|
|
/// lobby/map the overview (Esc) menu reopens it via <see cref="Open"/> as a modal pause overlay
|
|
/// (cursor freed, gameplay suspended via <see cref="GameModeManager.SetPaused"/>) - all Esc key
|
|
/// handling lives in <see cref="OverviewMenu"/>, which closes this overlay through <see cref="Close"/>.
|
|
/// The Disconnect button is context-sensitive: in Main it is an
|
|
/// application "Exit"; on the map it becomes "Back to Lobby" for the match authority (ends the match);
|
|
/// everyone else gets a plain disconnect. Only the visual <see cref="panelRoot"/> is toggled - this binder
|
|
/// stays alive on its (always-active) canvas so it keeps tracking state while the panel is hidden.
|
|
/// </summary>
|
|
public class OnlineSessionBrowserUI : MonoBehaviour
|
|
{
|
|
[SerializeField] private OnlineSessionManager manager;
|
|
|
|
[Header("Buttons")]
|
|
[SerializeField] private Button hostButton;
|
|
[SerializeField] private Button refreshButton;
|
|
[SerializeField] private Button joinButton;
|
|
[SerializeField] private Button disconnectButton;
|
|
|
|
[Header("List")]
|
|
[Tooltip("Content RectTransform of the session ScrollRect (rows are added here).")]
|
|
[SerializeField] private RectTransform listContent;
|
|
[Tooltip("A row prototype kept disabled; cloned once per session. Children named 'Name' and 'Players' carry the text.")]
|
|
[SerializeField] private GameObject entryTemplate;
|
|
|
|
[Header("Status")]
|
|
[SerializeField] private TMP_Text statusLabel;
|
|
|
|
[Header("Overlay")]
|
|
[Tooltip("Visual container toggled while connected (the panel root). Must be a CHILD of this " +
|
|
"binder's object so the binder keeps running while the panel is hidden.")]
|
|
[SerializeField] private GameObject panelRoot;
|
|
|
|
[Tooltip("Background tint of the selected row.")]
|
|
[SerializeField] private Color selectedTint = new Color(0.26f, 0.52f, 0.96f, 1f);
|
|
[Tooltip("Background tint of an unselected row.")]
|
|
[SerializeField] private Color normalTint = new Color(1f, 1f, 1f, 0.05f);
|
|
|
|
private struct Row { public string id; public Image image; public GameObject go; }
|
|
private readonly List<Row> _rows = new List<Row>();
|
|
private string _selectedId;
|
|
|
|
private MatchManager _match; // local match state (scene kind + authority); found at runtime
|
|
private GameModeManager _gameMode; // pause hook for the modal overlay; found at runtime
|
|
private TMP_Text _disconnectLabel; // disconnect button caption (relabelled to "Back to Lobby")
|
|
|
|
private bool _open; // overlay visibility while connected (toggled via Open/Close)
|
|
private bool _connected; // last-seen connection state (to detect transitions)
|
|
private Scene _lastScene; // last-seen active scene (to auto-close on a scene change)
|
|
private string _disconnectText; // last applied caption (avoids per-frame TMP churn)
|
|
|
|
private void Awake()
|
|
{
|
|
// The template is the prototype, never shown itself - layout ignores it while disabled.
|
|
if (entryTemplate) entryTemplate.SetActive(false);
|
|
|
|
_match = FindFirstObjectByType<MatchManager>();
|
|
_gameMode = FindFirstObjectByType<GameModeManager>();
|
|
}
|
|
|
|
private void OnEnable()
|
|
{
|
|
if (hostButton) hostButton.onClick.AddListener(OnHost);
|
|
if (refreshButton) refreshButton.onClick.AddListener(OnRefresh);
|
|
if (joinButton) joinButton.onClick.AddListener(OnJoin);
|
|
if (disconnectButton)
|
|
{
|
|
disconnectButton.onClick.AddListener(OnDisconnect);
|
|
_disconnectLabel = disconnectButton.GetComponentInChildren<TMP_Text>(true);
|
|
}
|
|
|
|
if (manager)
|
|
{
|
|
manager.Changed += OnManagerChanged;
|
|
manager.SessionsChanged += RebuildList;
|
|
}
|
|
|
|
_connected = manager && manager.IsConnected;
|
|
RebuildList();
|
|
OnManagerChanged();
|
|
ApplyVisibility();
|
|
}
|
|
|
|
private void OnDisable()
|
|
{
|
|
if (hostButton) hostButton.onClick.RemoveListener(OnHost);
|
|
if (refreshButton) refreshButton.onClick.RemoveListener(OnRefresh);
|
|
if (joinButton) joinButton.onClick.RemoveListener(OnJoin);
|
|
if (disconnectButton) disconnectButton.onClick.RemoveListener(OnDisconnect);
|
|
|
|
if (manager)
|
|
{
|
|
manager.Changed -= OnManagerChanged;
|
|
manager.SessionsChanged -= RebuildList;
|
|
}
|
|
}
|
|
|
|
private void Update()
|
|
{
|
|
if (!manager) return;
|
|
bool connected = manager.IsConnected;
|
|
|
|
// Auto-close the overlay if the scene changed underneath it (a remote ended/started the match, or
|
|
// we walked into Start Match) so it never lingers as a stale pause across a transition.
|
|
// Compared as Scene structs (handles), not names, so nothing is allocated per frame.
|
|
Scene scene = SceneManager.GetActiveScene();
|
|
if (connected && _open && _lastScene.IsValid() && scene != _lastScene)
|
|
SetOpen(false);
|
|
_lastScene = scene;
|
|
|
|
if (!connected) return; // Main: panel always shown
|
|
|
|
RefreshDisconnectLabel();
|
|
}
|
|
|
|
/// <summary>True while the modal overlay is open on a live connection (the always-shown Main panel
|
|
/// does not count). The overview menu yields Esc to this overlay while it is open.</summary>
|
|
public bool IsOpen => _open;
|
|
|
|
/// <summary>Opens the modal overlay (used by the overview menu's Session button); false when
|
|
/// not connected - in Main the panel is always visible and never a pause overlay.</summary>
|
|
public bool Open()
|
|
{
|
|
if (!(manager && manager.IsConnected)) return false;
|
|
SetOpen(true);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>Closes the modal overlay and resumes gameplay (Esc handling in <see cref="OverviewMenu"/>).</summary>
|
|
public void Close() => SetOpen(false);
|
|
|
|
private void OnHost() { if (manager) manager.Host(); }
|
|
private void OnRefresh() { if (manager) manager.Refresh(); }
|
|
private void OnJoin() { if (manager && !string.IsNullOrEmpty(_selectedId)) manager.Join(_selectedId); }
|
|
|
|
private void OnDisconnect()
|
|
{
|
|
if (!manager) return;
|
|
|
|
// In Main (no session) the button is an application Exit, so the player can quit the build
|
|
// without Alt+F4.
|
|
if (!manager.IsConnected)
|
|
{
|
|
QuitApplication();
|
|
return;
|
|
}
|
|
|
|
// On the map, the match AUTHORITY's button means "Back to Lobby" (end the match for everyone)
|
|
// rather than leaving the session. Everyone else - in the lobby, or a non-authority client on the
|
|
// map - does a plain disconnect.
|
|
if (_match && _match.IsLocalSceneMap && _match.IsLocalMatchAuthority)
|
|
{
|
|
_match.RequestEndMatch();
|
|
SetOpen(false); // heading back to the lobby; close the overlay + resume input
|
|
return;
|
|
}
|
|
|
|
manager.Disconnect();
|
|
}
|
|
|
|
private static void QuitApplication()
|
|
{
|
|
#if UNITY_EDITOR
|
|
UnityEditor.EditorApplication.isPlaying = false;
|
|
#else
|
|
Application.Quit();
|
|
#endif
|
|
}
|
|
|
|
private void OnManagerChanged()
|
|
{
|
|
if (!manager) return;
|
|
|
|
bool connected = manager.IsConnected;
|
|
if (connected != _connected)
|
|
{
|
|
_connected = connected;
|
|
// Connection up (hosted/joined) -> hide the panel; Esc reopens it in the lobby/map.
|
|
// Back to Main (disconnect/shutdown) -> show it again and drop any pause.
|
|
_open = false;
|
|
if (!connected && _gameMode) _gameMode.SetPaused(false);
|
|
ApplyVisibility();
|
|
}
|
|
|
|
if (statusLabel) statusLabel.text = manager.Status;
|
|
UpdateButtons();
|
|
RefreshDisconnectLabel();
|
|
}
|
|
|
|
private void SetOpen(bool open)
|
|
{
|
|
_open = open;
|
|
ApplyVisibility();
|
|
if (_gameMode) _gameMode.SetPaused(open);
|
|
}
|
|
|
|
private void ApplyVisibility()
|
|
{
|
|
bool show = !(manager && manager.IsConnected) || _open;
|
|
if (panelRoot && panelRoot.activeSelf != show)
|
|
panelRoot.SetActive(show);
|
|
}
|
|
|
|
private void RefreshDisconnectLabel()
|
|
{
|
|
if (!_disconnectLabel) return;
|
|
string text;
|
|
if (!(manager && manager.IsConnected))
|
|
text = "Exit"; // Main: quit the application
|
|
else if (_match && _match.IsLocalSceneMap && _match.IsLocalMatchAuthority)
|
|
text = "Back to Lobby";
|
|
else
|
|
text = "Disconnect";
|
|
if (text != _disconnectText)
|
|
{
|
|
_disconnectText = text;
|
|
_disconnectLabel.text = text;
|
|
}
|
|
}
|
|
|
|
private void UpdateButtons()
|
|
{
|
|
if (!manager) return;
|
|
bool busy = manager.IsBusy;
|
|
bool connected = manager.IsConnected;
|
|
bool hasSelection = !string.IsNullOrEmpty(_selectedId);
|
|
|
|
if (hostButton) hostButton.interactable = !busy && !connected;
|
|
if (refreshButton) refreshButton.interactable = !busy && !connected;
|
|
if (joinButton) joinButton.interactable = !busy && !connected && hasSelection;
|
|
// Usable whenever idle: connected -> Disconnect / Back to Lobby; in Main -> Exit.
|
|
if (disconnectButton) disconnectButton.interactable = !busy;
|
|
}
|
|
|
|
private void RebuildList()
|
|
{
|
|
for (int i = 0; i < _rows.Count; i++)
|
|
if (_rows[i].go) Destroy(_rows[i].go);
|
|
_rows.Clear();
|
|
|
|
if (manager && entryTemplate && listContent)
|
|
{
|
|
var sessions = manager.Sessions;
|
|
bool selectionStillPresent = false;
|
|
|
|
for (int i = 0; i < sessions.Count; i++)
|
|
{
|
|
var s = sessions[i];
|
|
var go = Instantiate(entryTemplate, listContent);
|
|
go.SetActive(true);
|
|
|
|
var nameText = FindText(go.transform, "Name");
|
|
if (nameText) nameText.text = s.Name;
|
|
|
|
var playersText = FindText(go.transform, "Players");
|
|
if (playersText)
|
|
{
|
|
int taken = Mathf.Max(0, s.MaxPlayers - s.AvailableSlots);
|
|
playersText.text = $"{taken}/{s.MaxPlayers}";
|
|
}
|
|
|
|
string id = s.Id; // capture per-row for the click closure
|
|
var button = go.GetComponent<Button>();
|
|
if (button) button.onClick.AddListener(() => Select(id));
|
|
|
|
_rows.Add(new Row { id = id, image = go.GetComponent<Image>(), go = go });
|
|
if (id == _selectedId) selectionStillPresent = true;
|
|
}
|
|
|
|
if (!selectionStillPresent) _selectedId = null;
|
|
}
|
|
|
|
ApplyTints();
|
|
UpdateButtons();
|
|
}
|
|
|
|
private void Select(string id)
|
|
{
|
|
_selectedId = id;
|
|
ApplyTints();
|
|
UpdateButtons();
|
|
}
|
|
|
|
private void ApplyTints()
|
|
{
|
|
for (int i = 0; i < _rows.Count; i++)
|
|
if (_rows[i].image)
|
|
_rows[i].image.color = _rows[i].id == _selectedId ? selectedTint : normalTint;
|
|
}
|
|
|
|
private static TMP_Text FindText(Transform root, string childName)
|
|
{
|
|
var t = root.Find(childName);
|
|
return t ? t.GetComponent<TMP_Text>() : null;
|
|
}
|
|
|
|
}
|