From 6ec93bd8cfd253b40694c77655c6bb5f2aca6b00 Mon Sep 17 00:00:00 2001 From: Paul Chote Date: Sat, 7 Jul 2018 17:10:58 +0000 Subject: [PATCH] Add player badges. --- OpenRA.Game/PlayerDatabase.cs | 69 +++++++++++ OpenRA.Game/PlayerProfile.cs | 45 +++++++ .../Widgets/Logic/PlayerProfileLogic.cs | 116 +++++++++++++++++- mods/cnc/chrome/playerprofile.yaml | 24 ++++ mods/cnc/chrome/tooltips.yaml | 5 + mods/common/chrome/playerprofile.yaml | 25 ++++ mods/common/chrome/tooltips.yaml | 8 ++ mods/d2k/chrome.yaml | 3 + mods/d2k/chrome/tooltips.yaml | 8 ++ mods/ra/chrome.yaml | 3 + mods/ts/chrome.yaml | 3 + 11 files changed, 306 insertions(+), 3 deletions(-) diff --git a/OpenRA.Game/PlayerDatabase.cs b/OpenRA.Game/PlayerDatabase.cs index fdfff950b7..cc00c66574 100644 --- a/OpenRA.Game/PlayerDatabase.cs +++ b/OpenRA.Game/PlayerDatabase.cs @@ -9,10 +9,79 @@ */ #endregion +using System; +using System.Collections.Generic; +using System.Drawing; +using System.IO; +using System.Linq; +using System.Net; +using OpenRA.Graphics; + namespace OpenRA { public class PlayerDatabase : IGlobalModData { public readonly string Profile = "https://forum.openra.net/openra/info/"; + + [FieldLoader.Ignore] + readonly object syncObject = new object(); + + [FieldLoader.Ignore] + readonly Dictionary spriteCache = new Dictionary(); + + // 128x128 is large enough for 25 unique 24x24 sprites + [FieldLoader.Ignore] + SheetBuilder sheetBuilder; + + public PlayerBadge LoadBadge(MiniYaml yaml) + { + if (sheetBuilder == null) + { + sheetBuilder = new SheetBuilder(SheetType.BGRA, 128); + + // We must manually force the buffer creation to avoid a crash + // that is indirectly triggered by rendering from a Sheet that + // has not yet been written to. + sheetBuilder.Current.CreateBuffer(); + } + + var labelNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Label"); + var icon24Node = yaml.Nodes.FirstOrDefault(n => n.Key == "Icon24"); + if (labelNode == null || icon24Node == null) + return null; + + Sprite sprite; + lock (syncObject) + { + if (!spriteCache.TryGetValue(icon24Node.Value.Value, out sprite)) + { + sprite = spriteCache[icon24Node.Value.Value] = sheetBuilder.Allocate(new Size(24, 24)); + + Action onComplete = i => + { + if (i.Error != null) + return; + + try + { + var icon = new Bitmap(new MemoryStream(i.Result)); + if (icon.Width == 24 && icon.Height == 24) + { + Game.RunAfterTick(() => + { + Util.FastCopyIntoSprite(sprite, icon); + sprite.Sheet.CommitBufferedData(); + }); + } + } + catch { } + }; + + new Download(icon24Node.Value.Value, _ => { }, onComplete); + } + } + + return new PlayerBadge(labelNode.Value.Value, sprite); + } } } diff --git a/OpenRA.Game/PlayerProfile.cs b/OpenRA.Game/PlayerProfile.cs index 33ac290adf..1c18d19bba 100644 --- a/OpenRA.Game/PlayerProfile.cs +++ b/OpenRA.Game/PlayerProfile.cs @@ -9,6 +9,10 @@ */ #endregion +using System.Collections.Generic; +using System.Linq; +using OpenRA.Graphics; + namespace OpenRA { public class PlayerProfile @@ -20,5 +24,46 @@ namespace OpenRA public readonly int ProfileID; public readonly string ProfileName; public readonly string ProfileRank = "Registered Player"; + + [FieldLoader.LoadUsing("LoadBadges")] + public readonly List Badges; + + static object LoadBadges(MiniYaml yaml) + { + var badges = new List(); + + var badgesNode = yaml.Nodes.FirstOrDefault(n => n.Key == "Badges"); + if (badgesNode != null) + { + try + { + var playerDatabase = Game.ModData.Manifest.Get(); + foreach (var badgeNode in badgesNode.Value.Nodes) + { + var badge = playerDatabase.LoadBadge(badgeNode.Value); + if (badge != null) + badges.Add(badge); + } + } + catch + { + // Discard badges on error + } + } + + return badges; + } + } + + public class PlayerBadge + { + public readonly string Label; + public readonly Sprite Icon24; + + public PlayerBadge(string label, Sprite icon24) + { + Label = label; + Icon24 = icon24; + } } } diff --git a/OpenRA.Mods.Common/Widgets/Logic/PlayerProfileLogic.cs b/OpenRA.Mods.Common/Widgets/Logic/PlayerProfileLogic.cs index 753236d51c..d3a19799d2 100644 --- a/OpenRA.Mods.Common/Widgets/Logic/PlayerProfileLogic.cs +++ b/OpenRA.Mods.Common/Widgets/Logic/PlayerProfileLogic.cs @@ -21,12 +21,18 @@ namespace OpenRA.Mods.Common.Widgets.Logic { public class LocalProfileLogic : ChromeLogic { + readonly WorldRenderer worldRenderer; readonly LocalPlayerProfile localProfile; + readonly Widget badgeContainer; + readonly Widget widget; bool notFound; + bool badgesVisible; [ObjectCreator.UseCtor] public LocalProfileLogic(Widget widget, WorldRenderer worldRenderer, Func minimalProfile) { + this.worldRenderer = worldRenderer; + this.widget = widget; localProfile = Game.LocalPlayerProfile; // Key registration @@ -78,6 +84,10 @@ namespace OpenRA.Mods.Common.Widgets.Logic destroyKey.OnClick = localProfile.DeleteKeypair; destroyKey.IsDisabled = minimalProfile; + badgeContainer = widget.Get("BADGES_CONTAINER"); + badgeContainer.IsVisible = () => badgesVisible && !minimalProfile() + && localProfile.State == LocalPlayerProfile.LinkState.Linked; + localProfile.RefreshPlayerData(() => RefreshComplete(false)); } @@ -86,7 +96,36 @@ namespace OpenRA.Mods.Common.Widgets.Logic if (updateNotFound) notFound = localProfile.State == LocalPlayerProfile.LinkState.Unlinked; - Game.RunAfterTick(Ui.ResetTooltips); + Game.RunAfterTick(() => + { + badgesVisible = false; + + if (localProfile.State == LocalPlayerProfile.LinkState.Linked) + { + if (localProfile.ProfileData.Badges.Any()) + { + Func negotiateWidth = _ => widget.Get("PROFILE_HEADER").Bounds.Width; + + // Remove any stale badges that may be left over from a previous session + badgeContainer.RemoveChildren(); + + var badges = Ui.LoadWidget("PLAYER_PROFILE_BADGES_INSERT", badgeContainer, new WidgetArgs() + { + { "worldRenderer", worldRenderer }, + { "profile", localProfile.ProfileData }, + { "negotiateWidth", negotiateWidth } + }); + + if (badges.Bounds.Height > 0) + { + badgeContainer.Bounds.Height = badges.Bounds.Height; + badgesVisible = true; + } + } + } + + Ui.ResetTooltips(); + }); } } @@ -102,6 +141,8 @@ namespace OpenRA.Mods.Common.Widgets.Logic playerDatabase = modData.Manifest.Get(); var header = widget.Get("HEADER"); + var badgeContainer = widget.Get("BADGES_CONTAINER"); + var badgeSeparator = badgeContainer.GetOrNull("SEPARATOR"); var profileHeader = header.Get("PROFILE_HEADER"); var messageHeader = header.Get("MESSAGE_HEADER"); @@ -145,6 +186,7 @@ namespace OpenRA.Mods.Common.Widgets.Logic profileWidth = Math.Max(profileWidth, rankFont.Measure(profile.ProfileRank).X + 2 * rankLabel.Bounds.Left); header.Bounds.Height += headerSizeOffset; + badgeContainer.Bounds.Y += header.Bounds.Height; if (client.IsAdmin) { profileWidth = Math.Max(profileWidth, adminFont.Measure(adminLabel.Text).X + 2 * adminLabel.Bounds.Left); @@ -152,11 +194,37 @@ namespace OpenRA.Mods.Common.Widgets.Logic adminContainer.IsVisible = () => true; profileHeader.Bounds.Height += adminLabel.Bounds.Height; header.Bounds.Height += adminLabel.Bounds.Height; + badgeContainer.Bounds.Y += adminLabel.Bounds.Height; + } + + Func negotiateWidth = badgeWidth => + { + profileWidth = Math.Min(Math.Max(badgeWidth, profileWidth), widget.Bounds.Width); + return profileWidth; + }; + + if (profile.Badges.Any()) + { + var badges = Ui.LoadWidget("PLAYER_PROFILE_BADGES_INSERT", badgeContainer, new WidgetArgs() + { + { "worldRenderer", worldRenderer }, + { "profile", profile }, + { "negotiateWidth", negotiateWidth } + }); + + if (badges.Bounds.Height > 0) + { + badgeContainer.Bounds.Height = badges.Bounds.Height; + badgeContainer.IsVisible = () => true; + } } profileWidth = Math.Min(profileWidth, widget.Bounds.Width); - header.Bounds.Width = widget.Bounds.Width = profileWidth; - widget.Bounds.Height = header.Bounds.Height; + header.Bounds.Width = widget.Bounds.Width = badgeContainer.Bounds.Width = profileWidth; + widget.Bounds.Height = header.Bounds.Height + badgeContainer.Bounds.Height; + + if (badgeSeparator != null) + badgeSeparator.Bounds.Width = profileWidth - 2 * badgeSeparator.Bounds.X; profileLoaded = true; }); @@ -182,11 +250,53 @@ namespace OpenRA.Mods.Common.Widgets.Logic header.Bounds.Height += messageHeader.Bounds.Height; header.Bounds.Width = widget.Bounds.Width = messageWidth; widget.Bounds.Height = header.Bounds.Height; + badgeContainer.Visible = false; new Download(playerDatabase.Profile + client.Fingerprint, _ => { }, onQueryComplete); } } + public class PlayerProfileBadgesLogic : ChromeLogic + { + [ObjectCreator.UseCtor] + public PlayerProfileBadgesLogic(Widget widget, PlayerProfile profile, Func negotiateWidth) + { + var showBadges = profile.Badges.Any(); + widget.IsVisible = () => showBadges; + + var badgeTemplate = widget.Get("BADGE_TEMPLATE"); + widget.RemoveChild(badgeTemplate); + + var width = 0; + var badgeOffset = badgeTemplate.Bounds.Y; + foreach (var badge in profile.Badges) + { + var b = badgeTemplate.Clone(); + var icon = b.Get("ICON"); + icon.GetSprite = () => badge.Icon24; + + var label = b.Get("LABEL"); + var labelFont = Game.Renderer.Fonts[label.Font]; + + var labelText = WidgetUtils.TruncateText(badge.Label, label.Bounds.Width, labelFont); + label.GetText = () => labelText; + + width = Math.Max(width, label.Bounds.Left + labelFont.Measure(labelText).X + icon.Bounds.X); + + b.Bounds.Y = badgeOffset; + widget.AddChild(b); + + badgeOffset += badgeTemplate.Bounds.Height; + } + + if (badgeOffset > badgeTemplate.Bounds.Y) + badgeOffset += 5; + + widget.Bounds.Width = negotiateWidth(width); + widget.Bounds.Height = badgeOffset; + } + } + public class AnonymousProfileTooltipLogic : ChromeLogic { [ObjectCreator.UseCtor] diff --git a/mods/cnc/chrome/playerprofile.yaml b/mods/cnc/chrome/playerprofile.yaml index 85f226e9ed..c340c78869 100644 --- a/mods/cnc/chrome/playerprofile.yaml +++ b/mods/cnc/chrome/playerprofile.yaml @@ -28,6 +28,11 @@ Container@LOCAL_PROFILE_PANEL: Font: TinyBold BaseLine: 1 Text: Logout + Background@BADGES_CONTAINER: + Width: PARENT_RIGHT + Y: 48 + Visible: false + Background: panel-black Background@GENERATE_KEYS: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -208,3 +213,22 @@ Container@LOCAL_PROFILE_PANEL: BaseLine: 1 Font: TinyBold Text: Retry + +Container@PLAYER_PROFILE_BADGES_INSERT: + Logic: PlayerProfileBadgesLogic + Width: PARENT_RIGHT + Children: + Container@BADGE_TEMPLATE: + Width: PARENT_RIGHT + Height: 25 + Children: + Sprite@ICON: + X: 6 + Y: 1 + Width: 24 + Height: 24 + Label@LABEL: + X: 36 + Width: PARENT_RIGHT - 60 + Height: 24 + Font: Bold diff --git a/mods/cnc/chrome/tooltips.yaml b/mods/cnc/chrome/tooltips.yaml index 13c8db871e..3cd8a9985f 100644 --- a/mods/cnc/chrome/tooltips.yaml +++ b/mods/cnc/chrome/tooltips.yaml @@ -293,3 +293,8 @@ Container@REGISTERED_PLAYER_TOOLTIP: Width: PARENT_RIGHT - 20 Height: 23 Font: Bold + Background@BADGES_CONTAINER: + Width: PARENT_RIGHT + Y: 0-1 + Visible: false + Background: panel-black diff --git a/mods/common/chrome/playerprofile.yaml b/mods/common/chrome/playerprofile.yaml index f6b14addd5..c8a4986fa2 100644 --- a/mods/common/chrome/playerprofile.yaml +++ b/mods/common/chrome/playerprofile.yaml @@ -28,6 +28,11 @@ Container@LOCAL_PROFILE_PANEL: Font: TinyBold BaseLine: 1 Text: Logout + Background@BADGES_CONTAINER: + Width: PARENT_RIGHT + Y: 48 + Visible: false + Background: dialog3 Background@GENERATE_KEYS: Width: PARENT_RIGHT Height: PARENT_BOTTOM @@ -208,3 +213,23 @@ Container@LOCAL_PROFILE_PANEL: BaseLine: 1 Font: TinyBold Text: Retry + +Container@PLAYER_PROFILE_BADGES_INSERT: + Logic: PlayerProfileBadgesLogic + Width: PARENT_RIGHT + Height: 110 + Children: + Container@BADGE_TEMPLATE: + Width: PARENT_RIGHT + Height: 25 + Children: + Sprite@ICON: + X: 6 + Y: 1 + Width: 24 + Height: 24 + Label@LABEL: + X: 36 + Width: PARENT_RIGHT - 60 + Height: 24 + Font: Bold diff --git a/mods/common/chrome/tooltips.yaml b/mods/common/chrome/tooltips.yaml index 108efd5d30..aeff05d379 100644 --- a/mods/common/chrome/tooltips.yaml +++ b/mods/common/chrome/tooltips.yaml @@ -218,6 +218,14 @@ Background@REGISTERED_PLAYER_TOOLTIP: Width: PARENT_RIGHT - 14 Height: 23 Font: Bold + Container@BADGES_CONTAINER: + Width: PARENT_RIGHT + Visible: false + Children: + Background@SEPARATOR: + X: 10 + Height: 1 + Background: tooltip-separator Background@PRODUCTION_TOOLTIP: Logic: ProductionTooltipLogic diff --git a/mods/d2k/chrome.yaml b/mods/d2k/chrome.yaml index 6e0bbfb22d..d9d01368f1 100644 --- a/mods/d2k/chrome.yaml +++ b/mods/d2k/chrome.yaml @@ -192,6 +192,9 @@ dialog3: dialog.png corner-bl: 640,127,1,1 corner-br: 767,127,1,1 +tooltip-separator: dialog.png + border-t: 641,0,126,1 + # Same as the half transparent frame used in the Asset Browser dialog4: dialog.png background: 517,392,54,54 diff --git a/mods/d2k/chrome/tooltips.yaml b/mods/d2k/chrome/tooltips.yaml index 2478dbe58f..91ec038be5 100644 --- a/mods/d2k/chrome/tooltips.yaml +++ b/mods/d2k/chrome/tooltips.yaml @@ -221,6 +221,14 @@ Background@REGISTERED_PLAYER_TOOLTIP: Width: PARENT_RIGHT - 14 Height: 23 Font: Bold + Container@BADGES_CONTAINER: + Width: PARENT_RIGHT + Visible: false + Children: + Background@SEPARATOR: + X: 10 + Height: 1 + Background: tooltip-separator Background@PRODUCTION_TOOLTIP: Logic: ProductionTooltipLogic diff --git a/mods/ra/chrome.yaml b/mods/ra/chrome.yaml index 5f2355da25..db66fbb000 100644 --- a/mods/ra/chrome.yaml +++ b/mods/ra/chrome.yaml @@ -510,6 +510,9 @@ dialog4: dialog.png corner-bl: 512,446,6,6 corner-br: 571,446,6,6 +tooltip-separator: dialog.png + border-t: 517,387,54,1 + # completely black tile dialog5: dialog.png background: 579,387,64,64 diff --git a/mods/ts/chrome.yaml b/mods/ts/chrome.yaml index 00459051e0..79f53e431f 100644 --- a/mods/ts/chrome.yaml +++ b/mods/ts/chrome.yaml @@ -464,6 +464,9 @@ dialog4: dialog.png corner-bl: 512,446,6,6 corner-br: 571,446,6,6 +tooltip-separator: dialog.png + border-t: 517,387,54,1 + # A copy of dialog3 (pressed button) progressbar-bg: dialog.png background: 641,1,126,126