11using System ;
2+ using System . Collections . Generic ;
3+ using System . Linq ;
4+ using System . Net ;
25using System . Threading ;
36using System . Threading . Tasks ;
7+ using ServerBrowser . Models ;
48using ServerBrowser . Models . Requests ;
59using ServerBrowser . Models . Responses ;
10+ using ServerBrowser . Network . Discovery ;
611using SiraUtil . Logging ;
12+ using UnityEngine ;
713using Zenject ;
814
915namespace ServerBrowser . Core
@@ -12,98 +18,186 @@ namespace ServerBrowser.Core
1218 /// Utility for browsing paginated lobbies on the Server Browser API.
1319 /// </summary>
1420 // ReSharper disable once ClassNeverInstantiated.Global
15- public class BssbBrowser
21+ public class BssbBrowser : IInitializable , IDisposable
1622 {
17- public const int DefaultPageSize = 6 ;
18-
1923 [ Inject ] private readonly SiraLog _log = null ! ;
2024 [ Inject ] private readonly BssbApiClient _apiClient = null ! ;
21-
22- public BrowseQueryParams QueryParams = new ( ) ;
23- public BrowseResponse ? PageData { get ; private set ; }
24-
25+ [ Inject ] private readonly DiscoveryClient _discoveryClient = null ! ;
26+
2527 /// <summary>
2628 /// This event is raised when loading a page has finished or failed.
2729 /// </summary>
2830 public event EventHandler ? UpdateEvent ;
29-
30- private CancellationTokenSource ? _loadingCts ;
31- private int _pageIndex ;
31+
32+ public BrowseQueryParams QueryParams = new ( ) ;
33+ public readonly Dictionary < string , BssbServer > AllServers = new ( ) ;
34+ public string ? MessageOfTheDay { get ; private set ; } = null ;
3235
3336 public bool IsLoading { get ; private set ; }
34- public bool LoadingErrored { get ; private set ; }
37+ public bool ApiRequestFailed { get ; private set ; }
3538
39+ private CancellationTokenSource ? _loadingCts ;
40+ private readonly Dictionary < string , float > _discoveryResponseAges = new ( ) ;
41+
42+ private const int DiscoveryTimeoutSeconds = 30 ; // discovery packets are sent every 5 secs
43+
44+ #region Init
45+
46+ public void Initialize ( )
47+ {
48+ _discoveryClient . ResponseReceived += HandleDiscoveryResponse ;
49+ }
50+
51+ public void Dispose ( )
52+ {
53+ _discoveryClient . Dispose ( ) ;
54+ _discoveryClient . ResponseReceived -= HandleDiscoveryResponse ;
55+ }
56+
57+ #endregion
58+
59+ #region Refresh API
60+
3661 public async Task ResetRefresh ( )
3762 {
3863 CancelLoading ( ) ;
39- _pageIndex = 0 ;
4064 await Refresh ( ) ;
4165 }
4266
4367 public async Task Refresh ( )
4468 {
45- CancelLoading ( ) ;
46- TriggerUpdate ( true ) ;
47-
48- // Calculate pagination offset
49- var pageSize = PageData ? . PageSize ?? DefaultPageSize ;
50- QueryParams . Offset = ( _pageIndex * pageSize ) ;
51-
52- // Query API (load page)
5369 try
5470 {
55- PageData = await _apiClient . Browse ( QueryParams , _loadingCts ! . Token ) ;
71+ CancelLoading ( ) ;
72+ TriggerUpdate ( true ) ;
73+
74+ // Query API (load page)
75+ BrowseResponse ? apiResult = null ;
76+
77+ try
78+ {
79+ apiResult = await _apiClient . Browse ( QueryParams , _loadingCts ! . Token ) ;
80+
81+ if ( apiResult is null )
82+ _log . Warn ( $ "Browser API request failed (request error, or invalid response)") ;
83+ else if ( apiResult . Servers == null )
84+ _log . Warn ( $ "Browser API sent null server list") ;
85+ else
86+ ProcessApiResponse ( apiResult ) ;
87+ }
88+ catch ( TaskCanceledException )
89+ {
90+ _log . Info ( $ "Browser API request cancelled") ;
91+ }
92+
93+ // Trigger update
94+ TriggerUpdate ( false , apiResult == null ) ;
5695 }
57- catch ( TaskCanceledException )
96+ catch ( Exception ex )
5897 {
59- PageData = null ;
98+ _log . Error ( $ "Browser refresh failed: { ex } ") ;
99+ TriggerUpdate ( false , true ) ;
60100 }
101+ }
61102
62- if ( PageData is not null )
63- _log . Debug ( $ "BrowseData loaded page (Index={ _pageIndex } , TotalCount={ PageData . TotalResultCount } , " +
64- $ "Limit={ PageData . PageSize } , LobbiesCount={ PageData . Servers ? . Count ?? 0 } , " +
65- $ "MOTD={ PageData . MessageOfTheDay } )") ;
66- else
67- _log . Error ( $ "BrowseData page load failed - received null response (Index={ _pageIndex } )") ;
68-
69- // Trigger update
70- TriggerUpdate ( false , ( PageData == null ) ) ;
103+ public void CancelLoading ( )
104+ {
105+ _loadingCts ? . Cancel ( ) ;
106+ _loadingCts ? . Dispose ( ) ;
107+ _loadingCts = new ( ) ;
71108 }
72109
73- public async Task PageUp ( )
110+ public void EnableDiscovery ( )
74111 {
75- if ( _pageIndex <= 0 )
76- return ;
77-
78- _pageIndex -- ;
79- await Refresh ( ) ;
112+ _discoveryClient . StartBroadcast ( ) ;
80113 }
81114
82- public async Task PageDown ( )
115+ public void DisableDiscovery ( )
83116 {
84- _pageIndex ++ ;
85- await Refresh ( ) ;
117+ _discoveryClient . StopBroadcast ( ) ;
86118 }
119+
120+ #endregion
87121
88- public void CancelLoading ( )
122+ #region Refresh data handlers
123+
124+ private void ProcessApiResponse ( BrowseResponse apiResponse )
89125 {
90- _loadingCts ? . Cancel ( ) ;
91- _loadingCts ? . Dispose ( ) ;
126+ MessageOfTheDay = apiResponse . MessageOfTheDay ;
127+
128+ if ( apiResponse . Servers == null )
129+ return ;
92130
93- _loadingCts = new ( ) ;
131+ // Remove stale discovery responses
132+ var localDiscoveryKeys = new HashSet < string > ( _discoveryResponseAges . Keys ) ;
133+
134+ foreach ( var discoveryKey in localDiscoveryKeys )
135+ {
136+ var timeDiff = Time . realtimeSinceStartup - _discoveryResponseAges [ discoveryKey ] ;
137+ if ( timeDiff < DiscoveryTimeoutSeconds )
138+ continue ;
139+
140+ // Timed out
141+ AllServers . Remove ( discoveryKey ) ;
142+ _discoveryResponseAges . Remove ( discoveryKey ) ;
143+ }
94144
95- if ( IsLoading )
145+ // Add or update servers, index missing servers
146+ var keysMissing = new HashSet < string > ( AllServers . Keys ) ;
147+
148+ foreach ( var server in apiResponse . Servers )
96149 {
97- TriggerUpdate ( false , true ) ;
150+ AllServers [ server . Key ] = server ;
151+ keysMissing . Remove ( server . Key ) ;
152+ }
153+
154+ // Remove servers not in API response
155+ foreach ( var key in keysMissing )
156+ {
157+ if ( localDiscoveryKeys . Contains ( key ) )
158+ // Don't remove
159+ continue ;
160+
161+ AllServers . Remove ( key ) ;
98162 }
99163 }
164+
165+ private void HandleDiscoveryResponse ( DiscoveryResponsePacket response , IPEndPoint source )
166+ {
167+ var serverData = response . ToServerData ( source ) ;
168+
169+ AllServers [ serverData . Key ] = serverData ;
170+ _discoveryResponseAges [ serverData . Key ] = Time . realtimeSinceStartup ;
171+
172+ TriggerUpdate ( IsLoading ) ;
173+ }
174+
175+ #endregion
100176
101- private void TriggerUpdate ( bool isLoading , bool didError = false )
177+ #region Events
178+
179+ private void TriggerUpdate ( bool isLoading , bool apiRequestFailed = false )
102180 {
103181 IsLoading = isLoading ;
104- LoadingErrored = didError ;
105-
182+ ApiRequestFailed = apiRequestFailed ;
183+
106184 UpdateEvent ? . Invoke ( this , EventArgs . Empty ) ;
107185 }
186+
187+ #endregion
188+
189+ #region Sort helper
190+
191+ public List < BssbServer > GetAllServersSorted ( )
192+ {
193+ return AllServers . Values
194+ // Primary sort: preference, our guess on how likely the player wants to see/join it
195+ . OrderByDescending ( server => server . PreferentialSortScore )
196+ // Secondary sort: prefer newer lobbies
197+ . ThenByDescending ( server => server . ReadOnlyFirstSeen )
198+ . ToList ( ) ;
199+ }
200+
201+ #endregion
108202 }
109203}
0 commit comments