diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs index 8d56fc10815d..582946ff004c 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs @@ -8,6 +8,8 @@ using osu.Game.Rulesets.Objects.Types; using OpenTK; using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; +using OpenTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawable { @@ -57,6 +59,14 @@ protected override void CheckForJudgements(bool userTriggered, double timeOffset AddJudgement(new Judgement { Result = CheckPosition.Invoke(HitObject) ? HitResult.Perfect : HitResult.Miss }); } + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + + if (HitObject is IHasComboInformation combo) + AccentColour = skin.GetValue(s => s.ComboColours.Count > 0 ? s.ComboColours[combo.ComboIndex % s.ComboColours.Count] : (Color4?)null) ?? Color4.White; + } + private const float preempt = 1000; protected override void UpdateState(ArmedState state) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 2e59e2dc60ef..d4d89c2aa386 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -5,6 +5,9 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Graphics; using System.Linq; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Skinning; +using OpenTK.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -34,6 +37,14 @@ protected sealed override void UpdateState(ArmedState state) } } + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + + if (HitObject is IHasComboInformation combo) + AccentColour = skin.GetValue(s => s.ComboColours.Count > 0 ? s.ComboColours[combo.ComboIndex % s.ComboColours.Count] : (Color4?)null) ?? Color4.White; + } + protected virtual void UpdatePreemptState() => this.FadeIn(HitObject.TimeFadein); protected virtual void UpdateCurrentState(ArmedState state) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs index 76ed89be6757..28552e6c3660 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs @@ -25,7 +25,7 @@ public ExplodePiece() Blending = BlendingMode.Additive, RelativeSizeAxes = Axes.Both, Alpha = 0.2f, - }, false); + }, s => s.GetTexture("Play/osu/hitcircle") == null); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/FlashPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/FlashPiece.cs index 921d24f69d65..50dc473750c8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/FlashPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/FlashPiece.cs @@ -29,7 +29,7 @@ public FlashPiece() { RelativeSizeAxes = Axes.Both } - }, false); + }, s => s.GetTexture("Play/osu/hitcircle") == null); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/GlowPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/GlowPiece.cs index a4e1916659c6..211e138b65c5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/GlowPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/GlowPiece.cs @@ -29,7 +29,7 @@ private void load(TextureStore textures) Texture = textures.Get(name), Blending = BlendingMode.Additive, Alpha = 0.5f - }, false); + }, s => s.GetTexture("Play/osu/hitcircle") == null); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs index 4220299c6628..0c1fd4c3640d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs @@ -40,7 +40,7 @@ public NumberPiece() Colour = Color4.White.Opacity(0.5f), }, Child = new Box() - }, false), + }, s => s.GetTexture("Play/osu/hitcircle") == null), number = new OsuSpriteText { Text = @"1", diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index f5d7d15a4718..54a279e9772b 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -105,6 +105,7 @@ private void load() runMigrations(); dependencies.Cache(SkinManager = new SkinManager(Host.Storage, contextFactory, Host, Audio)); + dependencies.CacheAs(SkinManager); var api = new APIAccess(LocalConfig); diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 02f88d9ee068..348364a2bf6a 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -6,7 +6,6 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Configuration; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Game.Audio; using osu.Game.Graphics; @@ -19,7 +18,7 @@ namespace osu.Game.Rulesets.Objects.Drawables { - public abstract class DrawableHitObject : CompositeDrawable, IHasAccentColour + public abstract class DrawableHitObject : SkinReloadableDrawable, IHasAccentColour { public readonly HitObject HitObject; diff --git a/osu.Game/Rulesets/Objects/Types/IHasComboIndex.cs b/osu.Game/Rulesets/Objects/Types/IHasComboIndex.cs new file mode 100644 index 000000000000..68474a6e2c70 --- /dev/null +++ b/osu.Game/Rulesets/Objects/Types/IHasComboIndex.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Rulesets.Objects.Types +{ + /// + /// A HitObject that is part of a combo and has extended information about its position relative to other combo objects. + /// + public interface IHasComboIndex : IHasCombo + { + /// + /// The offset of this hitobject in the current combo. + /// + int IndexInCurrentCombo { get; set; } + + /// + /// The offset of this hitobject in the current combo. + /// + int ComboIndex { get; set; } + + /// + /// Whether this is the last object in the current combo. + /// + bool LastInCombo { get; set; } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 8502812f266b..b0472f0e0d13 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -26,6 +26,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Ranking; +using osu.Game.Skinning; using osu.Game.Storyboards.Drawables; namespace osu.Game.Screens.Play @@ -163,7 +164,11 @@ private void load(AudioManager audio, APIAccess api, OsuConfigManager config) RelativeSizeAxes = Axes.Both, Alpha = 0, }, - RulesetContainer, + new LocalSkinOverrideContainer(working.Skin) + { + RelativeSizeAxes = Axes.Both, + Child = RulesetContainer + }, new SkipOverlay(firstObjectTime) { Clock = Clock, // skip button doesn't want to use the audio clock directly diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index c469e9125071..7422ae2e473e 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -3,6 +3,8 @@ using osu.Framework.Audio.Sample; using osu.Framework.Graphics; +using osu.Framework.Graphics.Textures; +using OpenTK.Graphics; namespace osu.Game.Skinning { @@ -11,17 +13,22 @@ public class DefaultSkin : Skin public DefaultSkin() : base(SkinInfo.Default) { - Configuration = new SkinConfiguration(); + Configuration = new SkinConfiguration + { + ComboColours = + { + new Color4(17, 136, 170, 255), + new Color4(102, 136, 0, 255), + new Color4(204, 102, 0, 255), + new Color4(121, 9, 13, 255) + } + }; } - public override Drawable GetDrawableComponent(string componentName) - { - return null; - } + public override Drawable GetDrawableComponent(string componentName) => null; - public override SampleChannel GetSample(string sampleName) - { - return null; - } + public override Texture GetTexture(string componentName) => null; + + public override SampleChannel GetSample(string sampleName) => null; } } diff --git a/osu.Game/Skinning/ISkinSource.cs b/osu.Game/Skinning/ISkinSource.cs new file mode 100644 index 000000000000..d8f259b4ea2d --- /dev/null +++ b/osu.Game/Skinning/ISkinSource.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Skinning +{ + /// + /// Provides access to skinnable elements. + /// + public interface ISkinSource + { + event Action SourceChanged; + + Drawable GetDrawableComponent(string componentName); + + Texture GetTexture(string componentName); + + SampleChannel GetSample(string sampleName); + + TValue GetValue(Func query) where TConfiguration : SkinConfiguration where TValue : class; + + TValue? GetValue(Func query) where TConfiguration : SkinConfiguration where TValue : struct; + } +} diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 64f65cd08cbb..caf5d6a8a6ad 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -56,12 +56,14 @@ public override Drawable GetDrawableComponent(string componentName) break; } - var texture = Textures.Get(componentName); + var texture = GetTexture(componentName); if (texture == null) return null; return new Sprite { Texture = texture }; } + public override Texture GetTexture(string componentName) => Textures.Get(componentName); + public override SampleChannel GetSample(string sampleName) => Samples.Get(sampleName); protected class LegacySkinResourceStore : IResourceStore diff --git a/osu.Game/Skinning/LocalSkinOverrideContainer.cs b/osu.Game/Skinning/LocalSkinOverrideContainer.cs new file mode 100644 index 000000000000..b7e2bd0dafe3 --- /dev/null +++ b/osu.Game/Skinning/LocalSkinOverrideContainer.cs @@ -0,0 +1,72 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Allocation; +using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Skinning +{ + public class LocalSkinOverrideContainer : Container, ISkinSource + { + public event Action SourceChanged; + + public Drawable GetDrawableComponent(string componentName) => source.GetDrawableComponent(componentName) ?? fallbackSource?.GetDrawableComponent(componentName); + + public Texture GetTexture(string componentName) => source.GetTexture(componentName) ?? fallbackSource.GetTexture(componentName); + + public SampleChannel GetSample(string sampleName) => source.GetSample(sampleName) ?? fallbackSource?.GetSample(sampleName); + + public TValue? GetValue(Func query) where TConfiguration : SkinConfiguration where TValue : struct + { + TValue? val = null; + var conf = (source as Skin)?.Configuration as TConfiguration; + if (conf != null) + val = query?.Invoke(conf); + + return val ?? fallbackSource?.GetValue(query); + } + + public TValue GetValue(Func query) where TConfiguration : SkinConfiguration where TValue : class + { + TValue val = null; + var conf = (source as Skin)?.Configuration as TConfiguration; + if (conf != null) + val = query?.Invoke(conf); + + return val ?? fallbackSource?.GetValue(query); + } + + private readonly ISkinSource source; + private ISkinSource fallbackSource; + + public LocalSkinOverrideContainer(ISkinSource source) + { + this.source = source; + } + + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); + + fallbackSource = dependencies.Get(); + if (fallbackSource != null) + fallbackSource.SourceChanged += () => SourceChanged?.Invoke(); + + dependencies.CacheAs(this); + + return dependencies; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (fallbackSource != null) + fallbackSource.SourceChanged -= SourceChanged; + } + } +} diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 53bcf30b0e48..02fb84a4a226 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -4,19 +4,30 @@ using System; using osu.Framework.Audio.Sample; using osu.Framework.Graphics; +using osu.Framework.Graphics.Textures; namespace osu.Game.Skinning { - public abstract class Skin : IDisposable + public abstract class Skin : IDisposable, ISkinSource { public readonly SkinInfo SkinInfo; public virtual SkinConfiguration Configuration { get; protected set; } + public event Action SourceChanged; + public abstract Drawable GetDrawableComponent(string componentName); public abstract SampleChannel GetSample(string sampleName); + public abstract Texture GetTexture(string componentName); + + public TValue GetValue(Func query) where TConfiguration : SkinConfiguration where TValue : class + => Configuration is TConfiguration conf ? query?.Invoke(conf) : null; + + public TValue? GetValue(Func query) where TConfiguration : SkinConfiguration where TValue : struct + => Configuration is TConfiguration conf ? query?.Invoke(conf) : null; + protected Skin(SkinInfo skin) { SkinInfo = skin; diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index fa65b923fb7e..f965a77cceed 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -7,14 +7,17 @@ using System.Linq.Expressions; using Microsoft.EntityFrameworkCore; using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Textures; using osu.Framework.Platform; using osu.Game.Database; using osu.Game.IO.Archives; namespace osu.Game.Skinning { - public class SkinManager : ArchiveModelManager + public class SkinManager : ArchiveModelManager, ISkinSource { private readonly AudioManager audio; @@ -89,6 +92,8 @@ public SkinManager(Storage storage, DatabaseContextFactory contextFactory, IIpcH { if (skin.SkinInfo != CurrentSkinInfo.Value) throw new InvalidOperationException($"Setting {nameof(CurrentSkin)}'s value directly is not supported. Use {nameof(CurrentSkinInfo)} instead."); + + SourceChanged?.Invoke(); }; // migrate older imports which didn't have access to skin.ini @@ -108,5 +113,17 @@ public SkinManager(Storage storage, DatabaseContextFactory contextFactory, IIpcH /// The query. /// The first result for the provided query, or null if no results were found. public SkinInfo Query(Expression> query) => ModelStore.ConsumableItems.AsNoTracking().FirstOrDefault(query); + + public event Action SourceChanged; + + public Drawable GetDrawableComponent(string componentName) => CurrentSkin.Value.GetDrawableComponent(componentName); + + public Texture GetTexture(string componentName) => CurrentSkin.Value.GetTexture(componentName); + + public SampleChannel GetSample(string sampleName) => CurrentSkin.Value.GetSample(sampleName); + + public TValue GetValue(Func query) where TConfiguration : SkinConfiguration where TValue : class => CurrentSkin.Value.GetValue(query); + + public TValue? GetValue(Func query) where TConfiguration : SkinConfiguration where TValue : struct => CurrentSkin.Value.GetValue(query); } } diff --git a/osu.Game/Skinning/SkinReloadableDrawable.cs b/osu.Game/Skinning/SkinReloadableDrawable.cs index 3e33f952cdb9..36f33e746a29 100644 --- a/osu.Game/Skinning/SkinReloadableDrawable.cs +++ b/osu.Game/Skinning/SkinReloadableDrawable.cs @@ -1,8 +1,8 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using osu.Framework.Allocation; -using osu.Framework.Configuration; using osu.Framework.Graphics.Containers; namespace osu.Game.Skinning @@ -12,33 +12,36 @@ namespace osu.Game.Skinning /// public abstract class SkinReloadableDrawable : CompositeDrawable { - private Bindable skin; + private readonly Func allowFallback; + private ISkinSource skin; /// /// Whether fallback to default skin should be allowed if the custom skin is missing this resource. /// - private readonly bool allowDefaultFallback; + private bool allowDefaultFallback => allowFallback == null || allowFallback.Invoke(skin); /// /// Create a new /// /// Whether fallback to default skin should be allowed if the custom skin is missing this resource. - protected SkinReloadableDrawable(bool fallback = true) + protected SkinReloadableDrawable(Func allowFallback = null) { - allowDefaultFallback = fallback; + this.allowFallback = allowFallback; } [BackgroundDependencyLoader] - private void load(SkinManager skinManager) + private void load(ISkinSource source) { - skin = skinManager.CurrentSkin.GetBoundCopy(); - skin.ValueChanged += skin => SkinChanged(skin, allowDefaultFallback || skin.SkinInfo == SkinInfo.Default); + skin = source; + skin.SourceChanged += onChange; } + private void onChange() => SkinChanged(skin, allowDefaultFallback); + protected override void LoadAsyncComplete() { base.LoadAsyncComplete(); - skin.TriggerChange(); + onChange(); } /// @@ -46,7 +49,7 @@ protected override void LoadAsyncComplete() /// /// The new skin. /// Whether fallback to default skin should be allowed if the custom skin is missing this resource. - protected virtual void SkinChanged(Skin skin, bool allowFallback) + protected virtual void SkinChanged(ISkinSource skin, bool allowFallback) { } } diff --git a/osu.Game/Skinning/SkinnableDrawable.cs b/osu.Game/Skinning/SkinnableDrawable.cs index 81abc9e80c9a..9314d16c390e 100644 --- a/osu.Game/Skinning/SkinnableDrawable.cs +++ b/osu.Game/Skinning/SkinnableDrawable.cs @@ -9,8 +9,8 @@ namespace osu.Game.Skinning { public class SkinnableDrawable : SkinnableDrawable { - public SkinnableDrawable(string name, Func defaultImplementation, bool fallback = true, bool restrictSize = true) - : base(name, defaultImplementation, fallback, restrictSize) + public SkinnableDrawable(string name, Func defaultImplementation, Func allowFallback = null, bool restrictSize = true) + : base(name, defaultImplementation, allowFallback, restrictSize) { } } @@ -31,7 +31,7 @@ public class SkinnableDrawable : SkinReloadableDrawable /// A function to create the default skin implementation of this element. /// Whther to fallback to the default implementation when a custom skin is specified but not implementation is present. /// Whether a user-skin drawable should be limited to the size of our parent. - public SkinnableDrawable(string name, Func defaultImplementation, bool fallback = true, bool restrictSize = true) : base(fallback) + public SkinnableDrawable(string name, Func defaultImplementation, Func allowFallback = null, bool restrictSize = true) : base(allowFallback) { componentName = name; createDefault = defaultImplementation; @@ -40,7 +40,7 @@ public SkinnableDrawable(string name, Func defaultImplementation, boo RelativeSizeAxes = Axes.Both; } - protected override void SkinChanged(Skin skin, bool allowFallback) + protected override void SkinChanged(ISkinSource skin, bool allowFallback) { var drawable = skin.GetDrawableComponent(componentName); if (drawable != null) diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index fd52d62d596f..07c8fd373592 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -31,7 +31,7 @@ private void load(AudioManager audio) public void Play() => channels?.ForEach(c => c.Play()); - protected override void SkinChanged(Skin skin, bool allowFallback) + protected override void SkinChanged(ISkinSource skin, bool allowFallback) { channels = samples.Select(s => { diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 8e5c488c838f..6460de179d62 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -874,8 +874,10 @@ + +