diff --git a/ServerBrowser/Games/GameExtension.cs b/ServerBrowser/Games/GameExtension.cs index a541795..a6b2ef0 100644 --- a/ServerBrowser/Games/GameExtension.cs +++ b/ServerBrowser/Games/GameExtension.cs @@ -25,6 +25,8 @@ public class GameExtension protected bool supportsPlayersQuery = true; protected bool supportsConnectAsSpectator = false; + public Steamworks Steamworks { get; set; } + // menu public string OptionMenuCaption { get; protected set; } = null; @@ -82,7 +84,7 @@ public virtual bool AcceptGameServer(IPEndPoint server) } #endregion - public virtual void Refresh() { } + public virtual void Refresh(ServerRow row = null, Action callback = null) { } // server list UI @@ -255,7 +257,7 @@ public virtual void Rcon(ServerRow row, int port, string password, string comman return 0; } - public virtual bool IsValidPlayer(Player player) + public virtual bool IsValidPlayer(ServerRow row, Player player) { return true; } @@ -282,10 +284,12 @@ public PlayerContextMenuItem(string text, Action handler, bool isDefaultAction = public class GameExtensionPool : IEnumerable> { private readonly Dictionary extensions = new Dictionary(); + public Steamworks Steamworks { get; set; } public void Add(Game game, GameExtension extension) { extensions[game] = extension; + extension.Steamworks = this.Steamworks; } public GameExtension Get(Game game) @@ -294,7 +298,7 @@ public GameExtension Get(Game game) if (!this.extensions.TryGetValue(game, out extension)) { extension = new GameExtension(); - this.extensions.Add(game, extension); + this.Add(game, extension); } return extension; } diff --git a/ServerBrowser/Games/QuakeLive.cs b/ServerBrowser/Games/QuakeLive.cs index 9cbe3f5..fa8a2f8 100644 --- a/ServerBrowser/Games/QuakeLive.cs +++ b/ServerBrowser/Games/QuakeLive.cs @@ -6,6 +6,7 @@ using System.Text; using System.Text.RegularExpressions; using System.Threading; +using System.Linq; using System.Windows.Forms; using DevExpress.Data; using DevExpress.XtraEditors.Controls; @@ -25,15 +26,6 @@ namespace ServerBrowser { public class QuakeLive : GameExtension { - public class QlStatsSkillInfo - { - public string server { get; set; } - public string gt { get; set; } - public int min { get; set; } - public int avg { get; set; } - public int max { get; set; } - } - private const int SecondsToWaitForMainWindowAfterLaunch = 20; private static readonly Dictionary gameTypeName = new Dictionary @@ -52,13 +44,20 @@ public class QlStatsSkillInfo {12, "RR"} }; + private static readonly string[] TeamNames = { null, "Play", "Red", "Blue", "Spec" }; + private static readonly Regex NameColors = new Regex("\\^[0-9]"); private readonly Game steamAppId; private bool useKeystrokesToConnect; private bool startExtraQL; private string extraQlPath; - private static readonly DataContractJsonSerializer jsonParser = new DataContractJsonSerializer(typeof(QlStatsSkillInfo[])); + private static readonly DataContractJsonSerializer serverSkillJsonParser = new DataContractJsonSerializer(typeof(QlStatsSkillInfo[])); + private static readonly DataContractJsonSerializer personalSkillJsonParser = new DataContractJsonSerializer(typeof(QlstatsGlickoRating)); + private static readonly DataContractJsonSerializer playerListJsonParser = new DataContractJsonSerializer(typeof(QlstatsPlayerList)); private Dictionary skillInfo = new Dictionary(); + private GridColumn colSkill; + private const string SkillTooltip = "QLStats.net skill rating max/avg/min (*100)"; + private readonly Dictionary qlstatsPlayerlists = new Dictionary(); #region ctor() @@ -122,7 +121,9 @@ public override void CustomizeServerGridColumns(GridView view) .OptionsFilter.AutoFilterCondition = AutoFilterCondition.Default; idx = view.Columns["PlayerCount"].VisibleIndex; - AddColumn(view, "_skill", "Skill", "QLStats.net skill rating max/avg/min (*100)", 60, ++idx, UnboundColumnType.Integer); + this.colSkill = AddColumn(view, "_skill", "Skill", SkillTooltip, 60, ++idx, UnboundColumnType.Integer); + AddColumn(view, "_score", "Score", "Current score", 50, ++idx, UnboundColumnType.Integer); + //AddColumn(view, "_time", "Time", "Match time", 30, ++idx, UnboundColumnType.Integer); AddColumn(view, "_teamsize", "TS", "Team Size", 30, ++idx, UnboundColumnType.Integer); idx = view.Columns["ServerInfo.Ping"].VisibleIndex; @@ -163,10 +164,76 @@ public override void CustomizeServerGridColumns(GridView view) #endregion + #region CustomizePlayerGridColumns() + public override void CustomizePlayerGridColumns(GridView view) + { + base.CustomizePlayerGridColumns(view); + + AddColumn(view, "_team", "Team", "", 30, 0); + AddColumn(view, "_skill", "Skill", SkillTooltip, 30, 2, UnboundColumnType.Integer); + view.Columns["Time"].Visible = false; + AddColumn(view, "_time", "Time", "Play time since map start", 50); + } + #endregion + #region Refresh() - public override void Refresh() + public override void Refresh(ServerRow row = null, Action callback = null) + { + LoadQlstatsPersonalRating(); + LoadQlstatsServerRatings(); + if (row == null) + this.qlstatsPlayerlists.Clear(); + else + this.LoadQlstatsPlayerList(row, callback); + } + #endregion + + #region LoadQlstatsPersonalRating() + private void LoadQlstatsPersonalRating() + { + if (this.Steamworks == null) return; + var steamid = this.Steamworks.GetUserID(); + if (steamid == 0) return; + using (var client = new XWebClient(2000)) + { + client.DownloadStringCompleted += (sender, args) => + { + try + { + using (var strm = new MemoryStream(Encoding.UTF8.GetBytes(args.Result))) + { + var result = (QlstatsGlickoRating)personalSkillJsonParser.ReadObject(strm); + var text = "\n\nYour personal rating, (uncertainty), [games]:"; + foreach (var player in result.players) + { + var gametypes = new[] { "ffa", "ca", "duel", "ctf", "tdm", "ft" }; + var ratings = new[] { player.ffa, player.ca, player.duel, player.ctf, player.tdm, player.ft }; + for (int i = 0; i < gametypes.Length; i++) + { + var rating = ratings[i]; + if (rating == null) continue; + text += $"\n{gametypes[i].ToUpper()}: {rating.r_rd} ({rating.rd}) [{rating.games}]"; + } + if (gametypes.Length == 0) + text = ""; + } + + this.colSkill.ToolTip = SkillTooltip + text; + } + } + catch + { + } + }; + client.DownloadStringAsync(new Uri("http://qlstats.net:8080/glicko/" + steamid)); + } + } + + #endregion + + #region LoadQlstatsServerRatings() + private void LoadQlstatsServerRatings() { - // request server skill rating data from qlstats.net using (var client = new XWebClient(2000)) { client.DownloadStringCompleted += (sender, args) => @@ -175,7 +242,7 @@ public override void Refresh() { using (var strm = new MemoryStream(Encoding.UTF8.GetBytes(args.Result))) { - var servers = (QlStatsSkillInfo[]) jsonParser.ReadObject(strm); + var servers = (QlStatsSkillInfo[])serverSkillJsonParser.ReadObject(strm); var dict = new Dictionary(); foreach (var server in servers) dict[server.server] = server; @@ -191,6 +258,36 @@ public override void Refresh() } #endregion + #region LoadQlstatsPlayerList() + private void LoadQlstatsPlayerList(ServerRow row, Action callback = null) + { + this.qlstatsPlayerlists[row] = null; + using (var client = new XWebClient(2000)) + { + client.Encoding = Encoding.UTF8; + + client.DownloadStringCompleted += (sender, args) => + { + try + { + using (var strm = new MemoryStream(Encoding.UTF8.GetBytes(args.Result))) + { + var playerList = (QlstatsPlayerList) playerListJsonParser.ReadObject(strm); + this.qlstatsPlayerlists[row] = playerList; + callback?.Invoke(); + } + } + catch + { + } + }; + + client.DownloadStringAsync(new Uri("http://qlstats.net:8088/api/server/" + row.EndPoint + "/players")); + } + } + #endregion + + #region GetServerCellValue() public override object GetServerCellValue(ServerRow row, string fieldName) @@ -238,6 +335,33 @@ public override object GetServerCellValue(ServerRow row, string fieldName) return null; return "" + (skill.max + 50)/100 + "/" + (skill.avg + 50)/100 + "/" + (skill.min + 50)/100; } + case "_score": + { + var state = row.GetRule("g_gameState"); + if (state == "PRE_GAME") + return null; + var red = row.GetRule("g_redScore"); + var blue = row.GetRule("g_blueScore"); + int ired, iblue; + if (int.TryParse(red, out ired) && int.TryParse(blue, out iblue) && iblue > ired) + return "" + iblue + ":" + ired; + return "" + red + ":" + blue; + } + case "_time": + { + var time = row.GetRule("g_levelStartTime"); + if (time == null) return null; + int itime; + if (!int.TryParse(time, out itime)) return null; + var dt = new DateTime(1970, 1, 1).AddSeconds(itime); + var span = DateTime.UtcNow - dt; + var str = span.Minutes.ToString("d2") + ":" + span.Seconds.ToString("d2"); + if (span.TotalHours >= 1) + str = span.Hours.ToString("d") + "h " + str; + if (span.TotalDays >= 1) + str = ((int)span.TotalDays) + "d " + str; + return str; + } case "g_instaGib": return row.GetRule(fieldName) == "1"; case "g_loadout": @@ -250,18 +374,57 @@ public override object GetServerCellValue(ServerRow row, string fieldName) #endregion + #region IsValidPlayer() - public override bool IsValidPlayer(Player player) + public override bool IsValidPlayer(ServerRow server, Player player) { // hack to remove ghost players which are not really on the server - return player.Score > 0 || player.Time < TimeSpan.FromHours(4); + return player.Score > 0 || player.Time < TimeSpan.FromHours(1); + } + #endregion + + #region GetPlayerCellValue() + public override object GetPlayerCellValue(ServerRow server, Player player, string fieldName) + { + if (fieldName[0] != '_') + return base.GetPlayerCellValue(server, player, fieldName); + + QlstatsPlayerList list; + QlstatsPlayerList.Player playerInfo = null; + this.qlstatsPlayerlists.TryGetValue(server, out list); + if (list?.players != null) + { + var cleanName = this.GetCleanPlayerName(player.Name); + foreach (var ch in "'\"<>") // some special characters which QL returns in the server query but strips out of ZMQ names + cleanName = cleanName.Replace(ch.ToString(), ""); + playerInfo = list.players.FirstOrDefault(p => this.GetCleanPlayerName(p.name) == cleanName); + } + if (playerInfo == null) + { + if (fieldName == "_time") + return player.Time.ToString("hh\\:mm\\:ss"); + return null; + } + + if (fieldName == "_team") + return TeamNames[playerInfo.team + 1]; + if (fieldName == "_skill") + return playerInfo.rating; //(playerInfo.rating+50)/100; + if (fieldName == "_time") + return (DateTime.UtcNow - new DateTime(1970, 1, 1).AddMilliseconds(playerInfo.time)).ToString("hh\\:mm\\:ss"); + + return null; } #endregion #region GetCleanPlayerName() public override string GetCleanPlayerName(Player player) { - return NameColors.Replace(player.Name, ""); + return this.GetCleanPlayerName(player.Name); + } + private string GetCleanPlayerName(string name) + { + return name == null ? null : NameColors.Replace(name, ""); } #endregion @@ -311,6 +474,26 @@ public override string GetCleanPlayerName(Player player) } #endregion + #region CustomizePlayerContextMenu() + public override void CustomizePlayerContextMenu(ServerRow server, Player player, List menu) + { + QlstatsPlayerList list; + if (!this.qlstatsPlayerlists.TryGetValue(server, out list)) + return; + var cleanName = this.GetCleanPlayerName(player); + var info = list.players.FirstOrDefault(p => this.GetCleanPlayerName(p.name) == cleanName); + if (info == null) + return; + + menu.Insert(0, new PlayerContextMenuItem("Open Steam Chat", () => { Process.Start("steam://friends/message/" + info.steamid); }, true)); + menu.Insert(1, new PlayerContextMenuItem("Show Steam Profile", () => { Process.Start("http://steamcommunity.com/profiles/" + info.steamid + "/"); })); + menu.Insert(2, new PlayerContextMenuItem("Show QLStats Profile", () => { Process.Start("http://qlstats.net:8080/player/" + info.steamid); })); + menu.Insert(3, new PlayerContextMenuItem("Add to Steam Friends", () => { Process.Start("steam://friends/add/" + info.steamid); })); + menu.Add(new PlayerContextMenuItem("Copy Steam-ID to Clipboard", () => { Clipboard.SetText(info.steamid); })); + } + #endregion + + #region Connect() public override bool Connect(ServerRow server, string password, bool spectate) @@ -426,9 +609,60 @@ private void SkipIntro(IntPtr win) #endregion + + + #region class QlStatsSkillInfo + public class QlStatsSkillInfo + { + public string server; + public string gt; + public int min; + public int avg; + public int max; + } + #endregion + + #region class QlstatsGlickoRating + + public class QlstatsGlickoRating + { + public class GametypeRating + { + public int games; + public int r_rd; + public int rd; + } + public class Players + { + public string steamid; + public GametypeRating ffa, ca, duel, ctf, tdm, ft; + } + + public Players[] players; + } + #endregion + + #region class QlstatsPlayerList + public class QlstatsPlayerList + { + public class Player + { + public string steamid; + public string name; + public int team; + public long time; + public int rating; + } + + public bool ok; + public Player[] players; + } + #endregion + + // ZeroMQ rcon stuff #if ZMQ -#region Rcon() + #region Rcon() public override void Rcon(ServerRow row, int port, string password, string command) { @@ -480,9 +714,9 @@ public override void Rcon(ServerRow row, int port, string password, string comma } } } -#endregion + #endregion -#region CreateClientAndMonitorSockets() + #region CreateClientAndMonitorSockets() private void CreateClientAndMonitorSockets(ZContext ctx, IPEndPoint endPoint, string password, out ZSocket client, out ZSocket monitor) { client = new ZSocket(ctx, ZSocketType.DEALER); @@ -502,9 +736,9 @@ private void CreateClientAndMonitorSockets(ZContext ctx, IPEndPoint endPoint, st client.SetOption(ZSocketOption.IDENTITY, ident); client.Connect("tcp://" + endPoint); } -#endregion + #endregion -#region CheckMonitor() + #region CheckMonitor() private Tuple CheckMonitor(ZSocket monitor) { try @@ -528,7 +762,7 @@ private Tuple CheckMonitor(ZSocket monitor) return null; } } -#endregion + #endregion #endif } } \ No newline at end of file diff --git a/ServerBrowser/Games/Toxikk.cs b/ServerBrowser/Games/Toxikk.cs index e37594d..09149de 100644 --- a/ServerBrowser/Games/Toxikk.cs +++ b/ServerBrowser/Games/Toxikk.cs @@ -137,6 +137,15 @@ public override object GetServerCellValue(ServerRow row, string fieldName) } #endregion + #region IsValidPlayer() + public override bool IsValidPlayer(ServerRow server, Player player) + { + // hack to remove ghost players which are not really on the server + this.UpdatePlayerInfos(server); + return server.Rules != null && !string.IsNullOrEmpty(player.Name) && this.playerInfos.ContainsKey(player.Name); + } + #endregion + #region GetBotCount() public override int? GetBotCount(ServerRow row) { @@ -418,6 +427,8 @@ private void UpdatePlayerInfos(ServerRow server) this.playerInfos.Add(name, info); ++i; } + + server.PlayerCount.Update(); } #endregion } diff --git a/ServerBrowser/PlayerCountInfo.cs b/ServerBrowser/PlayerCountInfo.cs index 1b50463..65b4343 100644 --- a/ServerBrowser/PlayerCountInfo.cs +++ b/ServerBrowser/PlayerCountInfo.cs @@ -55,7 +55,7 @@ public void Update() } else if (row.Players.Count > 0) // some games always return an empty list { - int? count = row.Players.Count(p => !string.IsNullOrEmpty(p.Name) && row.GameExtension.IsValidPlayer(p)); + int? count = row.Players.Count(p => !string.IsNullOrEmpty(p.Name) && row.GameExtension.IsValidPlayer(row, p)); // some games (CS:GO, TF2, QuakeLive) return bots in the player list if (count >= Bots) diff --git a/ServerBrowser/Properties/licenses.licx b/ServerBrowser/Properties/licenses.licx index 850cdbf..2785e37 100644 --- a/ServerBrowser/Properties/licenses.licx +++ b/ServerBrowser/Properties/licenses.licx @@ -1,8 +1,8 @@ -DevExpress.XtraBars.Docking.DockManager, DevExpress.XtraBars.v15.1, Version=15.1.8.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a -DevExpress.XtraEditors.Repository.RepositoryItemComboBox, DevExpress.XtraEditors.v15.1, Version=15.1.8.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a -DevExpress.XtraEditors.ComboBoxEdit, DevExpress.XtraEditors.v15.1, Version=15.1.8.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a -DevExpress.XtraEditors.CheckEdit, DevExpress.XtraEditors.v15.1, Version=15.1.8.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a -DevExpress.XtraEditors.Repository.RepositoryItemImageComboBox, DevExpress.XtraEditors.v15.1, Version=15.1.8.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a -DevExpress.XtraEditors.Repository.RepositoryItemCheckEdit, DevExpress.XtraEditors.v15.1, Version=15.1.8.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a -DevExpress.XtraGrid.GridControl, DevExpress.XtraGrid.v15.1, Version=15.1.8.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a -DevExpress.XtraEditors.ButtonEdit, DevExpress.XtraEditors.v15.1, Version=15.1.8.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a +DevExpress.XtraEditors.ButtonEdit, DevExpress.XtraEditors.v15.2, Version=15.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a +DevExpress.XtraBars.Docking.DockManager, DevExpress.XtraBars.v15.2, Version=15.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a +DevExpress.XtraEditors.ComboBoxEdit, DevExpress.XtraEditors.v15.2, Version=15.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a +DevExpress.XtraEditors.Repository.RepositoryItemImageComboBox, DevExpress.XtraEditors.v15.2, Version=15.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a +DevExpress.XtraEditors.Repository.RepositoryItemCheckEdit, DevExpress.XtraEditors.v15.2, Version=15.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a +DevExpress.XtraGrid.GridControl, DevExpress.XtraGrid.v15.2, Version=15.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a +DevExpress.XtraEditors.CheckEdit, DevExpress.XtraEditors.v15.2, Version=15.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a +DevExpress.XtraEditors.Repository.RepositoryItemComboBox, DevExpress.XtraEditors.v15.2, Version=15.2.4.0, Culture=neutral, PublicKeyToken=b88d1754d700e49a diff --git a/ServerBrowser/ServerBrowser.csproj b/ServerBrowser/ServerBrowser.csproj index c3c344c..8eafda4 100644 --- a/ServerBrowser/ServerBrowser.csproj +++ b/ServerBrowser/ServerBrowser.csproj @@ -77,41 +77,41 @@ CS1591 - + False True - + False True - + False True - + False True - + False True - + False True - + False True - + False True - - - + + + @@ -189,6 +189,7 @@ ConnectingWaitForm.cs + Component diff --git a/ServerBrowser/ServerBrowserForm.Designer.cs b/ServerBrowser/ServerBrowserForm.Designer.cs index aa6046d..bf679bc 100644 --- a/ServerBrowser/ServerBrowserForm.Designer.cs +++ b/ServerBrowser/ServerBrowserForm.Designer.cs @@ -88,6 +88,7 @@ private void InitializeComponent() this.miShowOptions = new DevExpress.XtraBars.BarButtonItem(); this.miShowServerQuery = new DevExpress.XtraBars.BarButtonItem(); this.miShowFilter = new DevExpress.XtraBars.BarButtonItem(); + this.miRestoreStandardLayout = new DevExpress.XtraBars.BarButtonItem(); this.mnuGameOptions = new DevExpress.XtraBars.BarLinkContainerItem(); this.mnuTabs = new DevExpress.XtraBars.BarSubItem(); this.miRenameTab = new DevExpress.XtraBars.BarButtonItem(); @@ -220,7 +221,6 @@ private void InitializeComponent() this.menuDetails = new DevExpress.XtraBars.PopupMenu(this.components); this.splashScreenManager1 = new DevExpress.XtraSplashScreen.SplashScreenManager(this, typeof(global::ServerBrowser.ConnectingWaitForm), false, true); this.timerHideWaitForm = new System.Windows.Forms.Timer(this.components); - this.miRestoreStandardLayout = new DevExpress.XtraBars.BarButtonItem(); ((System.ComponentModel.ISupportInitialize)(this.riCheckEdit)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.gcDetails)).BeginInit(); ((System.ComponentModel.ISupportInitialize)(this.gvDetails)).BeginInit(); @@ -383,9 +383,13 @@ private void InitializeComponent() this.colTime}); this.gvPlayers.GridControl = this.gcPlayers; this.gvPlayers.Name = "gvPlayers"; + this.gvPlayers.OptionsBehavior.Editable = false; this.gvPlayers.OptionsView.ShowGroupPanel = false; this.gvPlayers.OptionsView.ShowIndicator = false; + this.gvPlayers.SortInfo.AddRange(new DevExpress.XtraGrid.Columns.GridColumnSortInfo[] { + new DevExpress.XtraGrid.Columns.GridColumnSortInfo(this.colScore, DevExpress.Data.ColumnSortOrder.Descending)}); this.gvPlayers.CustomUnboundColumnData += new DevExpress.XtraGrid.Views.Base.CustomColumnDataEventHandler(this.gvPlayers_CustomUnboundColumnData); + this.gvPlayers.CustomRowFilter += new DevExpress.XtraGrid.Views.Base.RowFilterEventHandler(this.gvPlayers_CustomRowFilter); this.gvPlayers.MouseDown += new System.Windows.Forms.MouseEventHandler(this.gvPlayers_MouseDown); this.gvPlayers.DoubleClick += new System.EventHandler(this.gvPlayers_DoubleClick); // @@ -1257,6 +1261,13 @@ private void InitializeComponent() this.miShowFilter.Name = "miShowFilter"; this.miShowFilter.DownChanged += new DevExpress.XtraBars.ItemClickEventHandler(this.miShowFilter_DownChanged); // + // miRestoreStandardLayout + // + this.miRestoreStandardLayout.Caption = "Restore Standard Layout"; + this.miRestoreStandardLayout.Id = 34; + this.miRestoreStandardLayout.Name = "miRestoreStandardLayout"; + this.miRestoreStandardLayout.ItemClick += new DevExpress.XtraBars.ItemClickEventHandler(this.miRestoreStandardLayout_ItemClick); + // // mnuGameOptions // this.mnuGameOptions.Caption = "Game options"; @@ -2795,13 +2806,6 @@ private void InitializeComponent() this.timerHideWaitForm.Interval = 5000; this.timerHideWaitForm.Tick += new System.EventHandler(this.timerHideWaitForm_Tick); // - // miRestoreStandardLayout - // - this.miRestoreStandardLayout.Caption = "Restore Standard Layout"; - this.miRestoreStandardLayout.Id = 34; - this.miRestoreStandardLayout.Name = "miRestoreStandardLayout"; - this.miRestoreStandardLayout.ItemClick += new DevExpress.XtraBars.ItemClickEventHandler(this.miRestoreStandardLayout_ItemClick); - // // ServerBrowserForm // this.Appearance.Options.UseFont = true; diff --git a/ServerBrowser/ServerBrowserForm.cs b/ServerBrowser/ServerBrowserForm.cs index 8646312..5f55ac9 100644 --- a/ServerBrowser/ServerBrowserForm.cs +++ b/ServerBrowser/ServerBrowserForm.cs @@ -30,7 +30,7 @@ namespace ServerBrowser { public partial class ServerBrowserForm : XtraForm { - private const string Version = "2.23"; + private const string Version = "2.24"; private const string DevExpressVersion = "v15.1"; private const string CustomDetailColumnPrefix = "ServerInfo."; private const string CustomRuleColumnPrefix = "custRule."; @@ -46,7 +46,7 @@ public partial class ServerBrowserForm : XtraForm private readonly ServerQueryLogic queryLogic; private readonly string geoIpCachePath; private readonly GeoIpClient geoIpClient; - private readonly Steamworks steam = new Steamworks(); + private readonly Steamworks steam; private readonly MemoryStream defaultLayout = new MemoryStream(); private int geoIpModified; private readonly Dictionary favServers = new Dictionary(); @@ -70,6 +70,9 @@ public ServerBrowserForm() this.iniFile = new IniFile(iniPath); this.geoIpClient = new GeoIpClient(this.geoIpCachePath); + this.steam = new Steamworks(); + this.steam.Init(); + this.InitGameInfoExtenders(this.iniFile); this.queryLogic = new ServerQueryLogic(this.extenders); @@ -131,6 +134,7 @@ private void MoveConfigFilesFromOldLocation() private void InitGameInfoExtenders(IniFile ini) { + extenders.Steamworks = this.steam; extenders.Add(Game.Toxikk, new Toxikk()); extenders.Add(Game.Reflex, new Reflex()); extenders.Add(Game.QuakeLive, new QuakeLive(Game.QuakeLive)); @@ -794,8 +798,7 @@ protected void ReloadServerList() filter.Nor.Sv_Tags = this.ParseTags(this.viewModel.TagsExcludeServer); } this.CustomizeFilter(filter); - foreach (var ext in this.extenders) - ext.Value.Refresh(); + this.RefreshGameExtensions(); this.queryLogic.ReloadServerList(this.viewModel.serverSource, 750, this.viewModel.MasterServerQueryLimit, QueryMaster.Region.Rest_of_the_world, filter); } #endregion @@ -808,12 +811,19 @@ private void RefreshServerInfo() this.miStopUpdate.Enabled = true; this.timerReloadServers.Stop(); this.SetStatusMessage("Updating status of " + this.viewModel.servers.Count + " servers..."); - foreach (var ext in this.extenders) - ext.Value.Refresh(); + this.RefreshGameExtensions(); this.queryLogic.RefreshAllServers(this.viewModel.servers); } #endregion + #region RefreshGameExtensions() + private void RefreshGameExtensions() + { + // give game extenders a chance to update their data as well (i.e. from external sources) + foreach (var ext in this.extenders) + ext.Value.Refresh(); + } + #endregion #region FilterServerRow() @@ -972,7 +982,7 @@ private void UpdateCachedServerNames() #endregion #region UpdateGridDataSources() - private void UpdateGridDataSources() + private void UpdateGridDataSources(bool isCallback = false) { var row = (ServerRow)this.gvServers.GetFocusedRow(); this.viewModel.currentServer = row; @@ -993,6 +1003,7 @@ private void UpdateGridDataSources() var curSelName = (gvPlayers.GetFocusedRow() as Player)?.Name; this.gcPlayers.DataSource = row?.Players; this.gvPlayers.EndDataUpdate(); + this.gvPlayers.ExpandAllGroups(); if (curSelName != null) { int idx = row?.Players?.FindIndex(p => p.Name == curSelName) ?? -1; @@ -1005,6 +1016,14 @@ private void UpdateGridDataSources() this.gvRules.EndDataUpdate(); this.UpdateServerContextMenu(); + + if (row != null && !isCallback) + { + row.GameExtension.Refresh(row, () => + { + this.BeginInvoke((Action) (() => { this.UpdateGridDataSources(true); })); + }); + } } #endregion @@ -1718,7 +1737,7 @@ private void timerReloadServers_Tick(object sender, EventArgs e) if (this.cbNoUpdateWhilePlaying.Checked && this.cbUseSteamApi.Checked) { - if (steam.Init() && steam.IsInGame()) + if (steam.IsInGame()) return; } @@ -1953,6 +1972,7 @@ private void gvServers_CustomColumnSort(object sender, CustomColumnSortEventArgs #region miUpdateServerInfo_ItemClick private void miUpdateServerInfo_ItemClick(object sender, ItemClickEventArgs e) { + this.RefreshGameExtensions(); if (this.gvServers.SelectedRowsCount == 1) this.queryLogic.RefreshSingleServer((ServerRow) this.gvServers.GetFocusedRow()); else @@ -1961,8 +1981,7 @@ private void miUpdateServerInfo_ItemClick(object sender, ItemClickEventArgs e) foreach (var handle in this.gvServers.GetSelectedRows()) list.Add((ServerRow)this.gvServers.GetRow(handle)); this.queryLogic.RefreshAllServers(list); - } - + } } #endregion @@ -2087,6 +2106,24 @@ private void btnPasteAddresses_Click(object sender, EventArgs e) // Players grid + #region gvPlayers_CustomRowFilter + private void gvPlayers_CustomRowFilter(object sender, RowFilterEventArgs e) + { + // hide players based on GameExtension.IsValidPlayer() - e.g. ghost connections + var server = (ServerRow)this.gvServers.GetFocusedRow(); + if (server == null) return; + var players = server.Players; + if (players == null) return; + if (e.ListSourceRow >= players.Count) return; + var player = players[e.ListSourceRow]; + if (!server.GameExtension.IsValidPlayer(server, player)) + { + e.Visible = false; + e.Handled = true; + } + } + #endregion + #region gvPlayers_CustomUnboundColumnData private void gvPlayers_CustomUnboundColumnData(object sender, CustomColumnDataEventArgs e) { @@ -2398,6 +2435,5 @@ private void miNewFavoritesTab_ItemClick(object sender, ItemClickEventArgs e) this.AddNewTab("New Favorites", TabViewModel.SourceType.Favorites); } #endregion - } } \ No newline at end of file diff --git a/ServerBrowser/ServerBrowserForm.resx b/ServerBrowser/ServerBrowserForm.resx index 50aca4c..57dbd0b 100644 --- a/ServerBrowser/ServerBrowserForm.resx +++ b/ServerBrowser/ServerBrowserForm.resx @@ -126,11 +126,11 @@ 1692, 17 - - + + - AAEAAAD/////AQAAAAAAAAAMAgAAAFpEZXZFeHByZXNzLlV0aWxzLnYxNS4xLCBWZXJzaW9uPTE1LjEu - OC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI4OGQxNzU0ZDcwMGU0OWEMAwAAAFFT + AAEAAAD/////AQAAAAAAAAAMAgAAAFpEZXZFeHByZXNzLlV0aWxzLnYxNS4yLCBWZXJzaW9uPTE1LjIu + NC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI4OGQxNzU0ZDcwMGU0OWEMAwAAAFFT eXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRv a2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAAChEZXZFeHByZXNzLlV0aWxzLkltYWdlQ29sbGVjdGlvblN0 cmVhbWVyAgAAAAlJbWFnZVNpemUERGF0YQQHE1N5c3RlbS5EcmF3aW5nLlNpemUDAAAAAgIAAAAF/P// @@ -3811,10 +3811,10 @@ 1440, 17 - + - AAEAAAD/////AQAAAAAAAAAMAgAAAFpEZXZFeHByZXNzLlV0aWxzLnYxNS4xLCBWZXJzaW9uPTE1LjEu - OC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI4OGQxNzU0ZDcwMGU0OWEMAwAAAFFT + AAEAAAD/////AQAAAAAAAAAMAgAAAFpEZXZFeHByZXNzLlV0aWxzLnYxNS4yLCBWZXJzaW9uPTE1LjIu + NC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRva2VuPWI4OGQxNzU0ZDcwMGU0OWEMAwAAAFFT eXN0ZW0uRHJhd2luZywgVmVyc2lvbj00LjAuMC4wLCBDdWx0dXJlPW5ldXRyYWwsIFB1YmxpY0tleVRv a2VuPWIwM2Y1ZjdmMTFkNTBhM2EFAQAAAChEZXZFeHByZXNzLlV0aWxzLkltYWdlQ29sbGVjdGlvblN0 cmVhbWVyAgAAAAlJbWFnZVNpemUERGF0YQQHE1N5c3RlbS5EcmF3aW5nLlNpemUDAAAAAgIAAAAF/P// diff --git a/ServerBrowser/ServerQueryLogic.cs b/ServerBrowser/ServerQueryLogic.cs index 18aab17..f50f85e 100644 --- a/ServerBrowser/ServerQueryLogic.cs +++ b/ServerBrowser/ServerQueryLogic.cs @@ -101,6 +101,8 @@ public bool GetAndResetDataModified() public event EventHandler ReloadServerListComplete; public event EventHandler RefreshSingleServerComplete; + private readonly ThrottledThreadPool ThreadPool = new ThrottledThreadPool(50); + #region ctor() public ServerQueryLogic(GameExtensionPool gameExtensions) { diff --git a/ServerBrowser/Steamworks.cs b/ServerBrowser/Steamworks.cs index 1ef449f..da52363 100644 --- a/ServerBrowser/Steamworks.cs +++ b/ServerBrowser/Steamworks.cs @@ -6,7 +6,7 @@ namespace ServerBrowser { - class Steamworks : IDisposable + public class Steamworks : IDisposable { #region DllImport diff --git a/ServerBrowser/ThrottledThreadPool.cs b/ServerBrowser/ThrottledThreadPool.cs new file mode 100644 index 0000000..4fb3f0e --- /dev/null +++ b/ServerBrowser/ThrottledThreadPool.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; + +namespace ServerBrowser +{ + class ThrottledThreadPool + { + private readonly BlockingCollection queue = new BlockingCollection(); + private readonly Semaphore semaphore; + + public ThrottledThreadPool(int concurrency = 10) + { + this.semaphore = new Semaphore(concurrency, concurrency); + var thread = new Thread(this.TaskDispatcher); + thread.Name = "XThreadPool Dispatcher"; + thread.IsBackground = true; + thread.Start(); + } + + public void QueueUserWorkItem(WaitCallback handler, object state = null) + { + queue.Add(() => handler(state)); + } + + private void TaskDispatcher() + { + while (true) + { + var task = queue.Take(); + this.semaphore.WaitOne(); + ThreadPool.QueueUserWorkItem(state => + { + try + { + task(); + } + finally + { + this.semaphore.Release(); + } + }); + } + } + } +} diff --git a/changelog.md b/changelog.md index 4797b1c..581cdcb 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,13 @@ +2.24 +--- +- Quake Live: added "Score" column +- Quake Live: added "Team" and "Skill" columns to player list (using data from qlstats.net) +- Quake Live: added context menu to Player list with options for steam chat, friend request, steam profile, qlstats profile +- Quake Live: changed ghost-player time limit from 4h to 1h +- Ghost players are also removed from the player list now +- TOXIKK: remove "ghost players" from player counts +- Player list is sorted by score + 2.23 --- - Quake Live: remove "ghost players" (dead connections in the player list) from player counts