From fcf326ed83b4a0bbc77670576cd5e0060f2e64f3 Mon Sep 17 00:00:00 2001 From: hbeham Date: Sun, 28 Jun 2015 17:06:38 +0200 Subject: [PATCH] - moved server query logic to a separate class - added auto-refresh interval - keep previous information displayed until server data was actually updated - added alert when servers are found that pass the filter --- ServerBrowser/Program.cs | 7 + ServerBrowser/ServerBrowser.csproj | 10 +- ServerBrowser/ServerBrowserForm.Designer.cs | 42 ++- ServerBrowser/ServerBrowserForm.cs | 335 +++++--------------- ServerBrowser/ServerQueryLogic.cs | 328 +++++++++++++++++++ ServerBrowser/Toxikk.cs | 6 +- changelog.md | 7 + 7 files changed, 451 insertions(+), 284 deletions(-) create mode 100644 ServerBrowser/ServerQueryLogic.cs diff --git a/ServerBrowser/Program.cs b/ServerBrowser/Program.cs index b727902..8271011 100644 --- a/ServerBrowser/Program.cs +++ b/ServerBrowser/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Threading; using System.Windows.Forms; using DevExpress.LookAndFeel; @@ -11,6 +12,12 @@ static class Program [STAThread] static void Main() { +#if false + var culture = new CultureInfo("en"); + Application.CurrentCulture = culture; + Thread.CurrentThread.CurrentUICulture = culture; + Thread.CurrentThread.CurrentCulture = culture; +#endif InitExceptionHandling(); Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); diff --git a/ServerBrowser/ServerBrowser.csproj b/ServerBrowser/ServerBrowser.csproj index cc59231..8034c82 100644 --- a/ServerBrowser/ServerBrowser.csproj +++ b/ServerBrowser/ServerBrowser.csproj @@ -57,44 +57,37 @@ False - True False - True False - True False - True False - True False - True False - True False - True + @@ -133,6 +126,7 @@ + diff --git a/ServerBrowser/ServerBrowserForm.Designer.cs b/ServerBrowser/ServerBrowserForm.Designer.cs index f905bdd..9f3aa14 100644 --- a/ServerBrowser/ServerBrowserForm.Designer.cs +++ b/ServerBrowser/ServerBrowserForm.Designer.cs @@ -96,8 +96,8 @@ private void InitializeComponent() this.panelTopFill = new DevExpress.XtraEditors.PanelControl(); this.panelControls = new DevExpress.XtraEditors.PanelControl(); this.panelOptions = new DevExpress.XtraEditors.PanelControl(); + this.linkFilter1 = new DevExpress.XtraEditors.HyperlinkLabelControl(); this.cbAdvancedOptions = new DevExpress.XtraEditors.CheckButton(); - this.labelControl2 = new DevExpress.XtraEditors.LabelControl(); this.panelGame = new DevExpress.XtraEditors.PanelControl(); this.labelControl5 = new DevExpress.XtraEditors.LabelControl(); this.labelControl4 = new DevExpress.XtraEditors.LabelControl(); @@ -109,6 +109,7 @@ private void InitializeComponent() this.labelControl6 = new DevExpress.XtraEditors.LabelControl(); this.timerUpdateServerList = new System.Windows.Forms.Timer(this.components); this.panelAdvancedOptions = new DevExpress.XtraEditors.PanelControl(); + this.linkFilter2 = new DevExpress.XtraEditors.HyperlinkLabelControl(); this.spinRefreshInterval = new DevExpress.XtraEditors.SpinEdit(); this.cbAlert = new DevExpress.XtraEditors.CheckEdit(); this.cbShowGamePort = new DevExpress.XtraEditors.CheckEdit(); @@ -846,8 +847,8 @@ private void InitializeComponent() // panelOptions // this.panelOptions.BorderStyle = DevExpress.XtraEditors.Controls.BorderStyles.NoBorder; + this.panelOptions.Controls.Add(this.linkFilter1); this.panelOptions.Controls.Add(this.cbAdvancedOptions); - this.panelOptions.Controls.Add(this.labelControl2); this.panelOptions.Controls.Add(this.labelControl1); this.panelOptions.Controls.Add(this.btnQueryMaster); this.panelOptions.Controls.Add(this.comboRegion); @@ -858,6 +859,17 @@ private void InitializeComponent() this.panelOptions.Size = new System.Drawing.Size(969, 58); this.panelOptions.TabIndex = 26; // + // linkFilter1 + // + this.linkFilter1.Cursor = System.Windows.Forms.Cursors.Hand; + this.linkFilter1.Location = new System.Drawing.Point(30, 32); + this.linkFilter1.Name = "linkFilter1"; + this.linkFilter1.Size = new System.Drawing.Size(454, 13); + this.linkFilter1.TabIndex = 33; + this.linkFilter1.Text = "HINT: Use the top row of the table for simple filters or the filter editor<" + + "/href> for more complex filters."; + this.linkFilter1.HyperlinkClick += new DevExpress.Utils.HyperlinkClickEventHandler(this.linkFilter_HyperlinkClick); + // // cbAdvancedOptions // this.cbAdvancedOptions.Location = new System.Drawing.Point(502, 2); @@ -867,14 +879,6 @@ private void InitializeComponent() this.cbAdvancedOptions.Text = "Show Options"; this.cbAdvancedOptions.CheckedChanged += new System.EventHandler(this.cbAdvancedOptions_CheckedChanged); // - // labelControl2 - // - this.labelControl2.Location = new System.Drawing.Point(30, 32); - this.labelControl2.Name = "labelControl2"; - this.labelControl2.Size = new System.Drawing.Size(282, 13); - this.labelControl2.TabIndex = 23; - this.labelControl2.Text = "HINT: Use the top row of the table to specify filter criteria."; - // // panelGame // this.panelGame.BorderStyle = DevExpress.XtraEditors.Controls.BorderStyles.NoBorder; @@ -978,6 +982,7 @@ private void InitializeComponent() // // panelAdvancedOptions // + this.panelAdvancedOptions.Controls.Add(this.linkFilter2); this.panelAdvancedOptions.Controls.Add(this.spinRefreshInterval); this.panelAdvancedOptions.Controls.Add(this.cbRefreshSelectedServer); this.panelAdvancedOptions.Controls.Add(this.labelControl3); @@ -997,6 +1002,16 @@ private void InitializeComponent() this.panelAdvancedOptions.TabIndex = 30; this.panelAdvancedOptions.Visible = false; // + // linkFilter2 + // + this.linkFilter2.Cursor = System.Windows.Forms.Cursors.Hand; + this.linkFilter2.Location = new System.Drawing.Point(668, 60); + this.linkFilter2.Name = "linkFilter2"; + this.linkFilter2.Size = new System.Drawing.Size(27, 13); + this.linkFilter2.TabIndex = 32; + this.linkFilter2.Text = "filters"; + this.linkFilter2.HyperlinkClick += new DevExpress.Utils.HyperlinkClickEventHandler(this.linkFilter_HyperlinkClick); + // // spinRefreshInterval // this.spinRefreshInterval.EditValue = new decimal(new int[] { @@ -1028,8 +1043,8 @@ private void InitializeComponent() this.cbAlert.MenuManager = this.barManager1; this.cbAlert.Name = "cbAlert"; this.cbAlert.Properties.AutoWidth = true; - this.cbAlert.Properties.Caption = "Show alert and play sound when servers pass the filter criteria"; - this.cbAlert.Size = new System.Drawing.Size(322, 19); + this.cbAlert.Properties.Caption = "Show alert and play sound when servers pass the"; + this.cbAlert.Size = new System.Drawing.Size(261, 19); this.cbAlert.TabIndex = 2; this.cbAlert.CheckedChanged += new System.EventHandler(this.cbAlert_CheckedChanged); // @@ -1261,7 +1276,6 @@ private void InitializeComponent() private PanelControl panelControls; private PanelControl panelOptions; private LabelControl labelControl3; - private LabelControl labelControl2; private DevExpress.XtraBars.Docking.DockPanel panelServerList; private DevExpress.XtraBars.Docking.ControlContainer controlContainer1; private DevExpress.XtraBars.Docking.DockPanel panelContainer1; @@ -1309,6 +1323,8 @@ private void InitializeComponent() private SpinEdit spinRefreshInterval; private System.Windows.Forms.Timer timerRefreshServers; private DevExpress.XtraBars.Alerter.AlertControl alertControl1; + private HyperlinkLabelControl linkFilter2; + private HyperlinkLabelControl linkFilter1; diff --git a/ServerBrowser/ServerBrowserForm.cs b/ServerBrowser/ServerBrowserForm.cs index 5790dd0..90405b8 100644 --- a/ServerBrowser/ServerBrowserForm.cs +++ b/ServerBrowser/ServerBrowserForm.cs @@ -1,13 +1,11 @@ using System; using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Diagnostics; using System.Drawing; using System.IO; using System.Media; using System.Net; using System.Reflection; -using System.Threading; using System.Windows.Forms; using DevExpress.LookAndFeel; using DevExpress.Utils; @@ -39,32 +37,19 @@ public partial class ServerBrowserForm : XtraForm private const string Version = "1.7"; private string brandingUrl; - private List servers; + //private List servers; private ServerRow lastSelectedServer; - private volatile bool shutdown; private volatile Game steamAppId; private readonly Dictionary extenders = new Dictionary(); private GameExtension gameExtension; private int ignoreUiEvents; private readonly CheckEdit[] favGameRadioButtons; private readonly List gameIdForComboBoxIndex = new List(); - private volatile bool serverListUpdateNeeded; - private const int maxResults = 500; + private const int MaxResults = 500; private readonly PasswordForm passwordForm = new PasswordForm(); private bool showGamePortInAddress; - private volatile int prevRequestId; // logical clock to drop obsolete replies, e.g. when the user selected a different game in the meantime - private volatile UpdateRequest currentRequest = new UpdateRequest(0); - - class UpdateRequest - { - public UpdateRequest(int id) - { - this.Id = id; - } - - public readonly int Id; - public CountdownEvent PendingTasks; - } + private readonly ServerQueryLogic queryLogic; + private List servers; #region ctor() public ServerBrowserForm() @@ -88,7 +73,15 @@ public ServerBrowserForm() this.panelServerList.Dock = DockingStyle.Fill; this.Controls.Add(this.panelServerList); + UserLookAndFeel.Default.StyleChanged += LookAndFeel_StyleChanged; + LookAndFeel_StyleChanged(null, null); + base.Text += " " + Version; + + this.queryLogic = new ServerQueryLogic(); + this.queryLogic.UpdateStatus += (s, e) => this.BeginInvoke((Action)(() => { this.txtStatus.Text = e.Text; })); + this.queryLogic.UpdateServerListComplete += (s,e) => this.BeginInvoke((Action)(() => { OnUpdateServerListComplete(e.Rows); })); + this.queryLogic.UpdateSingleServerComplete += (s, e) => this.BeginInvoke((Action) (() => { OnUpdateSingleServerComplete(e); })); } #endregion @@ -108,7 +101,7 @@ protected override void OnLoad(EventArgs e) this.InitAppSettings(); this.miConnect.ItemAppearance.Normal.Font = new Font(this.miConnect.ItemAppearance.Normal.Font, FontStyle.Bold); - + this.linkFilter2.Left = this.cbAlert.Right; --this.ignoreUiEvents; this.UpdateServerList(); } @@ -123,7 +116,7 @@ protected override void OnClosed(EventArgs e) Properties.Settings.Default.RefreshInterval = Convert.ToInt32(this.spinRefreshInterval.EditValue); Properties.Settings.Default.Save(); - this.shutdown = true; + this.queryLogic.Cancel(); base.OnClosed(e); } #endregion @@ -355,228 +348,14 @@ private void UpdateServerList() if (this.SteamAppID == 0) // this would result in a truncated list of all games return; - var request = new UpdateRequest(++prevRequestId); this.txtStatus.Text = "Requesting server list from master server..."; - this.gvServers.BeginDataUpdate(); - var rows = new List(); // local reference to guarantee thread safety - this.servers = rows; - this.gcServers.DataSource = this.servers; - this.gvServers.EndDataUpdate(); - MasterServer master = new MasterServer(this.MasterServerEndpoint); - master.GetAddressesLimit = maxResults; - IpFilter filter = new IpFilter(); - filter.App = this.SteamAppID; var region = (QueryMaster.Region)steamRegions[this.comboRegion.SelectedIndex * 2 + 1]; - - master.GetAddresses(region, endpoints => OnMasterServerReceive(endpoints, request, rows), filter); - this.currentRequest = request; - } - #endregion - - #region OnMasterServerReceive() - private void OnMasterServerReceive(ReadOnlyCollection endPoints, UpdateRequest request, List rows) - { - // ignore results from older queries - if (request.Id != this.currentRequest.Id) - return; - - string statusText; - if (endPoints == null) - statusText = "Master server request timed out"; - else - { - statusText = "Requesting next batch of server list..."; - foreach (var ep in endPoints) - { - if (this.shutdown) - return; - if (ep.Address.Equals(IPAddress.Any)) - { - statusText = "Master server returned " + this.servers.Count + " servers"; - this.AllServersReceived(request, rows); - } - else if (servers.Count >= maxResults) - { - statusText = "Server list limited to " + maxResults + " entries"; - this.AllServersReceived(request, rows); - break; - } - else - rows.Add(new ServerRow(ep)); - } - } - - this.serverListUpdateNeeded = true; - this.BeginInvoke((Action) (() => - { - this.txtStatus.Text = statusText; - })); - } - #endregion - - #region AllServersReceived() - private void AllServersReceived(UpdateRequest request, List rows) - { - // use a background thread so that the caller doesn't have to wait for the accumulated Thread.Sleep() - ThreadPool.QueueUserWorkItem(dummy => - { - request.PendingTasks = new CountdownEvent(rows.Count); - foreach (var row in rows) - { - if (request.Id != this.currentRequest.Id) - return; - var safeRow = row; - ThreadPool.QueueUserWorkItem(context => UpdateServerDetails(safeRow, request)); - Thread.Sleep(5); // launching all threads at once results in totally wrong ping values - } - request.PendingTasks.Wait(); - this.BeginInvoke((Action) (() => - { - OnServerUpdateComplete(request, rows); - })); - }); - } - #endregion - - #region OnServerUpdateComplete() - private void OnServerUpdateComplete(UpdateRequest request, List rows) - { - if (request.Id == this.currentRequest.Id) - this.txtStatus.Text = "Update of " + rows.Count + " servers complete"; - - this.timerUpdateServerList_Tick(null, null); - if (this.gvServers.RowCount > 0 && this.cbAlert.Checked) - { - SystemSounds.Asterisk.Play(); - this.alertControl1.Show(this, "Steam Server Browser", "Found " + this.gvServers.RowCount + " server(s) matching your critera."); - } - } - - #endregion - - #region UpdateSingleServer() - private void UpdateSingleServer(ServerRow row) - { - row.Status = "updating..."; - this.currentRequest = new UpdateRequest(++this.prevRequestId); - this.currentRequest.PendingTasks = new CountdownEvent(1); - ThreadPool.QueueUserWorkItem(dummy => - this.UpdateServerDetails(row, this.currentRequest, () => - { - if (this.gvServers.GetRow(this.gvServers.FocusedRowHandle) == row) - this.UpdateGridDataSources(row); - })); - } - #endregion - - #region UpdateServerDetails() - private void UpdateServerDetails(ServerRow row, UpdateRequest request, Action callback = null) - { - try - { - if (request.Id != this.currentRequest.Id) // drop obsolete requests - return; - - string status; - using (Server server = ServerQuery.GetServerInstance(EngineType.Source, row.EndPoint, false, 500, 500)) - { - row.Retries = 0; - server.Retries = 3; - status = "timeout"; - if (this.UpdateServerInfo(row, server, request)) - { - this.UpdatePlayers(row, server, request); - this.UpdateRules(row, server, request); - status = "ok"; - } - } - - if (request.Id != this.currentRequest.Id) // status might have changed - return; - - if (row.Retries > 0) - status += " (" + row.Retries + ")"; - row.Status = status; - row.Update(); - - if (this.shutdown) - return; - this.serverListUpdateNeeded = true; - if (callback != null) - this.BeginInvoke(callback); - } - finally - { - request.PendingTasks.Signal(); - } - } - #endregion - - #region UpdateServerInfo() - private bool UpdateServerInfo(ServerRow row, Server server, UpdateRequest request) - { - return UpdateDetail(row, server, request, retryHandler => - { - row.ServerInfo = server.GetInfo(retryHandler); - row.RequestId = request.Id; - }); + var getRules = this.gameExtension == null || this.gameExtension.SupportsRulesQuery; + queryLogic.UpdateServerList(this.MasterServerEndpoint, MaxResults, this.SteamAppID, region, getRules); } #endregion - #region UpdatePlayers() - private void UpdatePlayers(ServerRow row, Server server, UpdateRequest request) - { - UpdateDetail(row, server, request, retryHandler => - { - var players = server.GetPlayers(retryHandler); - row.Players = players == null ? null : new List(players); - }); - } - #endregion - - #region UpdateRules() - private void UpdateRules(ServerRow row, Server server, UpdateRequest request) - { - if (gameExtension != null && !gameExtension.SupportsRulesQuery) - return; - UpdateDetail(row, server, request, retryHandler => - { - row.Rules = new List(server.GetRules(retryHandler)); - }); - } - #endregion - - #region UpdateDetail() - private bool UpdateDetail(ServerRow row, Server server, UpdateRequest request, Action> updater) - { - if (request.Id != this.currentRequest.Id) - return false; - - try - { - row.Status = "updating " + row.Retries; - this.serverListUpdateNeeded = true; - updater(retry => - { - if (request.Id != currentRequest.Id) - throw new OperationCanceledException(); - row.Status = "updating " + (++row.Retries + 1); - this.serverListUpdateNeeded = true; - }); - return true; - } - catch (TimeoutException) - { - return false; - } - catch - { - return true; - } - } - #endregion - #region EnumerateProps() private List> EnumerateProps(params object[] objects) { @@ -637,6 +416,38 @@ private string GetServerAddress(ServerRow row) } #endregion + #region OnUpdateServerListComplete() + private void OnUpdateServerListComplete(List rows) + { + this.txtStatus.Text = "Update of " + rows.Count + " servers complete"; + + this.timerUpdateServerList_Tick(null, null); + if (this.gvServers.RowCount > 0 && this.cbAlert.Checked) + { + SystemSounds.Asterisk.Play(); + this.alertControl1.Show(this, "Steam Server Browser", "Found " + this.gvServers.RowCount + " server(s) matching your criteria."); + } + } + #endregion + + #region OnUpdateSingleServerComplete() + private void OnUpdateSingleServerComplete(ServerEventArgs e) + { + if (this.gvServers.GetRow(this.gvServers.FocusedRowHandle) == e.Server) + this.UpdateGridDataSources(e.Server); + } + #endregion + + + #region LookAndFeel_StyleChanged + private void LookAndFeel_StyleChanged(object sender, EventArgs eventArgs) + { + var skin = DevExpress.Skins.CommonSkins.GetSkin(UserLookAndFeel.Default); + var color = skin.Colors["ControlText"]; + this.linkFilter1.Appearance.LinkColor = this.linkFilter1.Appearance.PressedColor = color; + this.linkFilter2.Appearance.LinkColor = this.linkFilter2.Appearance.PressedColor = color; + } + #endregion #region picLogo_Click private void picLogo_Click(object sender, EventArgs e) @@ -676,7 +487,6 @@ private void rbFavGame_CheckedChanged(object sender, EventArgs e) var ids = Properties.Settings.Default.FavGameIDs.Split(','); ids[idx] = ((int)this.SteamAppID).ToString(); Properties.Settings.Default.FavGameIDs = string.Join(",", ids); - Properties.Settings.Default.Save(); this.InitFavGameRadioButtons(); } else @@ -710,6 +520,21 @@ private void btnQueryMaster_Click(object sender, EventArgs e) } #endregion + #region linkFilter_HyperlinkClick + private void linkFilter_HyperlinkClick(object sender, HyperlinkClickEventArgs e) + { + this.gvServers.ShowFilterEditor(this.colHumanPlayers); + } + #endregion + + #region btnSkin_Click + private void btnSkin_Click(object sender, EventArgs e) + { + using (var dlg = new SkinPicker()) + dlg.ShowDialog(this); + } + #endregion + #region cbAdvancedOptions_CheckedChanged private void cbAdvancedOptions_CheckedChanged(object sender, EventArgs e) { @@ -722,15 +547,6 @@ private void cbShowGamePort_CheckedChanged(object sender, EventArgs e) { this.showGamePortInAddress = this.cbShowGamePort.Checked; Properties.Settings.Default.ShowGamePortInAddress = this.showGamePortInAddress; - Properties.Settings.Default.Save(); - } - #endregion - - #region btnSkin_Click - private void btnSkin_Click(object sender, EventArgs e) - { - using (var dlg = new SkinPicker()) - dlg.ShowDialog(this); } #endregion @@ -763,11 +579,11 @@ private void gvServers_FocusedRowChanged(object sender, DevExpress.XtraGrid.View if (!this.cbRefreshSelectedServer.Checked) return; - if (this.currentRequest.PendingTasks != null && !this.currentRequest.PendingTasks.IsSet) + if (this.queryLogic.IsUpdating) return; Application.DoEvents(); - UpdateSingleServer(row); + this.queryLogic.UpdateSingleServer(row); } catch (Exception ex) { @@ -818,18 +634,19 @@ private void dockManager1_StartDocking(object sender, DockPanelCancelEventArgs e #region timerUpdateServerList_Tick private void timerUpdateServerList_Tick(object sender, EventArgs e) { - if (!this.serverListUpdateNeeded) + if (!this.queryLogic.GetAndResetUpdateNeededFlag()) return; - this.serverListUpdateNeeded = false; + this.servers = this.queryLogic.Servers; ++ignoreUiEvents; this.gvServers.BeginDataUpdate(); + this.gcServers.DataSource = servers; this.gvServers.EndDataUpdate(); --ignoreUiEvents; if (this.lastSelectedServer != null) { int i = 0; - foreach (var server in this.servers) + foreach (var server in servers) { if (server.EndPoint.Equals(this.lastSelectedServer.EndPoint)) { @@ -840,6 +657,10 @@ private void timerUpdateServerList_Tick(object sender, EventArgs e) ++i; } } + + var curServer = this.gvServers.GetFocusedRow() as ServerRow; + if (curServer != null) + this.UpdateGridDataSources(curServer); } #endregion @@ -863,8 +684,6 @@ private void btnServerQuery_Click(object sender, EventArgs e) } } - this.currentRequest = new UpdateRequest(++this.prevRequestId); - this.currentRequest.PendingTasks = new CountdownEvent(1); if (serverRow == null) { this.gvServers.BeginDataUpdate(); @@ -873,7 +692,7 @@ private void btnServerQuery_Click(object sender, EventArgs e) this.gvServers.EndDataUpdate(); this.gvServers.FocusedRowHandle = this.gvServers.GetRowHandle(this.servers.Count - 1); } - this.UpdateServerDetails(serverRow, this.currentRequest, () => this.UpdateGridDataSources(serverRow)); + this.queryLogic.UpdateSingleServer(serverRow); } catch { @@ -884,7 +703,7 @@ private void btnServerQuery_Click(object sender, EventArgs e) #region miUpdateServerInfo_ItemClick private void miUpdateServerInfo_ItemClick(object sender, ItemClickEventArgs e) { - this.UpdateSingleServer((ServerRow)this.gvServers.GetFocusedRow()); + this.queryLogic.UpdateSingleServer((ServerRow)this.gvServers.GetFocusedRow()); } #endregion @@ -959,7 +778,6 @@ private void gvPlayers_CustomUnboundColumnData(object sender, DevExpress.XtraGri } #endregion - #region cbShowPlayerCountDetailColumns_CheckedChanged private void cbShowPlayerCountDetailColumns_CheckedChanged(object sender, EventArgs e) { @@ -979,7 +797,6 @@ private void cbAlert_CheckedChanged(object sender, EventArgs e) { if (!this.cbAlert.Checked || !string.IsNullOrEmpty(this.gvServers.ActiveFilterString)) return; - this.cbShowPlayerCountDetailColumns.Checked = true; this.gvServers.ActiveFilterString = "[ServerInfo.Players]>=1"; } #endregion @@ -1000,13 +817,11 @@ private void alertControl1_AlertClick(object sender, DevExpress.XtraBars.Alerter } #endregion - #region timerRefreshServers_Tick private void timerRefreshServers_Tick(object sender, EventArgs e) { this.UpdateServerList(); } #endregion - } } \ No newline at end of file diff --git a/ServerBrowser/ServerQueryLogic.cs b/ServerBrowser/ServerQueryLogic.cs new file mode 100644 index 0000000..8c7eedf --- /dev/null +++ b/ServerBrowser/ServerQueryLogic.cs @@ -0,0 +1,328 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Net; +using System.Threading; +using QueryMaster; + +namespace ServerBrowser +{ + #region class UpdateRequest + class UpdateRequest + { + public UpdateRequest(int id) + { + this.Id = id; + } + + public readonly int Id; + public CountdownEvent PendingTasks; + } + #endregion + + #region class TextEventArgs + public class TextEventArgs : EventArgs + { + public string Text { get; private set; } + + public TextEventArgs(string text) + { + this.Text = text; + } + } + #endregion + + #region class UpdateCompleteEventArgs + public class UpdateCompleteEventArgs : EventArgs + { + public List Rows { get; private set; } + + public UpdateCompleteEventArgs(List rows) + { + this.Rows = rows; + } + } + #endregion + + #region class ServerEventArgs + public class ServerEventArgs : EventArgs + { + public ServerRow Server { get; private set; } + + public ServerEventArgs(ServerRow server) + { + this.Server = server; + } + } + #endregion + + public class ServerQueryLogic + { + private volatile int prevRequestId; // logical clock to drop obsolete replies, e.g. when the user selected a different game in the meantime + private volatile UpdateRequest currentRequest = new UpdateRequest(0); + private volatile bool shutdown; + private volatile List allServers; + private List servers; + private int maxResults; + private bool queryServerRules; + private int serverListUpdateNeeded; + public event EventHandler UpdateStatus; + public event EventHandler UpdateServerListComplete; + public event EventHandler UpdateSingleServerComplete; + + #region IsUpdating + public bool IsUpdating + { + get { return this.currentRequest.PendingTasks != null && !this.currentRequest.PendingTasks.IsSet; } + } + #endregion + + #region GetAndResetUpdateNeededFlag() + public bool GetAndResetUpdateNeededFlag() + { + return Interlocked.Exchange(ref serverListUpdateNeeded, 0) != 0; + } + #endregion + + #region Servers + public List Servers { get { return this.allServers; } } + #endregion + + #region UpdateServerList() + // ReSharper disable ParameterHidesMember + public void UpdateServerList(IPEndPoint masterServerEndPoint, int maxResults, Game steamAppId, Region region, bool queryServerRules) + // ReSharper restore ParameterHidesMember + { + this.maxResults = maxResults; + this.queryServerRules = queryServerRules; + var request = new UpdateRequest(++prevRequestId); + var rows = new List(); // local reference to guarantee thread safety + this.servers = rows; + + MasterServer master = new MasterServer(masterServerEndPoint); + master.GetAddressesLimit = maxResults; + IpFilter filter = new IpFilter(); + filter.App = steamAppId; + + master.GetAddresses(region, endpoints => OnMasterServerReceive(endpoints, request, rows), filter); + this.currentRequest = request; + } + #endregion + + #region Cancel() + public void Cancel() + { + this.shutdown = true; + } + #endregion + + #region OnMasterServerReceive() + private void OnMasterServerReceive(ReadOnlyCollection endPoints, UpdateRequest request, List rows) + { + // ignore results from older queries + if (request.Id != this.currentRequest.Id) + return; + + string statusText; + if (endPoints == null) + statusText = "Master server request timed out"; + else + { + statusText = "Requesting next batch of server list..."; + foreach (var ep in endPoints) + { + if (this.shutdown) + return; + if (ep.Address.Equals(IPAddress.Any)) + { + statusText = "Master server returned " + this.servers.Count + " servers"; + this.AllServersReceived(request, rows); + } + else if (servers.Count >= maxResults) + { + statusText = "Server list limited to " + maxResults + " entries"; + this.AllServersReceived(request, rows); + break; + } + else + rows.Add(new ServerRow(ep)); + } + } + + this.serverListUpdateNeeded = 1; + if (this.UpdateStatus != null) + this.UpdateStatus(this, new TextEventArgs(statusText)); + } + #endregion + + #region AllServersReceived() + private void AllServersReceived(UpdateRequest request, List rows) + { + var oldServers = new Dictionary(); + if (this.allServers != null) + { + foreach (var server in this.allServers) + oldServers[server.EndPoint] = server; + } + for (int i=0, c=rows.Count; i + { + request.PendingTasks = new CountdownEvent(rows.Count); + foreach (var row in rows) + { + if (request.Id != this.currentRequest.Id) + return; + var safeRow = row; + ThreadPool.QueueUserWorkItem(context => UpdateServerDetails(safeRow, request)); + Thread.Sleep(5); // launching all threads at once results in totally wrong ping values + } + request.PendingTasks.Wait(); + + if (request.Id != this.currentRequest.Id) + return; + + if (this.UpdateServerListComplete != null) + this.UpdateServerListComplete(this, new UpdateCompleteEventArgs(rows)); + }); + } + #endregion + + + #region UpdateSingleServer() + public void UpdateSingleServer(ServerRow row) + { + row.Status = "updating..."; + this.currentRequest = new UpdateRequest(++this.prevRequestId); + this.currentRequest.PendingTasks = new CountdownEvent(1); + ThreadPool.QueueUserWorkItem(dummy => + this.UpdateServerDetails(row, this.currentRequest, () => + { + if (this.UpdateSingleServerComplete != null) + this.UpdateSingleServerComplete(this, new ServerEventArgs(row)); + })); + } + #endregion + + #region UpdateServerDetails() + private void UpdateServerDetails(ServerRow row, UpdateRequest request, Action callback = null) + { + try + { + if (request.Id != this.currentRequest.Id) // drop obsolete requests + return; + + string status; + using (Server server = ServerQuery.GetServerInstance(EngineType.Source, row.EndPoint, false, 500, 500)) + { + row.Retries = 0; + server.Retries = 3; + status = "timeout"; + if (this.UpdateServerInfo(row, server, request)) + { + this.UpdatePlayers(row, server, request); + this.UpdateRules(row, server, request); + status = "ok"; + } + } + + if (request.Id != this.currentRequest.Id) // status might have changed + return; + + if (row.Retries > 0) + status += " (" + row.Retries + ")"; + row.Status = status; + row.Update(); + + if (this.shutdown) + return; + this.serverListUpdateNeeded = 1; + if (callback != null) + callback(); + } + finally + { + request.PendingTasks.Signal(); + } + } + #endregion + + #region UpdateServerInfo() + private bool UpdateServerInfo(ServerRow row, Server server, UpdateRequest request) + { + bool ok= UpdateDetail(row, server, request, retryHandler => + { + row.ServerInfo = server.GetInfo(retryHandler); + row.RequestId = request.Id; + }); + if (!ok) + row.ServerInfo = null; + return ok; + } + #endregion + + #region UpdatePlayers() + private void UpdatePlayers(ServerRow row, Server server, UpdateRequest request) + { + bool ok = UpdateDetail(row, server, request, retryHandler => + { + var players = server.GetPlayers(retryHandler); + row.Players = players == null ? null : new List(players); + }); + if (!ok) + row.Players = null; + } + #endregion + + #region UpdateRules() + private void UpdateRules(ServerRow row, Server server, UpdateRequest request) + { + if (!this.queryServerRules) + return; + bool ok = UpdateDetail(row, server, request, retryHandler => + { + row.Rules = new List(server.GetRules(retryHandler)); + }); + if (!ok) + row.Rules = null; + } + #endregion + + #region UpdateDetail() + private bool UpdateDetail(ServerRow row, Server server, UpdateRequest request, Action> updater) + { + if (request.Id != this.currentRequest.Id) + return false; + + try + { + row.Status = "updating " + row.Retries; + this.serverListUpdateNeeded = 1; + updater(retry => + { + if (request.Id != currentRequest.Id) + throw new OperationCanceledException(); + row.Status = "updating " + (++row.Retries + 1); + this.serverListUpdateNeeded = 1; + }); + return true; + } + catch (TimeoutException) + { + return false; + } + catch + { + return true; + } + } + #endregion + } +} diff --git a/ServerBrowser/Toxikk.cs b/ServerBrowser/Toxikk.cs index e312794..0eae43a 100644 --- a/ServerBrowser/Toxikk.cs +++ b/ServerBrowser/Toxikk.cs @@ -58,7 +58,7 @@ public override object GetServerCellValue(ServerRow row, string fieldName) return row.GetRule(ToxikkSkillInfo.MinSkillClass) + "-" + row.GetRule(ToxikkSkillInfo.MaxSkillClass); //return new ToxikkSkillInfo(row, this); case "_best": - return Math.Round(this.GetBestPlayerSC(row), MidpointRounding.AwayFromZero); + return Math.Round(this.GetBestPlayerSC(row), 1, MidpointRounding.AwayFromZero); case IsOfficial: return row.GetRule(fieldName) == "1"; case "_gametype": @@ -208,7 +208,7 @@ public override object GetPlayerCellValue(ServerRow server, Player player, strin if (fieldName == "Team") return info.Team; if (fieldName == "SC") - return info.SkillClass; + return Math.Round(info.SkillClass, 1, MidpointRounding.AwayFromZero); if (fieldName == "Rank") return info.Rank; return null; @@ -234,7 +234,7 @@ public override List GetPlayerContextMenu(ServerRow serve private void UpdatePlayerInfos(ServerRow server) { // no need for update if it's the same server and update timestamp - if (server == this.serverForPlayerInfos && server.RequestId == this.serverRequestId) + if (server == this.serverForPlayerInfos && server.RequestId == this.serverRequestId && playerInfos.Count > 0) return; this.serverForPlayerInfos = server; diff --git a/changelog.md b/changelog.md index 494f6cd..09e2a40 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,10 @@ +1.7 +--- +- added auto-refresh interval +- updating server list no longer clears the whole table +- added feature to set an alert when servers are found (after a refresh) which match the filter criteria (e.g. game type = CTF and human players >= 3) +- TOXIKK: showing best player's Skill Class in a "Best" column + 1.6.4 --- - added context menu to server list to allow update, connect, connect as spec, copy address to clipboard