diff --git a/osu-framework b/osu-framework index f6fa5b80ed..9a773e62eb 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit f6fa5b80ed06f84c8fd25a2576eea8d51565785c +Subproject commit 9a773e62eb246206b918ba4fccf9f2507aaa4595 diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index be1e360fce..a3e5aba2db 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Catch.Objects { StartTime = lastTickTime, ComboColour = ComboColour, - X = Curve.PositionAt(distanceProgress).X / CatchPlayfield.BASE_WIDTH, + X = X + Curve.PositionAt(distanceProgress).X / CatchPlayfield.BASE_WIDTH, Samples = new List(Samples.Select(s => new SampleInfo { Bank = s.Bank, @@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Catch.Objects { StartTime = spanStartTime + t, ComboColour = ComboColour, - X = Curve.PositionAt(progress).X / CatchPlayfield.BASE_WIDTH, + X = X + Curve.PositionAt(progress).X / CatchPlayfield.BASE_WIDTH, Samples = new List(Samples.Select(s => new SampleInfo { Bank = s.Bank, @@ -120,14 +120,14 @@ namespace osu.Game.Rulesets.Catch.Objects Samples = Samples, ComboColour = ComboColour, StartTime = spanStartTime + spanDuration, - X = Curve.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH + X = X + Curve.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH }); } } public double EndTime => StartTime + this.SpanCount() * Curve.Distance / Velocity; - public float EndX => Curve.PositionAt(this.ProgressAt(1)).X / CatchPlayfield.BASE_WIDTH; + public float EndX => X + this.CurvePositionAt(1).X / CatchPlayfield.BASE_WIDTH; public double Duration => EndTime - StartTime; diff --git a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs index 3c9647117e..732d5f4109 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs @@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Mania.UI return null; } - protected override Vector2 GetAspectAdjustedSize() => new Vector2(1, 0.8f); + protected override Vector2 PlayfieldArea => new Vector2(1, 0.8f); protected override FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay, this); diff --git a/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs index 56efc25fa5..a8d895bc1d 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs @@ -5,6 +5,7 @@ using osu.Framework.Graphics.Cursor; using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; +using OpenTK; namespace osu.Game.Rulesets.Osu.Edit { @@ -17,6 +18,8 @@ namespace osu.Game.Rulesets.Osu.Edit protected override Playfield CreatePlayfield() => new OsuEditPlayfield(); + protected override Vector2 PlayfieldArea => Vector2.One; + protected override CursorContainer CreateCursor() => null; } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 6652a5fde2..ae19706da3 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -2,10 +2,12 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.Edit @@ -25,5 +27,7 @@ namespace osu.Game.Rulesets.Osu.Edit new HitObjectCompositionTool(), new HitObjectCompositionTool() }; + + protected override ScalableContainer CreateLayerContainer() => new ScalableContainer(OsuPlayfield.BASE_SIZE.X) { RelativeSizeAxes = Axes.Both }; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 41df7ae4a4..ede84e7737 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -7,9 +7,11 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Osu.Judgements; using osu.Framework.Graphics.Primitives; +using osu.Game.Configuration; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; @@ -29,6 +31,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { slider = s; + Position = s.StackedPosition; + DrawableSliderTail tail; Container ticks; Container repeatPoints; @@ -38,20 +42,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Body = new SliderBody(s) { AccentColour = AccentColour, - Position = s.StackedPosition, PathWidth = s.Scale * 64, }, - ticks = new Container(), - repeatPoints = new Container(), + ticks = new Container { RelativeSizeAxes = Axes.Both }, + repeatPoints = new Container { RelativeSizeAxes = Axes.Both }, Ball = new SliderBall(s) { + BypassAutoSizeAxes = Axes.Both, Scale = new Vector2(s.Scale), AccentColour = AccentColour, AlwaysPresent = true, Alpha = 0 }, - HeadCircle = new DrawableHitCircle(s.HeadCircle), - tail = new DrawableSliderTail(s.TailCircle) + HeadCircle = new DrawableHitCircle(s.HeadCircle) { Position = s.HeadCircle.Position - s.Position }, + tail = new DrawableSliderTail(s.TailCircle) { Position = s.TailCircle.Position - s.Position } }; components.Add(Body); @@ -64,10 +68,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables foreach (var tick in s.NestedHitObjects.OfType()) { - var drawableTick = new DrawableSliderTick(tick) - { - Position = tick.Position - }; + var drawableTick = new DrawableSliderTick(tick) { Position = tick.Position - s.Position }; ticks.Add(drawableTick); components.Add(drawableTick); @@ -76,10 +77,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables foreach (var repeatPoint in s.NestedHitObjects.OfType()) { - var drawableRepeatPoint = new DrawableRepeatPoint(repeatPoint, this) - { - Position = repeatPoint.Position - }; + var drawableRepeatPoint = new DrawableRepeatPoint(repeatPoint, this) { Position = repeatPoint.Position - s.Position }; repeatPoints.Add(drawableRepeatPoint); components.Add(drawableRepeatPoint); @@ -87,7 +85,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } - private int currentSpan; + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + config.BindWith(OsuSetting.SnakingInSliders, Body.SnakingIn); + config.BindWith(OsuSetting.SnakingOutSliders, Body.SnakingOut); + } + public bool Tracking; protected override void Update() @@ -96,21 +100,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Tracking = Ball.Tracking; - double progress = MathHelper.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1); - - int span = slider.SpanAt(progress); - progress = slider.ProgressAt(progress); - - if (span > currentSpan) - currentSpan = span; + double completionProgress = MathHelper.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1); //todo: we probably want to reconsider this before adding scoring, but it looks and feels nice. if (!HeadCircle.IsHit) - HeadCircle.Position = slider.Curve.PositionAt(progress); + HeadCircle.Position = slider.CurvePositionAt(completionProgress); - foreach (var c in components.OfType()) c.UpdateProgress(progress, span); + foreach (var c in components.OfType()) c.UpdateProgress(completionProgress); foreach (var c in components.OfType()) c.UpdateSnakingPosition(slider.Curve.PositionAt(Body.SnakedStart ?? 0), slider.Curve.PositionAt(Body.SnakedEnd ?? 0)); foreach (var t in components.OfType()) t.Tracking = Ball.Tracking; + + Size = Body.Size; + OriginPosition = Body.PathOffset; + + if (DrawSize != Vector2.Zero) + { + var childAnchorPosition = Vector2.Divide(OriginPosition, DrawSize); + foreach (var obj in NestedHitObjects) + obj.RelativeAnchorPosition = childAnchorPosition; + Ball.RelativeAnchorPosition = childAnchorPosition; + } } protected override void CheckForJudgements(bool userTriggered, double timeOffset) @@ -153,6 +162,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables this.FadeOut(fade_out_time, Easing.OutQuint).Expire(); } + + Expire(true); } public Drawable ProxiedLayer => HeadCircle.ApproachCircle; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 8835fc2b29..b907aea8c3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -19,8 +19,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public DrawableSliderTail(HitCircle hitCircle) : base(hitCircle) { - AlwaysPresent = true; + Origin = Anchor.Centre; + RelativeSizeAxes = Axes.Both; + FillMode = FillMode.Fit; + + AlwaysPresent = true; } protected override void CheckForJudgements(bool userTriggered, double timeOffset) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ApproachCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ApproachCircle.cs index 61e9027157..51f8b7026a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ApproachCircle.cs @@ -6,30 +6,24 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public class ApproachCircle : Container { - private readonly Sprite approachCircle; - public ApproachCircle() { Anchor = Anchor.Centre; Origin = Anchor.Centre; - AutoSizeAxes = Axes.Both; - - Children = new Drawable[] - { - approachCircle = new Sprite() - }; + RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] private void load(TextureStore textures) { - approachCircle.Texture = textures.Get(@"Play/osu/approachcircle"); + Child = new SkinnableDrawable("Play/osu/approachcircle", name => new Sprite { Texture = textures.Get(name) }); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs index 286df14056..e7b6598cf2 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs @@ -2,20 +2,16 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; using osu.Framework.Input.Bindings; +using osu.Game.Skinning; using OpenTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public class CirclePiece : Container, IKeyBindingHandler { - private readonly Sprite disc; - public Func Hit; public CirclePiece() @@ -27,26 +23,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Anchor = Anchor.Centre; Origin = Anchor.Centre; - Children = new Drawable[] - { - disc = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }, - new TrianglesPiece - { - RelativeSizeAxes = Axes.Both, - Blending = BlendingMode.Additive, - Alpha = 0.5f, - } - }; - } - - [BackgroundDependencyLoader] - private void load(TextureStore textures) - { - disc.Texture = textures.Get(@"Play/osu/disc"); + InternalChild = new SkinnableDrawable("Play/osu/hitcircle", _ => new DefaultCirclePiece()); } public bool OnPressed(OsuAction action) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultCirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultCirclePiece.cs new file mode 100644 index 0000000000..61f73b6d66 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultCirclePiece.cs @@ -0,0 +1,35 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +{ + public class DefaultCirclePiece : Container + { + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + RelativeSizeAxes = Axes.Both; + Children = new Drawable[] + { + new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = textures.Get(@"Play/osu/disc"), + }, + new TrianglesPiece + { + RelativeSizeAxes = Axes.Both, + Blending = BlendingMode.Additive, + Alpha = 0.5f, + } + }; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/GlowPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/GlowPiece.cs index 9a1208f998..a4e1916659 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/GlowPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/GlowPiece.cs @@ -6,34 +6,30 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public class GlowPiece : Container { - private readonly Sprite layer; - public GlowPiece() { Anchor = Anchor.Centre; Origin = Anchor.Centre; - - Children = new[] - { - layer = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Blending = BlendingMode.Additive, - Alpha = 0.5f - } - }; + RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] private void load(TextureStore textures) { - layer.Texture = textures.Get(@"Play/osu/ring-glow"); + Child = new SkinnableDrawable("Play/osu/ring-glow", name => new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = textures.Get(name), + Blending = BlendingMode.Additive, + Alpha = 0.5f + }, false); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs index afbf00f320..4220299c66 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.Sprites; using OpenTK.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { @@ -28,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Children = new Drawable[] { - new CircularContainer + new SkinnableDrawable("Play/osu/number-glow", name => new CircularContainer { Masking = true, Origin = Anchor.Centre, @@ -38,11 +39,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Radius = 60, Colour = Color4.White.Opacity(0.5f), }, - Children = new[] - { - new Box() - } - }, + Child = new Box() + }, false), number = new OsuSpriteText { Text = @"1", diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs index 2347927f2e..12cc0dc5d9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics.Containers; using OpenTK; using OpenTK.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { @@ -15,24 +16,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { Size = new Vector2(128); - Masking = true; - CornerRadius = Size.X / 2; - Anchor = Anchor.Centre; Origin = Anchor.Centre; - BorderThickness = 10; - BorderColour = Color4.White; - - Children = new Drawable[] + InternalChild = new SkinnableDrawable("Play/osu/hitcircleoverlay", _ => new Container { - new Box + Masking = true, + CornerRadius = Size.X / 2, + BorderThickness = 10, + BorderColour = Color4.White, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - AlwaysPresent = true, - Alpha = 0, - RelativeSizeAxes = Axes.Both + new Box + { + AlwaysPresent = true, + Alpha = 0, + RelativeSizeAxes = Axes.Both + } } - }; + }); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index 2fda299389..1921c51889 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input; +using osu.Game.Rulesets.Objects.Types; using OpenTK; using OpenTK.Graphics; @@ -139,9 +140,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } } - public void UpdateProgress(double progress, int span) + public void UpdateProgress(double completionProgress) { - Position = slider.Curve.PositionAt(progress); + Position = slider.CurvePositionAt(completionProgress); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs index 89af67ba2a..8c0eb7ff7d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Textures; -using osu.Game.Configuration; using OpenTK; using OpenTK.Graphics.ES30; using OpenTK.Graphics; @@ -30,6 +29,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces set { path.PathWidth = value; } } + /// + /// Offset in absolute coordinates from the start of the curve. + /// + public Vector2 PathOffset { get; private set; } + + public readonly List CurrentCurve = new List(); + + public readonly Bindable SnakingIn = new Bindable(); + public readonly Bindable SnakingOut = new Bindable(); + public double? SnakedStart { get; private set; } public double? SnakedEnd { get; private set; } @@ -46,8 +55,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces return; accentColour = value; - if (LoadState == LoadState.Ready) - Schedule(reloadTexture); + if (LoadState >= LoadState.Ready) + reloadTexture(); + } + } + + private Color4 borderColour = Color4.White; + /// + /// Used to colour the path border. + /// + public new Color4 BorderColour + { + get { return borderColour; } + set + { + if (borderColour == value) + return; + borderColour = value; + + if (LoadState >= LoadState.Ready) + reloadTexture(); } } @@ -55,6 +82,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private int textureWidth => (int)PathWidth * 2; + private Vector2 topLeftOffset; + private readonly Slider slider; public SliderBody(Slider s) { @@ -64,6 +93,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { container = new BufferedContainer { + RelativeSizeAxes = Axes.Both, CacheDrawnFrameBuffer = true, Children = new Drawable[] { @@ -87,26 +117,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces if (updateSnaking(p0, p1)) { - // Autosizing does not give us the desired behaviour here. - // We want the container to have the same size as the slider, - // and to be positioned such that the slider head is at (0,0). - container.Size = path.Size; - container.Position = -path.PositionInBoundingBox(slider.Curve.PositionAt(0) - CurrentCurve[0]); + // The path is generated such that its size encloses it. This change of size causes the path + // to move around while snaking, so we need to offset it to make sure it maintains the + // same position as when it is fully snaked. + var newTopLeftOffset = path.PositionInBoundingBox(Vector2.Zero); + path.Position = topLeftOffset - newTopLeftOffset; container.ForceRedraw(); } } - private Bindable snakingIn; - private Bindable snakingOut; - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load() { - snakingIn = config.GetBindable(OsuSetting.SnakingInSliders); - snakingOut = config.GetBindable(OsuSetting.SnakingOutSliders); - reloadTexture(); + computeSize(); } private void reloadTexture() @@ -130,10 +155,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces if (progress <= border_portion) { - bytes[i * 4] = 255; - bytes[i * 4 + 1] = 255; - bytes[i * 4 + 2] = 255; - bytes[i * 4 + 3] = (byte)(Math.Min(progress / aa_portion, 1) * 255); + bytes[i * 4] = (byte)(BorderColour.R * 255); + bytes[i * 4 + 1] = (byte)(BorderColour.G * 255); + bytes[i * 4 + 2] = (byte)(BorderColour.B * 255); + bytes[i * 4 + 3] = (byte)(Math.Min(progress / aa_portion, 1) * (BorderColour.A * 255)); } else { @@ -150,7 +175,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces path.Texture = texture; } - public readonly List CurrentCurve = new List(); + private void computeSize() + { + // Generate the entire curve + slider.Curve.GetPathToProgress(CurrentCurve, 0, 1); + foreach (Vector2 p in CurrentCurve) + path.AddVertex(p); + + Size = path.Size; + + topLeftOffset = path.PositionInBoundingBox(Vector2.Zero); + PathOffset = path.PositionInBoundingBox(CurrentCurve[0]); + } + private bool updateSnaking(double p0, double p1) { if (SnakedStart == p0 && SnakedEnd == p1) return false; @@ -162,26 +199,29 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces path.ClearVertices(); foreach (Vector2 p in CurrentCurve) - path.AddVertex(p - CurrentCurve[0]); + path.AddVertex(p); return true; } - public void UpdateProgress(double progress, int span) + public void UpdateProgress(double completionProgress) { + var span = slider.SpanAt(completionProgress); + var spanProgress = slider.ProgressAt(completionProgress); + double start = 0; - double end = snakingIn ? MathHelper.Clamp((Time.Current - (slider.StartTime - slider.TimePreempt)) / slider.TimeFadein, 0, 1) : 1; + double end = SnakingIn ? MathHelper.Clamp((Time.Current - (slider.StartTime - slider.TimePreempt)) / slider.TimeFadein, 0, 1) : 1; if (span >= slider.SpanCount() - 1) { if (Math.Min(span, slider.SpanCount() - 1) % 2 == 1) { start = 0; - end = snakingOut ? progress : 1; + end = SnakingOut ? spanProgress : 1; } else { - start = snakingOut ? progress : 0; + start = SnakingOut ? spanProgress : 0; } } diff --git a/osu.Game.Rulesets.Osu/Objects/ISliderProgress.cs b/osu.Game.Rulesets.Osu/Objects/ISliderProgress.cs index 54f783b664..a0566eaf17 100644 --- a/osu.Game.Rulesets.Osu/Objects/ISliderProgress.cs +++ b/osu.Game.Rulesets.Osu/Objects/ISliderProgress.cs @@ -5,6 +5,10 @@ namespace osu.Game.Rulesets.Osu.Objects { public interface ISliderProgress { - void UpdateProgress(double progress, int span); + /// + /// Updates the progress of this element along the slider. + /// + /// Amount of the slider completed. + void UpdateProgress(double completionProgress); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 5dd3d7aa89..61056832e9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -23,8 +23,8 @@ namespace osu.Game.Rulesets.Osu.Objects public double EndTime => StartTime + this.SpanCount() * Curve.Distance / Velocity; public double Duration => EndTime - StartTime; - public Vector2 StackedPositionAt(double t) => this.PositionAt(t) + StackOffset; - public override Vector2 EndPosition => this.PositionAt(1); + public Vector2 StackedPositionAt(double t) => StackedPosition + this.CurvePositionAt(t); + public override Vector2 EndPosition => Position + this.CurvePositionAt(1); public SliderCurve Curve { get; } = new SliderCurve(); @@ -66,18 +66,6 @@ namespace osu.Game.Rulesets.Osu.Objects /// public double SpanDuration => Duration / this.SpanCount(); - private int stackHeight; - - public override int StackHeight - { - get { return stackHeight; } - set - { - stackHeight = value; - Curve.Offset = StackOffset; - } - } - public double Velocity; public double TickDistance; @@ -111,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Objects HeadCircle = new HitCircle { StartTime = StartTime, - Position = StackedPosition, + Position = Position, IndexInCurrentCombo = IndexInCurrentCombo, ComboColour = ComboColour, Samples = Samples, @@ -121,7 +109,7 @@ namespace osu.Game.Rulesets.Osu.Objects TailCircle = new HitCircle { StartTime = EndTime, - Position = StackedEndPosition, + Position = EndPosition, IndexInCurrentCombo = IndexInCurrentCombo, ComboColour = ComboColour }; @@ -168,7 +156,7 @@ namespace osu.Game.Rulesets.Osu.Objects SpanIndex = span, SpanStartTime = spanStartTime, StartTime = spanStartTime + timeProgress * SpanDuration, - Position = Curve.PositionAt(distanceProgress), + Position = Position + Curve.PositionAt(distanceProgress), StackHeight = StackHeight, Scale = Scale, ComboColour = ComboColour, @@ -187,7 +175,7 @@ namespace osu.Game.Rulesets.Osu.Objects RepeatIndex = repeatIndex, SpanDuration = SpanDuration, StartTime = StartTime + repeat * SpanDuration, - Position = Curve.PositionAt(repeat % 2), + Position = Position + Curve.PositionAt(repeat % 2), StackHeight = StackHeight, Scale = Scale, ComboColour = ComboColour, diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index a22ac6aed1..274f7bff62 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -315,11 +315,11 @@ namespace osu.Game.Rulesets.Osu.Replays for (double j = FrameDelay; j < s.Duration; j += FrameDelay) { - Vector2 pos = s.PositionAt(j / s.Duration); + Vector2 pos = s.StackedPositionAt(j / s.Duration); AddFrameToReplay(new ReplayFrame(h.StartTime + j, pos.X, pos.Y, button)); } - AddFrameToReplay(new ReplayFrame(s.EndTime, s.EndPosition.X, s.EndPosition.Y, button)); + AddFrameToReplay(new ReplayFrame(s.EndTime, s.StackedEndPosition.X, s.StackedEndPosition.Y, button)); } // We only want to let go of our button if we are at the end of the current replay. Otherwise something is still going on after us so we need to keep the button pressed! diff --git a/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs b/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs index 55fa37882d..ed212b48cd 100644 --- a/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs +++ b/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs @@ -88,10 +88,15 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("Catmull Slider", () => testCatmull()); AddStep("Catmull Slider 1 Repeat", () => testCatmull(1)); AddStep("Catmull Slider 2 Repeats", () => testCatmull(2)); + + AddStep("Big Single, Large StackOffset", () => testSimpleBigLargeStackOffset()); + AddStep("Big 1 Repeat, Large StackOffset", () => testSimpleBigLargeStackOffset(1)); } private void testSimpleBig(int repeats = 0) => createSlider(2, repeats: repeats); + private void testSimpleBigLargeStackOffset(int repeats = 0) => createSlider(2, repeats: repeats, stackHeight: 10); + private void testSimpleMedium(int repeats = 0) => createSlider(5, repeats: repeats); private void testSimpleSmall(int repeats = 0) => createSlider(7, repeats: repeats); @@ -104,7 +109,7 @@ namespace osu.Game.Rulesets.Osu.Tests private void testShortHighSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 15); - private void createSlider(float circleSize = 2, float distance = 400, int repeats = 0, double speedMultiplier = 2) + private void createSlider(float circleSize = 2, float distance = 400, int repeats = 0, double speedMultiplier = 2, int stackHeight = 0) { var slider = new Slider { @@ -113,12 +118,13 @@ namespace osu.Game.Rulesets.Osu.Tests ComboColour = Color4.LightSeaGreen, ControlPoints = new List { - new Vector2(-(distance / 2), 0), - new Vector2(distance / 2, 0), + Vector2.Zero, + new Vector2(distance, 0), }, Distance = distance, RepeatCount = repeats, - RepeatSamples = createEmptySamples(repeats) + RepeatSamples = createEmptySamples(repeats), + StackHeight = stackHeight }; addSlider(slider, circleSize, speedMultiplier); @@ -133,9 +139,9 @@ namespace osu.Game.Rulesets.Osu.Tests ComboColour = Color4.LightSeaGreen, ControlPoints = new List { - new Vector2(-200, 0), - new Vector2(0, 200), - new Vector2(200, 0) + Vector2.Zero, + new Vector2(200, 200), + new Vector2(400, 0) }, Distance = 600, RepeatCount = repeats, @@ -157,12 +163,12 @@ namespace osu.Game.Rulesets.Osu.Tests ComboColour = Color4.LightSeaGreen, ControlPoints = new List { - new Vector2(-200, 0), - new Vector2(-50, 75), - new Vector2(0, 100), - new Vector2(100, -200), + Vector2.Zero, + new Vector2(150, 75), new Vector2(200, 0), - new Vector2(230, 0) + new Vector2(300, -200), + new Vector2(400, 0), + new Vector2(430, 0) }, Distance = 793.4417, RepeatCount = repeats, @@ -184,11 +190,11 @@ namespace osu.Game.Rulesets.Osu.Tests ComboColour = Color4.LightSeaGreen, ControlPoints = new List { - new Vector2(-200, 0), - new Vector2(-50, 75), - new Vector2(0, 100), - new Vector2(100, -200), - new Vector2(230, 0) + Vector2.Zero, + new Vector2(150, 75), + new Vector2(200, 100), + new Vector2(300, -200), + new Vector2(430, 0) }, Distance = 480, RepeatCount = repeats, @@ -210,7 +216,7 @@ namespace osu.Game.Rulesets.Osu.Tests ComboColour = Color4.LightSeaGreen, ControlPoints = new List { - new Vector2(0, 0), + Vector2.Zero, new Vector2(-200, 0), new Vector2(0, 0), new Vector2(0, -200), @@ -241,10 +247,10 @@ namespace osu.Game.Rulesets.Osu.Tests CurveType = CurveType.Catmull, ControlPoints = new List { - new Vector2(-100, 0), - new Vector2(-50, -50), - new Vector2(50, 50), - new Vector2(100, 0) + Vector2.Zero, + new Vector2(50, -50), + new Vector2(150, 50), + new Vector2(200, 0) }, Distance = 300, RepeatCount = repeats, diff --git a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs index 9cb6a13cb2..2af381dd71 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.UI protected override Vector2 GetAspectAdjustedSize() { var aspectSize = DrawSize.X * 0.75f < DrawSize.Y ? new Vector2(DrawSize.X, DrawSize.X * 0.75f) : new Vector2(DrawSize.Y * 4f / 3f, DrawSize.Y); - return new Vector2(aspectSize.X / DrawSize.X, aspectSize.Y / DrawSize.Y) * 0.75f; + return new Vector2(aspectSize.X / DrawSize.X, aspectSize.Y / DrawSize.Y); } protected override CursorContainer CreateCursor() => new GameplayCursor(); diff --git a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj index 7838fb7707..04903d11bf 100644 --- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj +++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj @@ -92,6 +92,7 @@ + diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs index 5493a5029b..afa3d162f4 100644 --- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs +++ b/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs @@ -2,10 +2,9 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Audio { @@ -14,7 +13,9 @@ namespace osu.Game.Rulesets.Taiko.Audio private readonly ControlPointInfo controlPoints; private readonly Dictionary mappings = new Dictionary(); - public DrumSampleMapping(ControlPointInfo controlPoints, AudioManager audio) + public readonly List Sounds = new List(); + + public DrumSampleMapping(ControlPointInfo controlPoints) { this.controlPoints = controlPoints; @@ -27,20 +28,34 @@ namespace osu.Game.Rulesets.Taiko.Audio foreach (var s in samplePoints) { + var centre = s.GetSampleInfo(); + var rim = s.GetSampleInfo(SampleInfo.HIT_CLAP); + + // todo: this is ugly + centre.Namespace = "taiko"; + rim.Namespace = "taiko"; + mappings[s.Time] = new DrumSample { - Centre = s.GetSampleInfo().GetChannel(audio.Sample, "Taiko"), - Rim = s.GetSampleInfo(SampleInfo.HIT_CLAP).GetChannel(audio.Sample, "Taiko") + Centre = addSound(centre), + Rim = addSound(rim) }; } } + private SkinnableSound addSound(SampleInfo sampleInfo) + { + var drawable = new SkinnableSound(sampleInfo); + Sounds.Add(drawable); + return drawable; + } + public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time]; public class DrumSample { - public SampleChannel Centre; - public SampleChannel Rim; + public SkinnableSound Centre; + public SkinnableSound Rim; } } } diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs index 002159439d..cb45ce2dce 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Taiko.Replays { foreach (var tick in drumRoll.NestedHitObjects.OfType()) { - Frames.Add(new TaikoReplayFrame(tick.StartTime, hitButton ? ReplayButtonState.Right1 : ReplayButtonState.Right2)); + Frames.Add(new TaikoReplayFrame(tick.StartTime, hitButton ? ReplayButtonState.Left1 : ReplayButtonState.Left2)); hitButton = !hitButton; } } @@ -95,16 +95,16 @@ namespace osu.Game.Rulesets.Taiko.Replays if (hit is CentreHit) { if (h.IsStrong) - button = ReplayButtonState.Right1 | ReplayButtonState.Right2; + button = ReplayButtonState.Left1 | ReplayButtonState.Left2; else - button = hitButton ? ReplayButtonState.Right1 : ReplayButtonState.Right2; + button = hitButton ? ReplayButtonState.Left1 : ReplayButtonState.Left2; } else { if (h.IsStrong) - button = ReplayButtonState.Left1 | ReplayButtonState.Left2; + button = ReplayButtonState.Right1 | ReplayButtonState.Right2; else - button = hitButton ? ReplayButtonState.Left1 : ReplayButtonState.Left2; + button = hitButton ? ReplayButtonState.Right1 : ReplayButtonState.Right2; } Frames.Add(new TaikoReplayFrame(h.StartTime, button)); diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs index 05e10b6fce..1a96b26d34 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs @@ -19,13 +19,13 @@ namespace osu.Game.Rulesets.Taiko.Replays var actions = new List(); if (CurrentFrame?.MouseRight1 == true) - actions.Add(TaikoAction.LeftCentre); - if (CurrentFrame?.MouseRight2 == true) - actions.Add(TaikoAction.RightCentre); - if (CurrentFrame?.MouseLeft1 == true) actions.Add(TaikoAction.LeftRim); - if (CurrentFrame?.MouseLeft2 == true) + if (CurrentFrame?.MouseRight2 == true) actions.Add(TaikoAction.RightRim); + if (CurrentFrame?.MouseLeft1 == true) + actions.Add(TaikoAction.LeftCentre); + if (CurrentFrame?.MouseLeft2 == true) + actions.Add(TaikoAction.RightCentre); return new List { new ReplayState { PressedActions = actions } }; } diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs index 98f20fd558..b918f495fc 100644 --- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs +++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs @@ -4,7 +4,6 @@ using System; using OpenTK; using osu.Framework.Allocation; -using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -34,9 +33,9 @@ namespace osu.Game.Rulesets.Taiko.UI } [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load() { - var sampleMappings = new DrumSampleMapping(controlPoints, audio); + var sampleMappings = new DrumSampleMapping(controlPoints); Children = new Drawable[] { @@ -63,6 +62,8 @@ namespace osu.Game.Rulesets.Taiko.UI CentreAction = TaikoAction.RightCentre } }; + + AddRangeInternal(sampleMappings.Sounds); } /// diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs index 8342009e80..fd31f738ee 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs @@ -88,6 +88,8 @@ namespace osu.Game.Rulesets.Taiko.UI return new Vector2(1, default_relative_height * aspectAdjust); } + protected override Vector2 PlayfieldArea => Vector2.One; + public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(this); protected override BeatmapConverter CreateBeatmapConverter() => new TaikoBeatmapConverter(IsForCurrentRuleset); diff --git a/osu.Game.Tests/Visual/TestCaseEditorSelectionLayer.cs b/osu.Game.Tests/Visual/TestCaseEditorSelectionLayer.cs index 50a39e6c33..60ddff64ba 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorSelectionLayer.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorSelectionLayer.cs @@ -5,15 +5,13 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using OpenTK; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Timing; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit.Layers.Selection; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual { @@ -27,44 +25,31 @@ namespace osu.Game.Tests.Visual }; [BackgroundDependencyLoader] - private void load() + private void load(OsuGameBase osuGame) { - var playfield = new OsuEditPlayfield(); - - Children = new Drawable[] + osuGame.Beatmap.Value = new TestWorkingBeatmap(new Beatmap { - new Container + HitObjects = new List { - RelativeSizeAxes = Axes.Both, - Clock = new FramedClock(new StopwatchClock()), - Child = playfield + new HitCircle { Position = new Vector2(256, 192), Scale = 0.5f }, + new HitCircle { Position = new Vector2(344, 148), Scale = 0.5f }, + new Slider + { + Position = new Vector2(128, 256), + ControlPoints = new List + { + Vector2.Zero, + new Vector2(216, 0), + }, + Distance = 400, + Velocity = 1, + TickDistance = 100, + Scale = 0.5f, + } }, - new SelectionLayer(playfield) - }; + }); - var hitCircle1 = new HitCircle { Position = new Vector2(256, 192), Scale = 0.5f }; - var hitCircle2 = new HitCircle { Position = new Vector2(344, 148), Scale = 0.5f }; - var slider = new Slider - { - ControlPoints = new List - { - new Vector2(128, 256), - new Vector2(344, 256), - }, - Distance = 400, - Position = new Vector2(128, 256), - Velocity = 1, - TickDistance = 100, - Scale = 0.5f, - }; - - hitCircle1.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - hitCircle2.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - - playfield.Add(new DrawableHitCircle(hitCircle1)); - playfield.Add(new DrawableHitCircle(hitCircle2)); - playfield.Add(new DrawableSlider(slider)); + Child = new OsuHitObjectComposer(new OsuRuleset()); } } } diff --git a/osu.Game/Audio/SampleInfo.cs b/osu.Game/Audio/SampleInfo.cs index e6f4a0b8d1..2014db6c61 100644 --- a/osu.Game/Audio/SampleInfo.cs +++ b/osu.Game/Audio/SampleInfo.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using osu.Framework.Audio.Sample; namespace osu.Game.Audio { @@ -14,22 +13,10 @@ namespace osu.Game.Audio public const string HIT_NORMAL = @"hitnormal"; public const string HIT_CLAP = @"hitclap"; - public SampleChannel GetChannel(SampleManager manager, string resourceNamespace = null) - { - SampleChannel channel = null; - - if (resourceNamespace != null) - channel = manager.Get($"Gameplay/{resourceNamespace}/{Bank}-{Name}"); - - // try without namespace as a fallback. - if (channel == null) - channel = manager.Get($"Gameplay/{Bank}-{Name}"); - - if (channel != null) - channel.Volume.Value = Volume / 100.0; - - return channel; - } + /// + /// An optional ruleset namespace. + /// + public string Namespace; /// /// The bank to load the sample from. diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index c33dd91330..3d927ef67c 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -14,6 +14,8 @@ namespace osu.Game.Configuration { // UI/selection defaults Set(OsuSetting.Ruleset, 0, 0, int.MaxValue); + Set(OsuSetting.Skin, 0, 0, int.MaxValue); + Set(OsuSetting.BeatmapDetailTab, BeatmapDetailTab.Details); Set(OsuSetting.ShowConvertedBeatmaps, true); @@ -122,6 +124,7 @@ namespace osu.Game.Configuration ChatDisplayHeight, Version, ShowConvertedBeatmaps, - SpeedChangeVisualisation + SpeedChangeVisualisation, + Skin } } diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index e83b30595e..a4b0c30478 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -13,6 +13,7 @@ using osu.Game.IO; using osu.Game.Rulesets; using DatabasedKeyBinding = osu.Game.Input.Bindings.DatabasedKeyBinding; using LogLevel = Microsoft.Extensions.Logging.LogLevel; +using osu.Game.Skinning; namespace osu.Game.Database { @@ -26,6 +27,7 @@ namespace osu.Game.Database public DbSet DatabasedSetting { get; set; } public DbSet FileInfo { get; set; } public DbSet RulesetInfo { get; set; } + public DbSet SkinInfo { get; set; } private readonly string connectionString; diff --git a/osu.Game/Migrations/20180219060912_AddSkins.Designer.cs b/osu.Game/Migrations/20180219060912_AddSkins.Designer.cs new file mode 100644 index 0000000000..83b8d6cf8a --- /dev/null +++ b/osu.Game/Migrations/20180219060912_AddSkins.Designer.cs @@ -0,0 +1,379 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using osu.Game.Database; +using System; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20180219060912_AddSkins")] + partial class AddSkins + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MD5Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntKey") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20180219060912_AddSkins.cs b/osu.Game/Migrations/20180219060912_AddSkins.cs new file mode 100644 index 0000000000..741fcf4079 --- /dev/null +++ b/osu.Game/Migrations/20180219060912_AddSkins.cs @@ -0,0 +1,73 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace osu.Game.Migrations +{ + public partial class AddSkins : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "SkinInfo", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Creator = table.Column(type: "TEXT", nullable: true), + DeletePending = table.Column(type: "INTEGER", nullable: false), + Name = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_SkinInfo", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "SkinFileInfo", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + FileInfoID = table.Column(type: "INTEGER", nullable: false), + Filename = table.Column(type: "TEXT", nullable: false), + SkinInfoID = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SkinFileInfo", x => x.ID); + table.ForeignKey( + name: "FK_SkinFileInfo_FileInfo_FileInfoID", + column: x => x.FileInfoID, + principalTable: "FileInfo", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_SkinFileInfo_SkinInfo_SkinInfoID", + column: x => x.SkinInfoID, + principalTable: "SkinInfo", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_SkinFileInfo_FileInfoID", + table: "SkinFileInfo", + column: "FileInfoID"); + + migrationBuilder.CreateIndex( + name: "IX_SkinFileInfo_SkinInfoID", + table: "SkinFileInfo", + column: "SkinInfoID"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SkinFileInfo"); + + migrationBuilder.DropTable( + name: "SkinInfo"); + } + } +} diff --git a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs index 157125102f..1627627790 100644 --- a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs +++ b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs @@ -281,6 +281,43 @@ namespace osu.Game.Migrations b.ToTable("RulesetInfo"); }); + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.ToTable("SkinInfo"); + }); + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => { b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") @@ -322,6 +359,19 @@ namespace osu.Game.Migrations .WithMany("BeatmapSets") .HasForeignKey("MetadataID"); }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); #pragma warning restore 612, 618 } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 4a451b3655..aeb23dccd7 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -31,6 +31,7 @@ using osu.Game.Rulesets; using osu.Game.Screens.Play; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Mods; +using osu.Game.Skinning; using OpenTK.Graphics; namespace osu.Game @@ -80,6 +81,8 @@ namespace osu.Game private Bindable configRuleset; public Bindable Ruleset = new Bindable(); + private Bindable configSkin; + private readonly string[] args; private SettingsOverlay settings; @@ -123,10 +126,18 @@ namespace osu.Game dependencies.CacheAs(this); + // bind config int to database RulesetInfo configRuleset = LocalConfig.GetBindable(OsuSetting.Ruleset); Ruleset.Value = RulesetStore.GetRuleset(configRuleset.Value) ?? RulesetStore.AvailableRulesets.First(); Ruleset.ValueChanged += r => configRuleset.Value = r.ID ?? 0; + // bind config int to database SkinInfo + configSkin = LocalConfig.GetBindable(OsuSetting.Skin); + + SkinManager.CurrentSkinInfo.ValueChanged += s => configSkin.Value = s.ID; + configSkin.ValueChanged += id => SkinManager.CurrentSkinInfo.Value = SkinManager.Query(s => s.ID == id) ?? SkinInfo.Default; + configSkin.TriggerChange(); + LocalConfig.BindWith(OsuSetting.VolumeInactive, inactiveVolumeAdjust); } @@ -188,7 +199,9 @@ namespace osu.Game CursorOverrideContainer.CanShowCursor = currentScreen?.CursorVisible ?? false; // hook up notifications to components. + SkinManager.PostNotification = n => notifications?.Post(n); BeatmapManager.PostNotification = n => notifications?.Post(n); + BeatmapManager.GetStableStorage = GetStorageForStableInstall; AddRange(new Drawable[] diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index b70055cc00..f3c46269d5 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -1,4 +1,4 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . +// Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; @@ -30,6 +30,7 @@ using osu.Game.Input.Bindings; using osu.Game.IO; using osu.Game.Rulesets; using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; namespace osu.Game { @@ -39,6 +40,8 @@ namespace osu.Game protected BeatmapManager BeatmapManager; + protected SkinManager SkinManager; + protected RulesetStore RulesetStore; protected FileStore FileStore; @@ -103,6 +106,8 @@ namespace osu.Game runMigrations(); + dependencies.Cache(SkinManager = new SkinManager(Host.Storage, contextFactory, Host, Audio)); + dependencies.Cache(API = new APIAccess { Username = LocalConfig.Get(OsuSetting.Username), @@ -120,6 +125,7 @@ namespace osu.Game fileImporters.Add(BeatmapManager); fileImporters.Add(ScoreStore); + fileImporters.Add(SkinManager); //this completely overrides the framework default. will need to change once we make a proper FontStore. dependencies.Cache(Fonts = new FontStore { ScaleAdjust = 100 }); diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index beb2b3b746..7c6e563c5b 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -477,7 +477,7 @@ namespace osu.Game.Overlays if (!api.IsLoggedIn) { - target.AddNewMessages(new ErrorMessage("Please login to participate in chat!")); + target.AddNewMessages(new ErrorMessage("Please sign in to participate in chat!")); return; } diff --git a/osu.Game/Overlays/Direct/PlayButton.cs b/osu.Game/Overlays/Direct/PlayButton.cs index 1d67bc2d90..0fb988ead7 100644 --- a/osu.Game/Overlays/Direct/PlayButton.cs +++ b/osu.Game/Overlays/Direct/PlayButton.cs @@ -22,6 +22,7 @@ namespace osu.Game.Overlays.Direct public Track Preview { get; private set; } private BeatmapSetInfo beatmapSet; + public BeatmapSetInfo BeatmapSet { get { return beatmapSet; } @@ -199,8 +200,7 @@ namespace osu.Game.Overlays.Direct // add back the user's music volume setting (since we are no longer in the global TrackManager's hierarchy). config.BindWith(FrameworkSetting.VolumeMusic, trackManager.Volume); - if (!string.IsNullOrEmpty(preview)) - Preview = trackManager.Get(preview); + Preview = trackManager.Get(preview); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index d959da52f3..a5d068adbd 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -208,7 +208,7 @@ namespace osu.Game.Overlays.Settings.Sections.General { username = new OsuTextBox { - PlaceholderText = "Username", + PlaceholderText = "Email address", RelativeSizeAxes = Axes.X, Text = api?.Username ?? string.Empty, TabbableContentContainer = this @@ -222,12 +222,12 @@ namespace osu.Game.Overlays.Settings.Sections.General }, new SettingsCheckbox { - LabelText = "Remember username", + LabelText = "Remember email address", Bindable = config.GetBindable(OsuSetting.SaveUsername), }, new SettingsCheckbox { - LabelText = "Stay logged in", + LabelText = "Stay signed in", Bindable = config.GetBindable(OsuSetting.SavePassword), }, new SettingsButton @@ -237,7 +237,7 @@ namespace osu.Game.Overlays.Settings.Sections.General }, new SettingsButton { - Text = "Register new account", + Text = "Register", //Action = registerLink } }; diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index f6915896d7..bc0b8b4aaa 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -1,26 +1,33 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; +using osu.Game.Skinning; using OpenTK; namespace osu.Game.Overlays.Settings.Sections { public class SkinSection : SettingsSection { + private SettingsDropdown skinDropdown; + public override string Header => "Skin"; + public override FontAwesome Icon => FontAwesome.fa_paint_brush; [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(OsuConfigManager config, SkinManager skins) { FlowContent.Spacing = new Vector2(0, 5); Children = new Drawable[] { + skinDropdown = new SettingsDropdown(), new SettingsSlider { LabelText = "Menu cursor size", @@ -39,6 +46,14 @@ namespace osu.Game.Overlays.Settings.Sections Bindable = config.GetBindable(OsuSetting.AutoCursorSize) }, }; + + void reloadSkins() => skinDropdown.Items = skins.GetAllUsableSkins().Select(s => new KeyValuePair(s.Name, s.ID)); + skins.ItemAdded += _ => reloadSkins(); + skins.ItemRemoved += _ => reloadSkins(); + + reloadSkins(); + + skinDropdown.Bindable = config.GetBindable(OsuSetting.Skin); } private class SizeSlider : OsuSliderBar diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 7f22b3764c..1246127257 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -25,20 +25,24 @@ namespace osu.Game.Rulesets.Edit protected ICompositionTool CurrentTool { get; private set; } + private RulesetContainer rulesetContainer; + private readonly List layerContainers = new List(); + protected HitObjectComposer(Ruleset ruleset) { this.ruleset = ruleset; - RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] private void load(OsuGameBase osuGame) { - RulesetContainer rulesetContainer; try { rulesetContainer = CreateRulesetContainer(ruleset, osuGame.Beatmap.Value); + + // TODO: should probably be done at a RulesetContainer level to share logic with Player. + rulesetContainer.Clock = new InterpolatingFramedClock((IAdjustableClock)osuGame.Beatmap.Value.Track ?? new StopwatchClock()); } catch (Exception e) { @@ -46,6 +50,14 @@ namespace osu.Game.Rulesets.Edit return; } + ScalableContainer createLayerContainerWithContent(Drawable content) + { + var container = CreateLayerContainer(); + container.Child = content; + layerContainers.Add(container); + return container; + } + RadioButtonCollection toolboxCollection; InternalChild = new GridContainer { @@ -66,20 +78,21 @@ namespace osu.Game.Rulesets.Edit }, new Container { + Name = "Content", RelativeSizeAxes = Axes.Both, - Masking = true, - BorderColour = Color4.White, - BorderThickness = 2, Children = new Drawable[] { - new Box + createLayerContainerWithContent(new Container { + Name = "Border", RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true, - }, + Masking = true, + BorderColour = Color4.White, + BorderThickness = 2, + Child = new Box { RelativeSizeAxes = Axes.Both, Alpha = 0, AlwaysPresent = true } + }), rulesetContainer, - new SelectionLayer(rulesetContainer.Playfield) + createLayerContainerWithContent(new SelectionLayer(rulesetContainer.Playfield)) } } }, @@ -90,8 +103,6 @@ namespace osu.Game.Rulesets.Edit } }; - rulesetContainer.Clock = new InterpolatingFramedClock((IAdjustableClock)osuGame.Beatmap.Value.Track ?? new StopwatchClock()); - toolboxCollection.Items = new[] { new RadioButton("Select", () => setCompositionTool(null)) } .Concat( @@ -102,10 +113,28 @@ namespace osu.Game.Rulesets.Edit toolboxCollection.Items[0].Select(); } + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + layerContainers.ForEach(l => + { + l.Anchor = rulesetContainer.Playfield.Anchor; + l.Origin = rulesetContainer.Playfield.Origin; + l.Position = rulesetContainer.Playfield.Position; + l.Size = rulesetContainer.Playfield.Size; + }); + } + private void setCompositionTool(ICompositionTool tool) => CurrentTool = tool; protected virtual RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => ruleset.CreateRulesetContainerWith(beatmap, true); protected abstract IReadOnlyList CompositionTools { get; } + + /// + /// Creates a which provides a layer above or below the . + /// + protected virtual ScalableContainer CreateLayerContainer() => new ScalableContainer { RelativeSizeAxes = Axes.Both }; } } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 2db02724ed..4c2683b389 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -3,20 +3,19 @@ using System; using System.Collections.Generic; -using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; -using osu.Game.Rulesets.Judgements; -using Container = osu.Framework.Graphics.Containers.Container; -using osu.Game.Rulesets.Objects.Types; -using OpenTK.Graphics; -using osu.Game.Audio; using System.Linq; -using osu.Game.Graphics; +using osu.Framework.Allocation; using osu.Framework.Configuration; -using OpenTK; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; +using osu.Game.Audio; +using osu.Game.Graphics; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; +using OpenTK; +using OpenTK.Graphics; namespace osu.Game.Rulesets.Objects.Drawables { @@ -32,7 +31,8 @@ namespace osu.Game.Rulesets.Objects.Drawables // Todo: Rulesets should be overriding the resources instead, but we need to figure out where/when to apply overrides first protected virtual string SampleNamespace => null; - protected List Samples = new List(); + protected SkinnableSound Samples; + protected virtual IEnumerable GetSamples() => HitObject.Samples; private List nestedHitObjects; @@ -83,31 +83,22 @@ namespace osu.Game.Rulesets.Objects.Drawables } [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load() { - var samples = GetSamples(); + var samples = GetSamples().ToArray(); + if (samples.Any()) { if (HitObject.SampleControlPoint == null) throw new ArgumentNullException(nameof(HitObject.SampleControlPoint), $"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}." + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); - - foreach (SampleInfo s in samples) + AddInternal(Samples = new SkinnableSound(samples.Select(s => new SampleInfo { - SampleInfo localSampleInfo = new SampleInfo - { - Bank = s.Bank ?? HitObject.SampleControlPoint.SampleBank, - Name = s.Name, - Volume = s.Volume > 0 ? s.Volume : HitObject.SampleControlPoint.SampleVolume - }; - - SampleChannel channel = localSampleInfo.GetChannel(audio.Sample, SampleNamespace); - - if (channel == null) - continue; - - Samples.Add(channel); - } + Bank = s.Bank ?? HitObject.SampleControlPoint.SampleBank, + Name = s.Name, + Volume = s.Volume > 0 ? s.Volume : HitObject.SampleControlPoint.SampleVolume, + Namespace = SampleNamespace + }).ToArray())); } } @@ -139,7 +130,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// Plays all the hitsounds for this . /// - public void PlaySamples() => Samples.ForEach(s => s?.Play()); + public void PlaySamples() => Samples?.Play(); protected override void Update() { @@ -221,10 +212,8 @@ namespace osu.Game.Rulesets.Objects.Drawables return false; if (NestedHitObjects != null) - { foreach (var d in NestedHitObjects) judgementOccurred |= d.UpdateJudgement(userTriggered); - } if (!ProvidesJudgement || judgementFinalized || judgementOccurred) return judgementOccurred; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 5fdc9a07e1..2fcf3205c1 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -41,9 +41,11 @@ namespace osu.Game.Rulesets.Objects.Legacy } else if ((type & ConvertHitObjectType.Slider) > 0) { + var pos = new Vector2(int.Parse(split[0]), int.Parse(split[1])); + CurveType curveType = CurveType.Catmull; double length = 0; - var points = new List { new Vector2(int.Parse(split[0]), int.Parse(split[1])) }; + var points = new List { Vector2.Zero }; string[] pointsplit = split[5].Split('|'); foreach (string t in pointsplit) @@ -69,7 +71,7 @@ namespace osu.Game.Rulesets.Objects.Legacy } string[] temp = t.Split(':'); - points.Add(new Vector2((int)Convert.ToDouble(temp[0], CultureInfo.InvariantCulture), (int)Convert.ToDouble(temp[1], CultureInfo.InvariantCulture))); + points.Add(new Vector2((int)Convert.ToDouble(temp[0], CultureInfo.InvariantCulture), (int)Convert.ToDouble(temp[1], CultureInfo.InvariantCulture)) - pos); } int repeatCount = Convert.ToInt32(split[6], CultureInfo.InvariantCulture); @@ -134,7 +136,7 @@ namespace osu.Game.Rulesets.Objects.Legacy for (int i = 0; i < nodes; i++) nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i])); - result = CreateSlider(new Vector2(int.Parse(split[0]), int.Parse(split[1])), combo, points, length, curveType, repeatCount, nodeSamples); + result = CreateSlider(pos, combo, points, length, curveType, repeatCount, nodeSamples); } else if ((type & ConvertHitObjectType.Spinner) > 0) { diff --git a/osu.Game/Rulesets/Objects/Types/IHasCurve.cs b/osu.Game/Rulesets/Objects/Types/IHasCurve.cs index 7f03854ea9..251ad3e3cd 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasCurve.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasCurve.cs @@ -30,21 +30,19 @@ namespace osu.Game.Rulesets.Objects.Types public static class HasCurveExtensions { /// - /// Computes the position on the curve at a given progress, accounting for repeat logic. - /// - /// Ranges from [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. - /// + /// Computes the position on the curve relative to how much of the has been completed. /// /// The curve. - /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. - public static Vector2 PositionAt(this IHasCurve obj, double progress) + /// [0, 1] where 0 is the start time of the and 1 is the end time of the . + /// The position on the curve. + public static Vector2 CurvePositionAt(this IHasCurve obj, double progress) => obj.Curve.PositionAt(obj.ProgressAt(progress)); /// - /// Finds the progress along the curve, accounting for repeat logic. + /// Computes the progress along the curve relative to how much of the has been completed. /// /// The curve. - /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. + /// [0, 1] where 0 is the start time of the and 1 is the end time of the . /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. public static double ProgressAt(this IHasCurve obj, double progress) { diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index a7fed7059b..bbf20c2c26 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -3,52 +3,37 @@ using System.Collections.Generic; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; -using OpenTK; using osu.Framework.Allocation; namespace osu.Game.Rulesets.UI { - public abstract class Playfield : Container + public abstract class Playfield : ScalableContainer { /// /// The HitObjects contained in this Playfield. /// public HitObjectContainer HitObjects { get; private set; } - public Container ScaledContent; - - protected override Container Content => content; - private readonly Container content; - - private List nestedPlayfields; - /// /// All the s nested inside this playfield. /// public IReadOnlyList NestedPlayfields => nestedPlayfields; + private List nestedPlayfields; /// /// A container for keeping track of DrawableHitObjects. /// - /// Whether we want our internal coordinate system to be scaled to a specified width. - protected Playfield(float? customWidth = null) + /// The width to scale the internal coordinate space to. + /// May be null if scaling based on is desired. If is also null, no scaling will occur. + /// + /// The height to scale the internal coordinate space to. + /// May be null if scaling based on is desired. If is also null, no scaling will occur. + /// + protected Playfield(float? customWidth = null, float? customHeight = null) + : base(customWidth, customHeight) { RelativeSizeAxes = Axes.Both; - - AddInternal(ScaledContent = new ScaledContainer - { - CustomWidth = customWidth, - RelativeSizeAxes = Axes.Both, - Children = new[] - { - content = new Container - { - RelativeSizeAxes = Axes.Both, - } - } - }); } [BackgroundDependencyLoader] @@ -94,22 +79,5 @@ namespace osu.Game.Rulesets.UI /// Creates the container that will be used to contain the s. /// protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer(); - - private class ScaledContainer : Container - { - /// - /// A value (in game pixels that we should scale our content to match). - /// - public float? CustomWidth; - - //dividing by the customwidth will effectively scale our content to the required container size. - protected override Vector2 DrawScale => CustomWidth.HasValue ? new Vector2(DrawSize.X / CustomWidth.Value) : base.DrawScale; - - protected override void Update() - { - base.Update(); - RelativeChildSize = new Vector2(DrawScale.X, RelativeChildSize.Y); - } - } } } diff --git a/osu.Game/Rulesets/UI/RulesetContainer.cs b/osu.Game/Rulesets/UI/RulesetContainer.cs index f4e700a8eb..05cb0f741b 100644 --- a/osu.Game/Rulesets/UI/RulesetContainer.cs +++ b/osu.Game/Rulesets/UI/RulesetContainer.cs @@ -319,7 +319,7 @@ namespace osu.Game.Rulesets.UI { base.Update(); - Playfield.Size = GetAspectAdjustedSize(); + Playfield.Size = GetAspectAdjustedSize() * PlayfieldArea; } /// @@ -330,11 +330,17 @@ namespace osu.Game.Rulesets.UI protected virtual BeatmapProcessor CreateBeatmapProcessor() => new BeatmapProcessor(); /// - /// Computes the final size of the in relative coordinate space after all - /// aspect and scale adjustments. + /// Computes the size of the in relative coordinate space after aspect adjustments. /// /// The aspect-adjusted size. - protected virtual Vector2 GetAspectAdjustedSize() => new Vector2(0.75f); // A sane default + protected virtual Vector2 GetAspectAdjustedSize() => Vector2.One; + + /// + /// The area of this that is available for the to use. + /// Must be specified in relative coordinate space to this . + /// This affects the final size of the but does not affect the 's scale. + /// + protected virtual Vector2 PlayfieldArea => new Vector2(0.75f); // A sane default /// /// Creates a converter to convert Beatmap to a specific mode. diff --git a/osu.Game/Rulesets/UI/ScalableContainer.cs b/osu.Game/Rulesets/UI/ScalableContainer.cs new file mode 100644 index 0000000000..43ed770f77 --- /dev/null +++ b/osu.Game/Rulesets/UI/ScalableContainer.cs @@ -0,0 +1,86 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using OpenTK; + +namespace osu.Game.Rulesets.UI +{ + /// + /// A which can have its internal coordinate system scaled to a specific size. + /// + public class ScalableContainer : Container + { + /// + /// The scaled content. + /// + public readonly Container ScaledContent; + + protected override Container Content => content; + private readonly Container content; + + /// + /// A which can have its internal coordinate system scaled to a specific size. + /// + /// The width to scale the internal coordinate space to. + /// May be null if scaling based on is desired. If is also null, no scaling will occur. + /// + /// The height to scale the internal coordinate space to. + /// May be null if scaling based on is desired. If is also null, no scaling will occur. + /// + public ScalableContainer(float? customWidth = null, float? customHeight = null) + { + AddInternal(ScaledContent = new ScaledContainer + { + CustomWidth = customWidth, + CustomHeight = customHeight, + RelativeSizeAxes = Axes.Both, + Child = content = new Container { RelativeSizeAxes = Axes.Both } + }); + } + + public class ScaledContainer : Container + { + /// + /// The value to scale the width of the content to match. + /// If null, is used. + /// + public float? CustomWidth; + + /// + /// The value to scale the height of the content to match. + /// if null, is used. + /// + public float? CustomHeight; + + /// + /// The scale that is required for the size of the content to match and . + /// + private Vector2 sizeScale + { + get + { + if (CustomWidth.HasValue && CustomHeight.HasValue) + return Vector2.Divide(DrawSize, new Vector2(CustomWidth.Value, CustomHeight.Value)); + if (CustomWidth.HasValue) + return new Vector2(DrawSize.X / CustomWidth.Value); + if (CustomHeight.HasValue) + return new Vector2(DrawSize.Y / CustomHeight.Value); + return Vector2.One; + } + } + + /// + /// Scale the content to the required container size by multiplying by . + /// + protected override Vector2 DrawScale => sizeScale * base.DrawScale; + + protected override void Update() + { + base.Update(); + RelativeChildSize = new Vector2(CustomWidth.HasValue ? sizeScale.X : RelativeChildSize.X, CustomHeight.HasValue ? sizeScale.Y : RelativeChildSize.Y); + } + } + } +} diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index e168f6daec..1c1c8f7f61 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -62,9 +62,14 @@ namespace osu.Game.Rulesets.UI.Scrolling /// Creates a new . /// /// The direction in which s in this container should scroll. - /// Whether we want our internal coordinate system to be scaled to a specified width - protected ScrollingPlayfield(ScrollingDirection direction, float? customWidth = null) - : base(customWidth) + /// The width to scale the internal coordinate space to. + /// May be null if scaling based on is desired. If is also null, no scaling will occur. + /// + /// The height to scale the internal coordinate space to. + /// May be null if scaling based on is desired. If is also null, no scaling will occur. + /// + protected ScrollingPlayfield(ScrollingDirection direction, float? customWidth = null, float? customHeight = null) + : base(customWidth, customHeight) { this.direction = direction; } diff --git a/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs b/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs index 6be6523175..273cceeeda 100644 --- a/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs @@ -133,7 +133,7 @@ namespace osu.Game.Screens.Select.Leaderboards replacePlaceholder(new MessagePlaceholder(@"No records yet!")); break; case PlaceholderState.NotLoggedIn: - replacePlaceholder(new MessagePlaceholder(@"Please login to view online leaderboards!")); + replacePlaceholder(new MessagePlaceholder(@"Please sign in to view online leaderboards!")); break; case PlaceholderState.NotSupporter: replacePlaceholder(new MessagePlaceholder(@"Please invest in a supporter tag to view this leaderboard!")); diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs new file mode 100644 index 0000000000..e40a43d400 --- /dev/null +++ b/osu.Game/Skinning/DefaultSkin.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 + +using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; + +namespace osu.Game.Skinning +{ + public class DefaultSkin : Skin + { + public DefaultSkin() + : base(SkinInfo.Default) + { + } + + public override Drawable GetDrawableComponent(string componentName) + { + return null; + } + + public override SampleChannel GetSample(string sampleName) + { + return null; + } + } +} diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs new file mode 100644 index 0000000000..5f34ddc2b5 --- /dev/null +++ b/osu.Game/Skinning/LegacySkin.cs @@ -0,0 +1,47 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.IO; +using System.Linq; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; + +namespace osu.Game.Skinning +{ + public class LegacySkin : Skin + { + private readonly TextureStore textures; + + private readonly SampleManager samples; + + public LegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager) + : base(skin) + { + samples = audioManager.GetSampleManager(storage); + textures = new TextureStore(new RawTextureLoaderStore(storage)); + } + + private string getPathForFile(string filename) => + SkinInfo.Files.FirstOrDefault(f => string.Equals(Path.GetFileNameWithoutExtension(f.Filename), filename, StringComparison.InvariantCultureIgnoreCase))?.FileInfo.StoragePath; + + public override Drawable GetDrawableComponent(string componentName) + { + var texture = textures.Get(getPathForFile(componentName.Split('/').Last())); + if (texture == null) return null; + + return new Sprite + { + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Texture = texture, + }; + } + + public override SampleChannel GetSample(string sampleName) => samples.Get(getPathForFile(sampleName.Split('/').Last())); + } +} diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs new file mode 100644 index 0000000000..fafbdec8f0 --- /dev/null +++ b/osu.Game/Skinning/Skin.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; + +namespace osu.Game.Skinning +{ + public abstract class Skin + { + public readonly SkinInfo SkinInfo; + + public abstract Drawable GetDrawableComponent(string componentName); + + public abstract SampleChannel GetSample(string sampleName); + + protected Skin(SkinInfo skin) + { + SkinInfo = skin; + } + } +} diff --git a/osu.Game/Skinning/SkinFileInfo.cs b/osu.Game/Skinning/SkinFileInfo.cs new file mode 100644 index 0000000000..e8caf8f44a --- /dev/null +++ b/osu.Game/Skinning/SkinFileInfo.cs @@ -0,0 +1,25 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using osu.Game.Database; +using osu.Game.IO; + +namespace osu.Game.Skinning +{ + public class SkinFileInfo : INamedFileInfo + { + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int ID { get; set; } + + public int SkinInfoID { get; set; } + + public int FileInfoID { get; set; } + + public FileInfo FileInfo { get; set; } + + [Required] + public string Filename { get; set; } + } +} diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs new file mode 100644 index 0000000000..45c8b97f63 --- /dev/null +++ b/osu.Game/Skinning/SkinInfo.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 System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using osu.Game.Database; + +namespace osu.Game.Skinning +{ + public class SkinInfo : IHasFiles, IEquatable, IHasPrimaryKey, ISoftDelete + { + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int ID { get; set; } + + public string Name { get; set; } + + public string Creator { get; set; } + + public List Files { get; set; } + + public bool DeletePending { get; set; } + + public static SkinInfo Default { get; } = new SkinInfo { Name = "osu!lazer", Creator = "team osu!" }; + + public bool Equals(SkinInfo other) => other != null && ID == other.ID; + } +} diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs new file mode 100644 index 0000000000..88d51eca10 --- /dev/null +++ b/osu.Game/Skinning/SkinManager.cs @@ -0,0 +1,77 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; +using osu.Framework.Audio; +using osu.Framework.Configuration; +using osu.Framework.Platform; +using osu.Game.Database; +using osu.Game.IO.Archives; + +namespace osu.Game.Skinning +{ + public class SkinManager : ArchiveModelManager + { + private readonly AudioManager audio; + + public readonly Bindable CurrentSkin = new Bindable(new DefaultSkin()); + public readonly Bindable CurrentSkinInfo = new Bindable(SkinInfo.Default) { Default = SkinInfo.Default }; + + public override string[] HandledExtensions => new[] { ".osk" }; + + /// + /// Returns a list of all usable s. + /// + /// A list of available . + public List GetAllUsableSkins() + { + var userSkins = ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList(); + userSkins.Insert(0, SkinInfo.Default); + return userSkins; + } + + protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo + { + Name = archive.Name + }; + + /// + /// Retrieve a instance for the provided + /// + /// The skin to lookup. + /// A instance correlating to the provided . + public Skin GetSkin(SkinInfo skinInfo) + { + if (skinInfo == SkinInfo.Default) + return new DefaultSkin(); + + return new LegacySkin(skinInfo, Files.Store, audio); + } + + private SkinStore store; + + public SkinManager(Storage storage, DatabaseContextFactory contextFactory, IIpcHost importHost, AudioManager audio) + : base(storage, contextFactory, new SkinStore(contextFactory, storage), importHost) + { + this.audio = audio; + + CurrentSkinInfo.ValueChanged += info => CurrentSkin.Value = GetSkin(info); + CurrentSkin.ValueChanged += skin => + { + if (skin.SkinInfo != CurrentSkinInfo.Value) + throw new InvalidOperationException($"Setting {nameof(CurrentSkin)}'s value directly is not supported. Use {nameof(CurrentSkinInfo)} instead."); + }; + } + + /// + /// Perform a lookup query on available s. + /// + /// 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); + } +} diff --git a/osu.Game/Skinning/SkinReloadableDrawable.cs b/osu.Game/Skinning/SkinReloadableDrawable.cs new file mode 100644 index 0000000000..3e33f952cd --- /dev/null +++ b/osu.Game/Skinning/SkinReloadableDrawable.cs @@ -0,0 +1,53 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Skinning +{ + /// + /// A drawable which has a callback when the skin changes. + /// + public abstract class SkinReloadableDrawable : CompositeDrawable + { + private Bindable skin; + + /// + /// Whether fallback to default skin should be allowed if the custom skin is missing this resource. + /// + private readonly bool allowDefaultFallback; + + /// + /// Create a new + /// + /// Whether fallback to default skin should be allowed if the custom skin is missing this resource. + protected SkinReloadableDrawable(bool fallback = true) + { + allowDefaultFallback = fallback; + } + + [BackgroundDependencyLoader] + private void load(SkinManager skinManager) + { + skin = skinManager.CurrentSkin.GetBoundCopy(); + skin.ValueChanged += skin => SkinChanged(skin, allowDefaultFallback || skin.SkinInfo == SkinInfo.Default); + } + + protected override void LoadAsyncComplete() + { + base.LoadAsyncComplete(); + skin.TriggerChange(); + } + + /// + /// Called when a change is made to the skin. + /// + /// 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) + { + } + } +} diff --git a/osu.Game/Skinning/SkinStore.cs b/osu.Game/Skinning/SkinStore.cs new file mode 100644 index 0000000000..ffd9873901 --- /dev/null +++ b/osu.Game/Skinning/SkinStore.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Linq; +using Microsoft.EntityFrameworkCore; +using osu.Framework.Platform; +using osu.Game.Database; + +namespace osu.Game.Skinning +{ + public class SkinStore : MutableDatabaseBackedStore + { + public SkinStore(DatabaseContextFactory contextFactory, Storage storage = null) + : base(contextFactory, storage) + { + } + + protected override IQueryable AddIncludesForConsumption(IQueryable query) => + base.AddIncludesForConsumption(query) + .Include(s => s.Files).ThenInclude(f => f.FileInfo); + } +} diff --git a/osu.Game/Skinning/SkinnableDrawable.cs b/osu.Game/Skinning/SkinnableDrawable.cs new file mode 100644 index 0000000000..cd669778a6 --- /dev/null +++ b/osu.Game/Skinning/SkinnableDrawable.cs @@ -0,0 +1,44 @@ +// 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.Graphics; + +namespace osu.Game.Skinning +{ + public class SkinnableDrawable : SkinnableDrawable + { + public SkinnableDrawable(string name, Func defaultImplementation, bool fallback = true) + : base(name, defaultImplementation, fallback) + { + } + } + + public class SkinnableDrawable : SkinReloadableDrawable + where T : Drawable + { + private readonly Func createDefault; + + private readonly string componentName; + + public SkinnableDrawable(string name, Func defaultImplementation, bool fallback = true) : base(fallback) + { + componentName = name; + createDefault = defaultImplementation; + + RelativeSizeAxes = Axes.Both; + } + + protected override void SkinChanged(Skin skin, bool allowFallback) + { + var drawable = skin.GetDrawableComponent(componentName); + if (drawable == null && allowFallback) + drawable = createDefault(componentName); + + if (drawable != null) + InternalChild = drawable; + else + ClearInternal(); + } + } +} diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs new file mode 100644 index 0000000000..fd52d62d59 --- /dev/null +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -0,0 +1,62 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Audio; + +namespace osu.Game.Skinning +{ + public class SkinnableSound : SkinReloadableDrawable + { + private readonly SampleInfo[] samples; + private SampleChannel[] channels; + + private AudioManager audio; + + public SkinnableSound(params SampleInfo[] samples) + { + this.samples = samples; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + this.audio = audio; + } + + public void Play() => channels?.ForEach(c => c.Play()); + + protected override void SkinChanged(Skin skin, bool allowFallback) + { + channels = samples.Select(s => + { + var ch = loadChannel(s, skin.GetSample); + if (ch == null && allowFallback) + ch = loadChannel(s, audio.Sample.Get); + return ch; + }).Where(c => c != null).ToArray(); + } + + private SampleChannel loadChannel(SampleInfo info, Func getSampleFunction) + { + SampleChannel ch = null; + + if (info.Namespace != null) + ch = getSampleFunction($"Gameplay/{info.Namespace}/{info.Bank}-{info.Name}"); + + // try without namespace as a fallback. + if (ch == null) + ch = getSampleFunction($"Gameplay/{info.Bank}-{info.Name}"); + + if (ch != null) + ch.Volume.Value = info.Volume / 100.0; + + return ch; + } + } +} diff --git a/osu.Game/Tests/Visual/TestCasePerformancePoints.cs b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs index c531edb893..5b32433467 100644 --- a/osu.Game/Tests/Visual/TestCasePerformancePoints.cs +++ b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs @@ -230,7 +230,7 @@ namespace osu.Game.Tests.Visual { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = "Please login to see online scores", + Text = "Please sign in to see online scores", }; } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 39261fbe57..6a06bf540b 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -322,6 +322,10 @@ 20171209034410_AddRulesetInfoShortName.cs + + + 20180219060912_AddSkins.cs + @@ -357,6 +361,7 @@ + @@ -849,6 +854,16 @@ + + + + + + + + + + @@ -921,4 +936,4 @@ - \ No newline at end of file +