UI/RoomBrowser.cs
using System;
using System.Collections.Generic;
using Sandbox.Network;
using Sandbox.UI;

/// <summary>
/// 房间列表(v0.6.7.0 用户需求):主菜单 [2] JOIN GAME 打开——列出 Steam 上本游戏的公开大厅,
/// 点击行加入;REFRESH 手动刷新(显示中每 10s 自动刷一次);JOIN LOCAL 同机调试直连兜底
/// (双开时大厅查询搜不到同账号房间,靠这条进);BACK 回主菜单。
/// 纯 C# Panel + 同目录 RoomBrowser.cs.scss(显隐走 Style.Display,与 LobbyPanel 同款)。
/// </summary>
public sealed class RoomBrowser : PanelComponent
{
	Label _status;
	Label[] _rows;
	Label _refresh;
	Label _local;
	Label _back;

	readonly List<LobbyInformation> _lobbies = new();
	bool _built;
	bool _shown;
	bool _joining;          // 已点了一个房间正在连(防重复点击;成功由 Tick 加入流收尾)
	bool _retryOpen;        // EnsureWorld 先于 OnStart 调 Show 时重试
	bool _querying;
	TimeSince _sinceQuery = 999f;

	const int RowCount = 10;

	protected override void OnStart()
	{
		base.OnStart();
		BuildUi();
		Hide();
	}

	void BuildUi()
	{
		var root = Panel;
		if ( root is null ) return;
		if ( _built && _status.IsValid() && root.ChildrenCount > 0 ) return;

		root.DeleteChildren( true );

		root.Style.Position = PositionMode.Absolute;
		root.Style.Left = 0f;
		root.Style.Top = 0f;
		root.Style.Right = 0f;
		root.Style.Bottom = 0f;
		root.Style.BackgroundColor = new Color( 0.008f, 0.012f, 0.05f, 0.55f );
		root.Style.PointerEvents = PointerEvents.All;

		var title = new Label() { Classes = "rb-title" };
		title.Text = "ROOM BROWSER";
		root.AddChild( title );

		_status = new Label() { Classes = "rb-status" };
		root.AddChild( _status );

		var head = new Label() { Classes = "rb-head" };
		head.Text = "ROOM                          PLAYERS";
		root.AddChild( head );

		_rows = new Label[RowCount];
		for ( int i = 0; i < _rows.Length; i++ )
		{
			var row = new Label() { Classes = "rb-row" };
			row.Style.Position = PositionMode.Absolute;
			row.Style.Left = 460f;
			row.Style.Width = 1000f;
			row.Style.Top = 300f + i * 36f;
			int index = i;
			// lambda 捕获 this(实例方法)——无捕获静态 lambda 热重载后无法重映射(实测点击死锁)
			row.AddEventListener( "onclick", () => OnRowClicked( index ) );
			root.AddChild( row );
			_rows[i] = row;
		}

		_refresh = new Label() { Classes = "rb-refresh" };
		_refresh.Text = "REFRESH";
		_refresh.AddEventListener( "onclick", () => OnRefreshClicked() );
		root.AddChild( _refresh );

		_local = new Label() { Classes = "rb-local" };
		_local.Text = "JOIN LOCAL — SAME MACHINE";
		_local.AddEventListener( "onclick", () => OnLocalClicked() );
		root.AddChild( _local );

		_back = new Label() { Classes = "rb-back" };
		_back.Text = "BACK — [4]";
		_back.AddEventListener( "onclick", () => OnBackClicked() );
		root.AddChild( _back );

		_built = true;
	}

	/// <summary> 主菜单 [2] 进入(CircleroyaleGame.MenuAction 调用):开门即查询 </summary>
	public void Show()
	{
		BuildUi();
		if ( !_built )
		{
			_retryOpen = true;   // Panel 未就绪(EnsureWorld 同帧调用),OnUpdate 补开
			return;
		}

		_retryOpen = false;
		_shown = true;
		_joining = false;
		Panel.Style.Display = DisplayMode.Flex;
		_sinceQuery = 999f;   // 首帧立即查询
		SetStatus( "" );
		RenderRows();
	}

	/// <summary> 收起(加入成功 / BACK / 回菜单) </summary>
	public void Hide()
	{
		_shown = false;
		_retryOpen = false;
		_joining = false;
		if ( Panel.IsValid() ) Panel.Style.Display = DisplayMode.None;
	}

	/// <summary> 状态行(加入失败/超时提示也走这里) </summary>
	public void SetStatus( string text )
	{
		if ( _status.IsValid() ) _status.Text = text;
	}

	/// <summary> 页面是否开着(ReconcileUi 终态判定用:开着时主菜单让位) </summary>
	public bool IsOpen => _shown;

	protected override void OnUpdate()
	{
		base.OnUpdate();

		BuildUi();

		if ( _retryOpen && !_shown )
		{
			Show();
			return;
		}

		if ( !_shown ) return;

		if ( Panel.IsValid() ) Panel.Style.Display = DisplayMode.Flex;
		Mouse.Visibility = MouseVisibility.Visible;   // 行/按钮要能点

		// 显示中每 10s 自动刷新一次房间列表(手动 REFRESH 也可)
		if ( !_querying && _sinceQuery > 10f ) Refresh();

		if ( Input.Pressed( "Slot4" ) ) OnBackClicked();
	}

	/// <summary> 查询 Steam 大厅(异步;回来时若已关闭则丢弃) </summary>
	async void Refresh()
	{
		if ( _querying ) return;

		_querying = true;
		_sinceQuery = 0;
		SetStatus( "FINDING ROOMS ..." );

		try
		{
			var lobbies = await Networking.QueryLobbies();

			if ( !_shown ) return;   // 查询回来时页面已经关了

			// 人数多的排前面(满员也列出但置灰不可点)
			_lobbies.Clear();
			_lobbies.AddRange( lobbies );
			_lobbies.Sort( ( a, b ) => b.Members.CompareTo( a.Members ) );

			if ( _lobbies.Count == 0 )
				SetStatus( "NO OPEN ROOMS — HOST ONE FROM ANOTHER MACHINE, OR JOIN LOCAL BELOW" );
			else
				SetStatus( $"{_lobbies.Count} ROOM{(_lobbies.Count == 1 ? "" : "S")} FOUND — CLICK TO JOIN" );

			RenderRows();
		}
		catch ( Exception e )
		{
			Log.Warning( $"[net] room list query failed: {e.Message}" );
			if ( _shown ) SetStatus( "QUERY FAILED — TRY REFRESH" );
		}
		finally
		{
			_querying = false;
		}
	}

	void RenderRows()
	{
		if ( _rows is null || _rows[0] is null || !_rows[0].IsValid() ) return;

		for ( int i = 0; i < _rows.Length; i++ )
		{
			var row = _rows[i];
			if ( !row.IsValid() ) continue;

			if ( i >= _lobbies.Count )
			{
				row.Text = "";
				continue;
			}

			var l = _lobbies[i];
			var name = string.IsNullOrWhiteSpace( l.Name ) ? "CIRCLEROYALE ROOM" : l.Name;
			row.Text = $"{name}   —   {l.Members}/{l.MaxMembers}{( l.IsFull ? "  (FULL)" : "" )}";
			row.Style.FontColor = l.IsFull
				? new Color( 1f, 1f, 1f, 0.25f )
				: new Color( 1f, 1f, 1f, 0.8f );
		}
	}

	void OnRowClicked( int index )
	{
		if ( !_shown || _joining ) return;
		if ( index >= _lobbies.Count ) return;

		var lobby = _lobbies[index];
		if ( lobby.IsFull )
		{
			SetStatus( "THAT ROOM IS FULL — PICK ANOTHER" );
			return;
		}

		_joining = true;
		SetStatus( $"JOINING '{( string.IsNullOrWhiteSpace( lobby.Name ) ? "ROOM" : lobby.Name )}' ..." );
		NetworkManager.Instance?.JoinLobby( lobby );
	}

	/// <summary> 手动刷新 </summary>
	void OnRefreshClicked()
	{
		if ( !_shown || _joining ) return;
		Refresh();
	}

	/// <summary> 同机调试直连(双开搜不到同账号大厅,走这条) </summary>
	void OnLocalClicked()
	{
		if ( !_shown || _joining ) return;

		_joining = true;
		SetStatus( "CONNECTING TO LOCAL HOST ..." );
		CircleroyaleGame.Current?.JoinLocalRoom();
	}

	/// <summary> 回主菜单 </summary>
	void OnBackClicked()
	{
		if ( !_shown ) return;
		CircleroyaleGame.Current?.CloseBrowser();
	}
}