From b248b2e5e3bf8b62ea94b9e1d5eb85e3809d448e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 May 2021 22:43:48 +0900 Subject: [PATCH] Hook up full save/load flow --- osu.Game/Screens/Play/HUD/ISkinnableTarget.cs | 15 ---- osu.Game/Screens/Play/HUDOverlay.cs | 2 +- osu.Game/Skinning/DefaultSkin.cs | 42 +--------- .../Skinning/Editor/SkinBlueprintContainer.cs | 8 -- osu.Game/Skinning/Editor/SkinEditor.cs | 43 +++++++++- osu.Game/Skinning/ISkinnableTarget.cs | 4 +- osu.Game/Skinning/LegacySkin.cs | 26 +++++- osu.Game/Skinning/Skin.cs | 81 ++++++++++++++++++- osu.Game/Skinning/SkinManager.cs | 32 ++++++++ 9 files changed, 181 insertions(+), 72 deletions(-) delete mode 100644 osu.Game/Screens/Play/HUD/ISkinnableTarget.cs diff --git a/osu.Game/Screens/Play/HUD/ISkinnableTarget.cs b/osu.Game/Screens/Play/HUD/ISkinnableTarget.cs deleted file mode 100644 index 6cd269333a..0000000000 --- a/osu.Game/Screens/Play/HUD/ISkinnableTarget.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Skinning; - -namespace osu.Game.Screens.Play.HUD -{ - /// - /// A container which supports skinnable components being added to it. - /// - public interface ISkinnableTarget - { - public SkinnableTarget Target { get; } - } -} diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 7220365673..30b735d28a 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -22,7 +22,7 @@ using osuTK; namespace osu.Game.Screens.Play { [Cached] - public class HUDOverlay : Container, IKeyBindingHandler, IDefaultSkinnableTarget + public class HUDOverlay : Container, IKeyBindingHandler { public const float FADE_DURATION = 300; diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 48da841c45..bfb5b8ef56 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -2,17 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.IO; -using System.Linq; -using Newtonsoft.Json; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Audio; -using osu.Game.Screens.Play.HUD; using osuTK.Graphics; namespace osu.Game.Skinning @@ -20,35 +14,11 @@ namespace osu.Game.Skinning public class DefaultSkin : Skin { public DefaultSkin() - : base(SkinInfo.Default) + : base(SkinInfo.Default, null) { Configuration = new DefaultSkinConfiguration(); } - public override Drawable GetDrawableComponent(ISkinComponent component) - { - switch (component) - { - case SkinnableTargetComponent target: - switch (target.Target) - { - case SkinnableTarget.MainHUDComponents: - var infos = JsonConvert.DeserializeObject>(File.ReadAllText("/Users/Dean/json-out.json")).Where(i => i.Target == SkinnableTarget.MainHUDComponents); - - var container = new SkinnableTargetWrapper(target.Target) - { - ChildrenEnumerable = infos.Select(i => i.CreateInstance()) - }; - - return container; - } - - break; - } - - return null; - } - public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null; public override ISample GetSample(ISampleInfo sampleInfo) => null; @@ -72,14 +42,4 @@ namespace osu.Game.Skinning return null; } } - - public class SkinnableTargetWrapper : Container, ISkinnableTarget - { - public SkinnableTarget Target { get; } - - public SkinnableTargetWrapper(SkinnableTarget target) - { - Target = target; - } - } } diff --git a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs index a6f60afd90..45e541abf0 100644 --- a/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs +++ b/osu.Game/Skinning/Editor/SkinBlueprintContainer.cs @@ -1,12 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.IO; using System.Linq; -using Newtonsoft.Json; using osu.Framework.Graphics; using osu.Framework.Testing; -using osu.Game.Extensions; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Compose.Components; @@ -32,11 +29,6 @@ namespace osu.Game.Skinning.Editor { ISkinnableComponent[] skinnableComponents = target.ChildrenOfType().ToArray(); - // todo: the OfType() call can be removed with better IDrawable support. - string json = JsonConvert.SerializeObject(skinnableComponents.OfType().Select(d => d.CreateSerialisedInformation()), new JsonSerializerSettings { Formatting = Formatting.Indented }); - - File.WriteAllText("/Users/Dean/json-out.json", json); - foreach (var c in skinnableComponents) AddBlueprintFor(c); diff --git a/osu.Game/Skinning/Editor/SkinEditor.cs b/osu.Game/Skinning/Editor/SkinEditor.cs index 18a8b220df..30ee3097d2 100644 --- a/osu.Game/Skinning/Editor/SkinEditor.cs +++ b/osu.Game/Skinning/Editor/SkinEditor.cs @@ -11,6 +11,8 @@ using osu.Framework.Testing; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Play.HUD; namespace osu.Game.Skinning.Editor { @@ -24,6 +26,9 @@ namespace osu.Game.Skinning.Editor protected override bool StartHidden => true; + [Resolved] + private SkinManager skins { get; set; } + public SkinEditor(Drawable target) { this.target = target; @@ -53,7 +58,24 @@ namespace osu.Game.Skinning.Editor Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RequestPlacement = placeComponent - } + }, + new TriangleButton + { + Text = "Save Changes", + Width = 140, + Height = 50, + Padding = new MarginPadding + { + Top = 10, + Left = 10, + }, + Margin = new MarginPadding + { + Right = 10, + Bottom = 10, + }, + Action = save, + }, } }; @@ -71,7 +93,7 @@ namespace osu.Game.Skinning.Editor var targetContainer = target.ChildrenOfType().FirstOrDefault(); - targetContainer?.Add(instance); + (targetContainer as Container)?.Add(instance); } protected override void LoadComplete() @@ -80,6 +102,23 @@ namespace osu.Game.Skinning.Editor Show(); } + private void save() + { + var currentSkin = skins.CurrentSkin.Value; + + var legacySkin = currentSkin as LegacySkin; + + if (legacySkin == null) + return; + + SkinnableElementTargetContainer[] targetContainers = target.ChildrenOfType().ToArray(); + + foreach (var t in targetContainers) + legacySkin.UpdateDrawableTarget(t); + + skins.Save(skins.CurrentSkin.Value); + } + protected override bool OnHover(HoverEvent e) => true; protected override bool OnMouseDown(MouseDownEvent e) => true; diff --git a/osu.Game/Skinning/ISkinnableTarget.cs b/osu.Game/Skinning/ISkinnableTarget.cs index 607e89fdec..90e48c8bba 100644 --- a/osu.Game/Skinning/ISkinnableTarget.cs +++ b/osu.Game/Skinning/ISkinnableTarget.cs @@ -2,14 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; namespace osu.Game.Skinning { /// /// Denotes a container which can house s. /// - public interface ISkinnableTarget : IContainerCollection + public interface ISkinnableTarget : IDrawable { + public SkinnableTarget Target { get; } } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 70f8d1d882..fc03ce909c 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -58,7 +58,7 @@ namespace osu.Game.Skinning } protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore storage, [CanBeNull] IStorageResourceProvider resources, string filename) - : base(skin) + : base(skin, resources) { using (var stream = storage?.GetStream(filename)) { @@ -321,8 +321,32 @@ namespace osu.Game.Skinning public override Drawable GetDrawableComponent(ISkinComponent component) { + if (base.GetDrawableComponent(component) is Drawable c) + return c; + switch (component) { + case SkinnableTargetComponent target: + + switch (target.Target) + { + case SkinnableTarget.MainHUDComponents: + return new SkinnableTargetWrapper(target.Target) + { + RelativeSizeAxes = Axes.Both, + Children = new[] + { + // TODO: these should fallback to the osu!classic skin. + GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ComboCounter)) ?? new DefaultComboCounter(), + GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreCounter)) ?? new DefaultScoreCounter(), + GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.AccuracyCounter)) ?? new DefaultAccuracyCounter(), + GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.HealthDisplay)) ?? new DefaultHealthDisplay(), + } + }; + } + + return null; + case HUDSkinComponent hudComponent: { if (!this.HasFont(LegacyFont.Score)) diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 6d1bce2cb1..03ecabc886 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -2,12 +2,19 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Newtonsoft.Json; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Audio; +using osu.Game.IO; +using osu.Game.Screens.Play.HUD; namespace osu.Game.Skinning { @@ -15,9 +22,13 @@ namespace osu.Game.Skinning { public readonly SkinInfo SkinInfo; + private readonly IStorageResourceProvider resources; + public SkinConfiguration Configuration { get; protected set; } - public abstract Drawable GetDrawableComponent(ISkinComponent componentName); + public IDictionary DrawableComponentInfo => drawableComponentInfo; + + private readonly Dictionary drawableComponentInfo = new Dictionary(); public abstract ISample GetSample(ISampleInfo sampleInfo); @@ -27,9 +38,62 @@ namespace osu.Game.Skinning public abstract IBindable GetConfig(TLookup lookup); - protected Skin(SkinInfo skin) + protected Skin(SkinInfo skin, IStorageResourceProvider resources) { SkinInfo = skin; + + // may be null for default skin. + this.resources = resources; + } + + public void UpdateDrawableTarget(SkinnableElementTargetContainer targetContainer) + { + DrawableComponentInfo[targetContainer.Target] = targetContainer.CreateSerialisedChildren().ToArray(); + } + + public virtual Drawable GetDrawableComponent(ISkinComponent component) + { + switch (component) + { + case SkinnableTargetComponent target: + + var skinnableTarget = target.Target; + + if (!DrawableComponentInfo.TryGetValue(skinnableTarget, out var skinnableInfo)) + { + switch (skinnableTarget) + { + case SkinnableTarget.MainHUDComponents: + + // skininfo files may be null for default skin. + var fileInfo = SkinInfo.Files?.FirstOrDefault(f => f.Filename == $"{skinnableTarget}.json"); + + if (fileInfo == null) + return null; + + var bytes = resources?.Files.Get(fileInfo.FileInfo.StoragePath); + + if (bytes == null) + return null; + + string jsonContent = Encoding.UTF8.GetString(bytes); + + DrawableComponentInfo[skinnableTarget] = skinnableInfo = + JsonConvert.DeserializeObject>(jsonContent).Where(i => i.Target == SkinnableTarget.MainHUDComponents).ToArray(); + break; + } + } + + if (skinnableInfo == null) + return null; + + return new SkinnableTargetWrapper(skinnableTarget) + { + ChildrenEnumerable = skinnableInfo.Select(i => i.CreateInstance()) + }; + } + + return null; } #region Disposal @@ -58,4 +122,17 @@ namespace osu.Game.Skinning #endregion } + + public class SkinnableTargetWrapper : Container, IDefaultSkinnableTarget + { + // this is just here to implement the interface and allow a silly parent lookup to work (where the target is not the direct parent for reasons). + // TODO: need to fix this. + public SkinnableTarget Target { get; } + + public SkinnableTargetWrapper(SkinnableTarget target) + { + Target = target; + RelativeSizeAxes = Axes.Both; + } + } } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index ac4d63159a..f9e22c9c89 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -6,9 +6,11 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Linq.Expressions; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; +using Newtonsoft.Json; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; @@ -17,6 +19,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Framework.Utils; @@ -158,6 +161,35 @@ namespace osu.Game.Skinning return new LegacySkin(skinInfo, this); } + public void Save(Skin skin) + { + // some skins don't support saving just yet. + // eventually we will want to create a copy of the skin to allow for customisation. + if (skin.SkinInfo.Files == null) + return; + + foreach (var drawableInfo in skin.DrawableComponentInfo) + { + // todo: the OfType() call can be removed with better IDrawable support. + string json = JsonConvert.SerializeObject(drawableInfo.Value, new JsonSerializerSettings { Formatting = Formatting.Indented }); + + using (var streamContent = new MemoryStream(Encoding.UTF8.GetBytes(json))) + { + string filename = $"{drawableInfo.Key}.json"; + + var oldFile = skin.SkinInfo.Files.FirstOrDefault(f => f.Filename == filename); + + if (oldFile != null) + ReplaceFile(skin.SkinInfo, oldFile, streamContent, oldFile.Filename); + else + AddFile(skin.SkinInfo, streamContent, filename); + + Logger.Log($"Saving out {filename} with {json.Length} bytes"); + Logger.Log(json); + } + } + } + /// /// Perform a lookup query on available s. ///