diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs index 05ef947e4b..8d56fc1081 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable { if (CheckPosition == null) return; - if (timeOffset > 0) + if (timeOffset >= 0) AddJudgement(new Judgement { Result = CheckPosition.Invoke(HitObject) ? HitResult.Perfect : HitResult.Miss }); } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs index c8b15aec61..965ca62674 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs @@ -26,6 +26,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable AddNested(getVisualRepresentation?.Invoke(o)); } + protected override bool ProvidesJudgement => false; + protected override void AddNested(DrawableHitObject h) { var catchObject = (DrawableCatchHitObject)h; diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index 6df9498881..0a8a53c6f1 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -24,17 +24,13 @@ namespace osu.Game.Rulesets.Catch.Scoring switch (obj) { case JuiceStream stream: - AddJudgement(new CatchJudgement { Result = HitResult.Perfect }); - AddJudgement(new CatchJudgement { Result = HitResult.Perfect }); - foreach (var _ in stream.NestedHitObjects.Cast()) AddJudgement(new CatchJudgement { Result = HitResult.Perfect }); break; case BananaShower shower: - AddJudgement(new CatchJudgement { Result = HitResult.Perfect }); - foreach (var _ in shower.NestedHitObjects.Cast()) AddJudgement(new CatchJudgement { Result = HitResult.Perfect }); + AddJudgement(new CatchJudgement { Result = HitResult.Perfect }); break; case Fruit _: AddJudgement(new CatchJudgement { Result = HitResult.Perfect }); diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 7f56c3bbb1..39b7ffb387 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -35,10 +35,6 @@ namespace osu.Game.Rulesets.Catch.UI ScaledContent.AddRange(new Drawable[] { - content = new Container - { - RelativeSizeAxes = Axes.Both, - }, explodingFruitContainer = new Container { RelativeSizeAxes = Axes.Both, @@ -49,7 +45,11 @@ namespace osu.Game.Rulesets.Catch.UI ExplodingFruitTarget = explodingFruitContainer, Anchor = Anchor.BottomLeft, Origin = Anchor.TopLeft, - } + }, + content = new Container + { + RelativeSizeAxes = Axes.Both, + }, }); } diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 5252ba294a..7c548f70d4 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -84,9 +84,9 @@ namespace osu.Game.Rulesets.Catch.UI } } - protected override void Update() + protected override void UpdateAfterChildren() { - base.Update(); + base.UpdateAfterChildren(); var state = GetContainingInputManager().CurrentState as CatchFramedReplayInputHandler.CatchReplayState; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs index 013bb358ed..daa017477f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly DrawableSlider drawableSlider; /// - /// Whether currently in the last ControlPoint of the slider body's curve. + /// Whether this repeat point is at the end of the slider's curve. /// private bool isRepeatAtEnd => repeatPoint.RepeatIndex % 2 == 0; @@ -83,8 +83,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables var curve = drawableSlider.Body.CurrentCurve; if (curve.Count < 3 || curve.All(p => p == Position)) return; - var referencePoint = curve[isRepeatAtEnd ? curve.IndexOf(Position, curve.Count - 2) - 1 : curve[0] == curve[1] ? 2 : 1]; - Rotation = MathHelper.RadiansToDegrees((float)Math.Atan2(referencePoint.Y - Position.Y, referencePoint.X - Position.X)); + int referenceIndex; + //We are looking for the next point in the curve different than our position + //Since there can be more than one point equal to our position, we iterate until one is found + if (isRepeatAtEnd) + { + referenceIndex = curve.Count - 1; + while (curve[referenceIndex] == Position) + referenceIndex--; + } + else + { + referenceIndex = 0; + while (curve[referenceIndex] == Position) + referenceIndex++; + } + Rotation = MathHelper.RadiansToDegrees((float)Math.Atan2(curve[referenceIndex].Y - Position.Y, curve[referenceIndex].X - Position.X)); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 5f464402d0..5795bb8405 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -64,15 +64,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables foreach (var tick in s.NestedHitObjects.OfType()) { - var spanStartTime = s.StartTime + tick.SpanIndex * s.SpanDuration; - var fadeInTime = spanStartTime + (tick.StartTime - spanStartTime) / 2 - (tick.SpanIndex == 0 ? HitObject.TimeFadein : HitObject.TimeFadein / 2); - var fadeOutTime = spanStartTime + s.SpanDuration; - var drawableTick = new DrawableSliderTick(tick) { - FadeInTime = fadeInTime, - FadeOutTime = fadeOutTime, - Position = tick.Position, + Position = tick.Position }; ticks.Add(drawableTick); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index ae76f1e0e1..c616d15de3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -1,7 +1,6 @@ // 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; using osu.Game.Rulesets.Objects.Drawables; using OpenTK; @@ -14,10 +13,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSliderTick : DrawableOsuHitObject, IRequireTracking { - private readonly SliderTick sliderTick; - - public double FadeInTime; - public double FadeOutTime; + private const double anim_duration = 150; public bool Tracking { get; set; } @@ -25,8 +21,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public DrawableSliderTick(SliderTick sliderTick) : base(sliderTick) { - this.sliderTick = sliderTick; - Size = new Vector2(16) * sliderTick.Scale; Masking = true; @@ -56,13 +50,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void UpdatePreemptState() { - var animIn = Math.Min(150, sliderTick.StartTime - FadeInTime); - this.Animate( - d => d.FadeIn(animIn), - d => d.ScaleTo(0.5f).ScaleTo(1.2f, animIn) + d => d.FadeIn(anim_duration), + d => d.ScaleTo(0.5f).ScaleTo(1.2f, anim_duration / 2) ).Then( - d => d.ScaleTo(1, 150, Easing.Out) + d => d.ScaleTo(1, anim_duration / 2, Easing.Out) ); } @@ -71,15 +63,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables switch (state) { case ArmedState.Idle: - this.Delay(FadeOutTime - sliderTick.StartTime).FadeOut(); + this.Delay(HitObject.TimePreempt).FadeOut(); break; case ArmedState.Miss: - this.FadeOut(160) - .FadeColour(Color4.Red, 80); + this.FadeOut(anim_duration) + .FadeColour(Color4.Red, anim_duration / 2); break; case ArmedState.Hit: - this.FadeOut(120, Easing.OutQuint) - .ScaleTo(Scale * 1.5f, 120, Easing.OutQuint); + this.FadeOut(anim_duration, Easing.OutQuint) + .ScaleTo(Scale * 1.5f, anim_duration, Easing.OutQuint); break; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index d4444c5c5d..3bde7e790b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -23,6 +23,7 @@ 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 SliderCurve Curve { get; } = new SliderCurve(); @@ -167,6 +168,7 @@ namespace osu.Game.Rulesets.Osu.Objects AddNested(new SliderTick { SpanIndex = span, + SpanStartTime = spanStartTime, StartTime = spanStartTime + timeProgress * SpanDuration, Position = Curve.PositionAt(distanceProgress), StackHeight = StackHeight, diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs index 5f9a978902..966db73eb9 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs @@ -1,10 +1,30 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; + namespace osu.Game.Rulesets.Osu.Objects { public class SliderTick : OsuHitObject { public int SpanIndex { get; set; } + public double SpanStartTime { get; set; } + + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + { + base.ApplyDefaultsToSelf(controlPointInfo, difficulty); + + double offset; + + if (SpanIndex > 0) + // Adding 200 to include the offset stable used. + // This is so on repeats ticks don't appear too late to be visually processed by the player. + offset = 200; + else + offset = TimeFadein * 0.66f; + + TimePreempt = (StartTime - SpanStartTime) / 2 + offset; + } } } diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs index 70cfbebfff..fb5acbb643 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs @@ -29,8 +29,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty protected override void PreprocessHitObjects() { - foreach (OsuHitObject h in Beatmap.HitObjects) - (h as Slider)?.Curve?.Calculate(); + new OsuBeatmapProcessor().PostProcess(Beatmap); } public override double Calculate(Dictionary categoryDifficulty = null) diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs index 4f950353dc..c817cd0ff3 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -3,7 +3,6 @@ using System; using System.Linq; -using osu.Game.Rulesets.Objects.Types; using OpenTK; using osu.Game.Rulesets.Osu.Objects; @@ -95,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing var computeVertex = new Action(t => { // ReSharper disable once PossibleInvalidOperationException (bugged in current r# version) - var diff = slider.PositionAt(t) - slider.LazyEndPosition.Value; + var diff = slider.StackedPositionAt(t) - slider.LazyEndPosition.Value; float dist = diff.Length; if (dist > approxFollowCircleRadius) diff --git a/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs b/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs index 00c924c15e..ba555efb8e 100644 --- a/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs +++ b/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs @@ -31,7 +31,9 @@ namespace osu.Game.Rulesets.Osu.Tests { typeof(SliderBall), typeof(SliderBody), + typeof(SliderTick), typeof(DrawableSlider), + typeof(DrawableSliderTick), typeof(DrawableRepeatPoint), typeof(DrawableOsuHitObject) }; @@ -45,6 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests public TestCaseSlider() { base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 })); + AddStep("Big Single", () => testSimpleBig()); AddStep("Medium Single", () => testSimpleMedium()); AddStep("Small Single", () => testSimpleSmall()); @@ -76,14 +79,17 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("Linear Slider 1 Repeat", () => testLinear(1)); AddStep("Linear Slider 2 Repeats", () => testLinear(2)); - AddStep("Bezier Slider", () => testBeizer()); - AddStep("Bezier Slider 1 Repeat", () => testBeizer(1)); - AddStep("Bezier Slider 2 Repeats", () => testBeizer(2)); + AddStep("Bezier Slider", () => testBezier()); + AddStep("Bezier Slider 1 Repeat", () => testBezier(1)); + AddStep("Bezier Slider 2 Repeats", () => testBezier(2)); - AddStep("Linear Overlapping", () => testLinearOverlaping()); - AddStep("Linear Overlapping 1 Repeat", () => testLinearOverlaping(1)); - AddStep("Linear Overlapping 2 Repeats", () => testLinearOverlaping(2)); - // TODO add catmull + AddStep("Linear Overlapping", () => testLinearOverlapping()); + AddStep("Linear Overlapping 1 Repeat", () => testLinearOverlapping(1)); + AddStep("Linear Overlapping 2 Repeats", () => testLinearOverlapping(2)); + + AddStep("Catmull Slider", () => testCatmull()); + AddStep("Catmull Slider 1 Repeat", () => testCatmull(1)); + AddStep("Catmull Slider 2 Repeats", () => testCatmull(2)); } private void testSimpleBig(int repeats = 0) => createSlider(2, repeats: repeats); @@ -120,7 +126,9 @@ namespace osu.Game.Rulesets.Osu.Tests addSlider(slider, circleSize, speedMultiplier); } - private void testPerfect(int repeats = 0) + private void testPerfect(int repeats = 0) => createPerfect(repeats); + + private void createPerfect(int repeats) { var slider = new Slider { @@ -141,7 +149,9 @@ namespace osu.Game.Rulesets.Osu.Tests addSlider(slider, 2, 3); } - private void testLinear(int repeats = 0) + private void testLinear(int repeats = 0) => createLinear(repeats); + + private void createLinear(int repeats) { var slider = new Slider { @@ -166,7 +176,9 @@ namespace osu.Game.Rulesets.Osu.Tests addSlider(slider, 2, 3); } - private void testBeizer(int repeats = 0) + private void testBezier(int repeats = 0) => createBezier(repeats); + + private void createBezier(int repeats) { var slider = new Slider { @@ -190,7 +202,9 @@ namespace osu.Game.Rulesets.Osu.Tests addSlider(slider, 2, 3); } - private void testLinearOverlaping(int repeats = 0) + private void testLinearOverlapping(int repeats = 0) => createOverlapping(repeats); + + private void createOverlapping(int repeats) { var slider = new Slider { @@ -215,6 +229,35 @@ namespace osu.Game.Rulesets.Osu.Tests addSlider(slider, 2, 3); } + private void testCatmull(int repeats = 0) => createCatmull(repeats); + + private void createCatmull(int repeats = 0) + { + var repeatSamples = new List>(); + for (int i = 0; i < repeats; i++) + repeatSamples.Add(new List()); + + var slider = new Slider + { + StartTime = Time.Current + 1000, + Position = new Vector2(-100, 0), + ComboColour = Color4.LightSeaGreen, + CurveType = CurveType.Catmull, + ControlPoints = new List + { + new Vector2(-100, 0), + new Vector2(-50, -50), + new Vector2(50, 50), + new Vector2(100, 0) + }, + Distance = 300, + RepeatCount = repeats, + RepeatSamples = repeatSamples + }; + + addSlider(slider, 3, 1); + } + private List> createEmptySamples(int repeats) { var repeatSamples = new List>(); @@ -228,7 +271,7 @@ namespace osu.Game.Rulesets.Osu.Tests var cpi = new ControlPointInfo(); cpi.DifficultyPoints.Add(new DifficultyControlPoint { SpeedMultiplier = speedMultiplier }); - slider.ApplyDefaults(cpi, new BeatmapDifficulty { CircleSize = circleSize }); + slider.ApplyDefaults(cpi, new BeatmapDifficulty { CircleSize = circleSize, SliderTickRate = 3 }); var drawable = new DrawableSlider(slider) { diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index 9b8bb83bb8..b91f37d84d 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring /// /// Taiko fails at the end of the map if the player has not half-filled their HP bar. /// - protected override bool DefaultFailCondition => Hits == MaxHits && Health.Value <= 0.5; + protected override bool DefaultFailCondition => JudgedHits == MaxHits && Health.Value <= 0.5; private double hpIncreaseTick; private double hpIncreaseGreat; diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs new file mode 100644 index 0000000000..f102e4c59f --- /dev/null +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -0,0 +1,247 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Game.Online.Chat; + +namespace osu.Game.Tests.Chat +{ + [TestFixture] + public class MessageFormatterTests + { + [Test] + public void TestBareLink() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a http://www.basic-link.com/?test=test." }); + + Assert.AreEqual(result.Content, result.DisplayContent); + Assert.AreEqual(1, result.Links.Count); + Assert.AreEqual("http://www.basic-link.com/?test=test", result.Links[0].Url); + Assert.AreEqual(10, result.Links[0].Index); + Assert.AreEqual(36, result.Links[0].Length); + } + + [Test] + public void TestMultipleComplexLinks() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a http://test.io/link#fragment. (see https://twitter.com). Also, This string should not be altered. http://example.com/" }); + + Assert.AreEqual(result.Content, result.DisplayContent); + Assert.AreEqual(3, result.Links.Count); + + Assert.AreEqual("http://test.io/link#fragment", result.Links[0].Url); + Assert.AreEqual(10, result.Links[0].Index); + Assert.AreEqual(28, result.Links[0].Length); + + Assert.AreEqual("https://twitter.com", result.Links[1].Url); + Assert.AreEqual(45, result.Links[1].Index); + Assert.AreEqual(19, result.Links[1].Length); + + Assert.AreEqual("http://example.com/", result.Links[2].Url); + Assert.AreEqual(108, result.Links[2].Index); + Assert.AreEqual(19, result.Links[2].Length); + } + + [Test] + public void TestAjaxLinks() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "https://twitter.com/#!/hashbanglinks" }); + + Assert.AreEqual(result.Content, result.DisplayContent); + Assert.AreEqual(result.Content, result.Links[0].Url); + Assert.AreEqual(0, result.Links[0].Index); + Assert.AreEqual(36, result.Links[0].Length); + } + + [Test] + public void TestUnixHomeLinks() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "http://www.chiark.greenend.org.uk/~sgtatham/putty/" }); + + Assert.AreEqual(result.Content, result.DisplayContent); + Assert.AreEqual(result.Content, result.Links[0].Url); + Assert.AreEqual(0, result.Links[0].Index); + Assert.AreEqual(50, result.Links[0].Length); + } + + [Test] + public void TestCaseInsensitiveLinks() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "look: http://puu.sh/7Ggh8xcC6/asf0asd9876.NEF" }); + + Assert.AreEqual(result.Content, result.DisplayContent); + Assert.AreEqual(6, result.Links[0].Index); + Assert.AreEqual(39, result.Links[0].Length); + } + + [Test] + public void TestWikiLink() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a [[Wiki Link]]." }); + + Assert.AreEqual("This is a Wiki Link.", result.DisplayContent); + Assert.AreEqual(1, result.Links.Count); + Assert.AreEqual("https://osu.ppy.sh/wiki/Wiki Link", result.Links[0].Url); + Assert.AreEqual(10, result.Links[0].Index); + Assert.AreEqual(9, result.Links[0].Length); + } + + [Test] + public void TestMultiWikiLink() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a [[Wiki Link]] [[Wiki:Link]][[Wiki.Link]]." }); + + Assert.AreEqual("This is a Wiki Link Wiki:LinkWiki.Link.", result.DisplayContent); + Assert.AreEqual(3, result.Links.Count); + + Assert.AreEqual("https://osu.ppy.sh/wiki/Wiki Link", result.Links[0].Url); + Assert.AreEqual(10, result.Links[0].Index); + Assert.AreEqual(9, result.Links[0].Length); + + Assert.AreEqual("https://osu.ppy.sh/wiki/Wiki:Link", result.Links[1].Url); + Assert.AreEqual(20, result.Links[1].Index); + Assert.AreEqual(9, result.Links[1].Length); + + Assert.AreEqual("https://osu.ppy.sh/wiki/Wiki.Link", result.Links[2].Url); + Assert.AreEqual(29, result.Links[2].Index); + Assert.AreEqual(9, result.Links[2].Length); + } + + [Test] + public void TestOldFormatLink() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a (simple test)[https://osu.ppy.sh] of links." }); + + Assert.AreEqual("This is a simple test of links.", result.DisplayContent); + Assert.AreEqual(1, result.Links.Count); + Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url); + Assert.AreEqual(10, result.Links[0].Index); + Assert.AreEqual(11, result.Links[0].Length); + } + + [Test] + public void TestNewFormatLink() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a [https://osu.ppy.sh simple test]." }); + + Assert.AreEqual("This is a simple test.", result.DisplayContent); + Assert.AreEqual(1, result.Links.Count); + Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url); + Assert.AreEqual(10, result.Links[0].Index); + Assert.AreEqual(11, result.Links[0].Length); + } + + [Test] + public void TestMarkdownFormatLink() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a [simple test](https://osu.ppy.sh)." }); + + Assert.AreEqual("This is a simple test.", result.DisplayContent); + Assert.AreEqual(1, result.Links.Count); + Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url); + Assert.AreEqual(10, result.Links[0].Index); + Assert.AreEqual(11, result.Links[0].Length); + } + + [Test] + public void TestChannelLink() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This is an #english and #japanese." }); + + Assert.AreEqual(result.Content, result.DisplayContent); + Assert.AreEqual(2, result.Links.Count); + Assert.AreEqual("osu://chan/#english", result.Links[0].Url); + Assert.AreEqual("osu://chan/#japanese", result.Links[1].Url); + } + + [Test] + public void TestOsuProtocol() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a custom protocol osu://chan/#english." }); + + Assert.AreEqual(result.Content, result.DisplayContent); + Assert.AreEqual(1, result.Links.Count); + Assert.AreEqual("osu://chan/#english", result.Links[0].Url); + Assert.AreEqual(26, result.Links[0].Index); + Assert.AreEqual(19, result.Links[0].Length); + + result = MessageFormatter.FormatMessage(new Message { Content = "This is a [custom protocol](osu://chan/#english)." }); + + Assert.AreEqual("This is a custom protocol.", result.DisplayContent); + Assert.AreEqual(1, result.Links.Count); + Assert.AreEqual("osu://chan/#english", result.Links[0].Url); + Assert.AreEqual("#english", result.Links[0].Argument); + Assert.AreEqual(10, result.Links[0].Index); + Assert.AreEqual(15, result.Links[0].Length); + } + + [Test] + public void TestOsuMpProtocol() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "Join my multiplayer game osump://12346." }); + + Assert.AreEqual(result.Content, result.DisplayContent); + Assert.AreEqual(1, result.Links.Count); + Assert.AreEqual("osump://12346", result.Links[0].Url); + Assert.AreEqual(25, result.Links[0].Index); + Assert.AreEqual(13, result.Links[0].Length); + } + + [Test] + public void TestRecursiveBreaking() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a [https://osu.ppy.sh [[simple test]]]." }); + + Assert.AreEqual("This is a [[simple test]].", result.DisplayContent); + Assert.AreEqual(1, result.Links.Count); + Assert.AreEqual("https://osu.ppy.sh", result.Links[0].Url); + Assert.AreEqual(10, result.Links[0].Index); + Assert.AreEqual(15, result.Links[0].Length); + } + + [Test] + public void TestLinkComplex() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "This is a [http://www.simple-test.com simple test] with some [traps] and [[wiki links]]. Don't forget to visit https://osu.ppy.sh (now!)[http://google.com]\uD83D\uDE12" }); + + Assert.AreEqual("This is a simple test with some [traps] and wiki links. Don't forget to visit https://osu.ppy.sh now!\0\0\0", result.DisplayContent); + Assert.AreEqual(5, result.Links.Count); + + Link f = result.Links.Find(l => l.Url == "https://osu.ppy.sh/wiki/wiki links"); + Assert.AreEqual(44, f.Index); + Assert.AreEqual(10, f.Length); + + f = result.Links.Find(l => l.Url == "http://www.simple-test.com"); + Assert.AreEqual(10, f.Index); + Assert.AreEqual(11, f.Length); + + f = result.Links.Find(l => l.Url == "http://google.com"); + Assert.AreEqual(97, f.Index); + Assert.AreEqual(4, f.Length); + + f = result.Links.Find(l => l.Url == "https://osu.ppy.sh"); + Assert.AreEqual(78, f.Index); + Assert.AreEqual(18, f.Length); + + f = result.Links.Find(l => l.Url == "\uD83D\uDE12"); + Assert.AreEqual(101, f.Index); + Assert.AreEqual(3, f.Length); + } + + [Test] + public void TestEmoji() + { + Message result = MessageFormatter.FormatMessage(new Message { Content = "Hello world\uD83D\uDE12<--This is an emoji,There are more:\uD83D\uDE10\uD83D\uDE00,\uD83D\uDE20" }); + Assert.AreEqual("Hello world\0\0\0<--This is an emoji,There are more:\0\0\0\0\0\0,\0\0\0", result.DisplayContent); + Assert.AreEqual(result.Links.Count, 4); + Assert.AreEqual(result.Links[0].Index, 11); + Assert.AreEqual(result.Links[1].Index, 49); + Assert.AreEqual(result.Links[2].Index, 52); + Assert.AreEqual(result.Links[3].Index, 56); + Assert.AreEqual(result.Links[0].Url, "\uD83D\uDE12"); + Assert.AreEqual(result.Links[1].Url, "\uD83D\uDE10"); + Assert.AreEqual(result.Links[2].Url, "\uD83D\uDE00"); + Assert.AreEqual(result.Links[3].Url, "\uD83D\uDE20"); + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseChatLink.cs b/osu.Game.Tests/Visual/TestCaseChatLink.cs new file mode 100644 index 0000000000..3a7be686e1 --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseChatLink.cs @@ -0,0 +1,217 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Online.Chat; +using osu.Game.Overlays.Chat; +using osu.Game.Users; +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; + +namespace osu.Game.Tests.Visual +{ + public class TestCaseChatLink : OsuTestCase + { + private readonly TestChatLineContainer textContainer; + private Color4 linkColour; + + public override IReadOnlyList RequiredTypes => new[] + { + typeof(ChatLine), + typeof(Message), + typeof(LinkFlowContainer), + typeof(DummyEchoMessage), + typeof(LocalEchoMessage), + typeof(MessageFormatter) + }; + + private DependencyContainer dependencies; + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(parent); + + public TestCaseChatLink() + { + Add(textContainer = new TestChatLineContainer + { + Padding = new MarginPadding { Left = 20, Right = 20 }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + }); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + linkColour = colours.Blue; + dependencies.Cache(new ChatOverlay + { + AvailableChannels = + { + new Channel { Name = "#english" }, + new Channel { Name = "#japanese" } + } + }); + + testLinksGeneral(); + testEcho(); + } + + private void clear() => AddStep("clear messages", textContainer.Clear); + + private void addMessageWithChecks(string text, int linkAmount = 0, bool isAction = false, bool isImportant = false, params LinkAction[] expectedActions) + { + int index = textContainer.Count + 1; + var newLine = new ChatLine(new DummyMessage(text, isAction, isImportant, index)); + textContainer.Add(newLine); + + AddAssert($"msg #{index} has {linkAmount} link(s)", () => newLine.Message.Links.Count == linkAmount); + AddAssert($"msg #{index} has the right action", hasExpectedActions); + AddAssert($"msg #{index} is " + (isAction ? "italic" : "not italic"), () => newLine.ContentFlow.Any() && isAction == isItalic()); + AddAssert($"msg #{index} shows {linkAmount} link(s)", isShowingLinks); + + bool hasExpectedActions() + { + var expectedActionsList = expectedActions.ToList(); + + if (expectedActionsList.Count != newLine.Message.Links.Count) + return false; + + for (int i = 0; i < newLine.Message.Links.Count; i++) + { + var action = newLine.Message.Links[i].Action; + if (action != expectedActions[i]) return false; + } + + return true; + } + + bool isItalic() => newLine.ContentFlow.Where(d => d is OsuSpriteText).Cast().All(sprite => sprite.Font == "Exo2.0-MediumItalic"); + + bool isShowingLinks() + { + bool hasBackground = !string.IsNullOrEmpty(newLine.Message.Sender.Colour); + + Color4 textColour = isAction && hasBackground ? OsuColour.FromHex(newLine.Message.Sender.Colour) : Color4.White; + + var linkCompilers = newLine.ContentFlow.Where(d => d is DrawableLinkCompiler).ToList(); + var linkSprites = linkCompilers.SelectMany(comp => ((DrawableLinkCompiler)comp).Parts); + + return linkSprites.All(d => d.Colour == linkColour) + && newLine.ContentFlow.Except(linkSprites.Concat(linkCompilers)).All(d => d.Colour == textColour); + } + } + + private void testLinksGeneral() + { + addMessageWithChecks("test!"); + addMessageWithChecks("osu.ppy.sh!"); + addMessageWithChecks("https://osu.ppy.sh!", 1, expectedActions: LinkAction.External); + addMessageWithChecks("00:12:345 (1,2) - Test?", 1, expectedActions: LinkAction.OpenEditorTimestamp); + addMessageWithChecks("Wiki link for tasty [[Performance Points]]", 1, expectedActions: LinkAction.External); + addMessageWithChecks("(osu forums)[https://osu.ppy.sh/forum] (old link format)", 1, expectedActions: LinkAction.External); + addMessageWithChecks("[https://osu.ppy.sh/home New site] (new link format)", 1, expectedActions: LinkAction.External); + addMessageWithChecks("[osu forums](https://osu.ppy.sh/forum) (new link format 2)", 1, expectedActions: LinkAction.External); + addMessageWithChecks("[https://osu.ppy.sh/home This is only a link to the new osu webpage but this is supposed to test word wrap.]", 1, expectedActions: LinkAction.External); + addMessageWithChecks("is now listening to [https://osu.ppy.sh/s/93523 IMAGE -MATERIAL- ]", 1, true, expectedActions: LinkAction.OpenBeatmapSet); + addMessageWithChecks("is now playing [https://osu.ppy.sh/b/252238 IMAGE -MATERIAL- ]", 1, true, expectedActions: LinkAction.OpenBeatmap); + addMessageWithChecks("Let's (try)[https://osu.ppy.sh/home] [https://osu.ppy.sh/b/252238 multiple links] https://osu.ppy.sh/home", 3, + expectedActions: new[] { LinkAction.External, LinkAction.OpenBeatmap, LinkAction.External }); + // note that there's 0 links here (they get removed if a channel is not found) + addMessageWithChecks("#lobby or #osu would be blue (and work) in the ChatDisplay test (when a proper ChatOverlay is present)."); + addMessageWithChecks("I am important!", 0, false, true); + addMessageWithChecks("feels important", 0, true, true); + addMessageWithChecks("likes to post this [https://osu.ppy.sh/home link].", 1, true, true, expectedActions: LinkAction.External); + addMessageWithChecks("Join my multiplayer game osump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch); + addMessageWithChecks("Join my [multiplayer game](osump://12346).", 1, expectedActions: LinkAction.JoinMultiplayerMatch); + addMessageWithChecks("Join my [#english](osu://chan/#english).", 1, expectedActions: LinkAction.OpenChannel); + addMessageWithChecks("Join my osu://chan/#english.", 1, expectedActions: LinkAction.OpenChannel); + addMessageWithChecks("Join my #english or #japanese channels.", 2, expectedActions: new[] { LinkAction.OpenChannel, LinkAction.OpenChannel }); + addMessageWithChecks("Join my #english or #nonexistent #hashtag channels.", 1, expectedActions: LinkAction.OpenChannel); + } + + private void testEcho() + { + int echoCounter = 0; + + addEchoWithWait("sent!", "received!"); + addEchoWithWait("https://osu.ppy.sh/home", null, 500); + addEchoWithWait("[https://osu.ppy.sh/forum let's try multiple words too!]"); + addEchoWithWait("(long loading times! clickable while loading?)[https://osu.ppy.sh/home]", null, 5000); + + void addEchoWithWait(string text, string completeText = null, double delay = 250) + { + var newLine = new ChatLine(new DummyEchoMessage(text)); + + AddStep($"send msg #{++echoCounter} after {delay}ms", () => + { + textContainer.Add(newLine); + Scheduler.AddDelayed(() => newLine.Message = new DummyMessage(completeText ?? text), delay); + }); + + AddUntilStep(() => textContainer.All(line => line.Message is DummyMessage), $"wait for msg #{echoCounter}"); + } + } + + private class DummyEchoMessage : LocalEchoMessage + { + public DummyEchoMessage(string text) + { + Content = text; + Timestamp = DateTimeOffset.Now; + Sender = DummyMessage.TEST_SENDER; + } + } + + private class DummyMessage : Message + { + private static long messageCounter; + + internal static readonly User TEST_SENDER_BACKGROUND = new User + { + Username = @"i-am-important", + Id = 42, + Colour = "#250cc9", + }; + + internal static readonly User TEST_SENDER = new User + { + Username = @"Somebody", + Id = 1, + }; + + public new DateTimeOffset Timestamp = DateTimeOffset.Now; + + public DummyMessage(string text, bool isAction = false, bool isImportant = false, int number = 0) + : base(messageCounter++) + { + Content = text; + IsAction = isAction; + Sender = new User + { + Username = $"User {number}", + Id = number, + Colour = isImportant ? "#250cc9" : null, + }; + } + } + + private class TestChatLineContainer : FillFlowContainer + { + protected override int Compare(Drawable x, Drawable y) + { + var xC = (ChatLine)x; + var yC = (ChatLine)y; + + return xC.Message.CompareTo(yC.Message); + } + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseReplaySettingsOverlay.cs b/osu.Game.Tests/Visual/TestCaseReplaySettingsOverlay.cs index a5d8019bc1..595a93b194 100644 --- a/osu.Game.Tests/Visual/TestCaseReplaySettingsOverlay.cs +++ b/osu.Game.Tests/Visual/TestCaseReplaySettingsOverlay.cs @@ -4,7 +4,7 @@ using osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Play.HUD; -using osu.Game.Screens.Play.ReplaySettings; +using osu.Game.Screens.Play.PlayerSettings; namespace osu.Game.Tests.Visual { @@ -14,7 +14,7 @@ namespace osu.Game.Tests.Visual { ExampleContainer container; - Add(new ReplaySettingsOverlay + Add(new PlayerSettingsOverlay { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, @@ -28,7 +28,7 @@ namespace osu.Game.Tests.Visual Text = @"Button", })); - AddStep(@"Add checkbox", () => container.Add(new ReplayCheckbox + AddStep(@"Add checkbox", () => container.Add(new PlayerCheckbox { LabelText = "Checkbox", })); @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual })); } - private class ExampleContainer : ReplayGroup + private class ExampleContainer : PlayerSettingsGroup { protected override string Title => @"example"; } diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index d30241fae4..8301f1f734 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -94,6 +94,7 @@ + @@ -137,6 +138,7 @@ + diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 33810c9712..c33dd91330 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -39,6 +39,8 @@ namespace osu.Game.Configuration }; // Audio + Set(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01); + Set(OsuSetting.MenuVoice, true); Set(OsuSetting.MenuMusic, true); @@ -101,6 +103,7 @@ namespace osu.Game.Configuration MouseDisableButtons, MouseDisableWheel, AudioOffset, + VolumeInactive, MenuMusic, MenuVoice, CursorRotation, diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs new file mode 100644 index 0000000000..9f1b44af44 --- /dev/null +++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs @@ -0,0 +1,100 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Online.Chat; +using System; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using System.Collections.Generic; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; + +namespace osu.Game.Graphics.Containers +{ + public class LinkFlowContainer : OsuTextFlowContainer + { + public LinkFlowContainer(Action defaultCreationParameters = null) + : base(defaultCreationParameters) + { + } + + public override bool HandleMouseInput => true; + + private OsuGame game; + + private Action showNotImplementedError; + + [BackgroundDependencyLoader(true)] + private void load(OsuGame game, NotificationOverlay notifications) + { + // will be null in tests + this.game = game; + + showNotImplementedError = () => notifications?.Post(new SimpleNotification + { + Text = @"This link type is not yet supported!", + Icon = FontAwesome.fa_life_saver, + }); + } + + public void AddLinks(string text, List links) + { + if (string.IsNullOrEmpty(text) || links == null) + return; + + if (links.Count == 0) + { + AddText(text); + return; + } + + int previousLinkEnd = 0; + foreach (var link in links) + { + AddText(text.Substring(previousLinkEnd, link.Index - previousLinkEnd)); + AddLink(text.Substring(link.Index, link.Length), link.Url, link.Action, link.Argument); + previousLinkEnd = link.Index + link.Length; + } + + AddText(text.Substring(previousLinkEnd)); + } + + public void AddLink(string text, string url, LinkAction linkType = LinkAction.External, string linkArgument = null, string tooltipText = null) + { + AddInternal(new DrawableLinkCompiler(AddText(text).ToList()) + { + TooltipText = tooltipText ?? (url != text ? url : string.Empty), + Action = () => + { + switch (linkType) + { + case LinkAction.OpenBeatmap: + // todo: replace this with overlay.ShowBeatmap(id) once an appropriate API call is implemented. + if (int.TryParse(linkArgument, out int beatmapId)) + Process.Start($"https://osu.ppy.sh/b/{beatmapId}"); + break; + case LinkAction.OpenBeatmapSet: + if (int.TryParse(linkArgument, out int setId)) + game?.ShowBeatmapSet(setId); + break; + case LinkAction.OpenChannel: + game?.OpenChannel(linkArgument); + break; + case LinkAction.OpenEditorTimestamp: + case LinkAction.JoinMultiplayerMatch: + case LinkAction.Spectate: + showNotImplementedError?.Invoke(); + break; + case LinkAction.External: + Process.Start(url); + break; + default: + throw new NotImplementedException($"This {nameof(LinkAction)} ({linkType.ToString()}) is missing an associated action."); + } + }, + }); + } + } +} diff --git a/osu.Game/Graphics/Containers/OsuClickableContainer.cs b/osu.Game/Graphics/Containers/OsuClickableContainer.cs index 4e95050bda..b9ee1f4463 100644 --- a/osu.Game/Graphics/Containers/OsuClickableContainer.cs +++ b/osu.Game/Graphics/Containers/OsuClickableContainer.cs @@ -16,6 +16,8 @@ namespace osu.Game.Graphics.Containers protected override Container Content => content; + protected virtual HoverClickSounds CreateHoverClickSounds(HoverSampleSet sampleSet) => new HoverClickSounds(sampleSet); + public OsuClickableContainer(HoverSampleSet sampleSet = HoverSampleSet.Normal) { this.sampleSet = sampleSet; @@ -33,7 +35,7 @@ namespace osu.Game.Graphics.Containers InternalChildren = new Drawable[] { content, - new HoverClickSounds(sampleSet) + CreateHoverClickSounds(sampleSet) }; } } diff --git a/osu.Game/Graphics/Containers/OsuHoverContainer.cs b/osu.Game/Graphics/Containers/OsuHoverContainer.cs index 7c62e90f56..fd1742871b 100644 --- a/osu.Game/Graphics/Containers/OsuHoverContainer.cs +++ b/osu.Game/Graphics/Containers/OsuHoverContainer.cs @@ -1,8 +1,10 @@ // 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 OpenTK.Graphics; using osu.Framework.Allocation; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Input; @@ -10,24 +12,34 @@ namespace osu.Game.Graphics.Containers { public class OsuHoverContainer : OsuClickableContainer { - private Color4 hoverColour; + protected Color4 HoverColour; + + protected Color4 IdleColour = Color4.White; + + protected virtual IEnumerable EffectTargets => new[] { Content }; protected override bool OnHover(InputState state) { - this.FadeColour(hoverColour, 500, Easing.OutQuint); + EffectTargets.ForEach(d => d.FadeColour(HoverColour, 500, Easing.OutQuint)); return base.OnHover(state); } protected override void OnHoverLost(InputState state) { - this.FadeColour(Color4.White, 500, Easing.OutQuint); + EffectTargets.ForEach(d => d.FadeColour(IdleColour, 500, Easing.OutQuint)); base.OnHoverLost(state); } [BackgroundDependencyLoader] private void load(OsuColour colours) { - hoverColour = colours.Yellow; + HoverColour = colours.Yellow; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + EffectTargets.ForEach(d => d.FadeColour(IdleColour)); } } } diff --git a/osu.Game/Online/Chat/DrawableLinkCompiler.cs b/osu.Game/Online/Chat/DrawableLinkCompiler.cs new file mode 100644 index 0000000000..234781fb52 --- /dev/null +++ b/osu.Game/Online/Chat/DrawableLinkCompiler.cs @@ -0,0 +1,59 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics.Cursor; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using OpenTK; + +namespace osu.Game.Online.Chat +{ + /// + /// An invisible drawable that brings multiple pieces together to form a consumable clickable link. + /// + public class DrawableLinkCompiler : OsuHoverContainer, IHasTooltip + { + /// + /// Each word part of a chat link (split for word-wrap support). + /// + public List Parts; + + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => Parts.Any(d => d.ReceiveMouseInputAt(screenSpacePos)); + + protected override HoverClickSounds CreateHoverClickSounds(HoverSampleSet sampleSet) => new LinkHoverSounds(sampleSet, Parts); + + public DrawableLinkCompiler(IEnumerable parts) + { + Parts = parts.ToList(); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + IdleColour = colours.Blue; + } + + protected override IEnumerable EffectTargets => Parts; + + public string TooltipText { get; set; } + + private class LinkHoverSounds : HoverClickSounds + { + private readonly List parts; + + public LinkHoverSounds(HoverSampleSet sampleSet, List parts) + : base(sampleSet) + { + this.parts = parts; + } + + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => parts.Any(d => d.ReceiveMouseInputAt(screenSpacePos)); + } + } +} diff --git a/osu.Game/Online/Chat/Message.cs b/osu.Game/Online/Chat/Message.cs index bc79469ce0..99735c4d65 100644 --- a/osu.Game/Online/Chat/Message.cs +++ b/osu.Game/Online/Chat/Message.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Collections.Generic; using System.ComponentModel; using Newtonsoft.Json; using osu.Game.Users; @@ -40,6 +41,17 @@ namespace osu.Game.Online.Chat { } + /// + /// The text that is displayed in chat. + /// + public string DisplayContent { get; set; } + + /// + /// The links found in this message. + /// + /// The s' and s are according to + public List Links; + public Message(long? id) { Id = id; diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs new file mode 100644 index 0000000000..906f42d50e --- /dev/null +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -0,0 +1,263 @@ +// 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.Text.RegularExpressions; + +namespace osu.Game.Online.Chat +{ + public static class MessageFormatter + { + // [[Performance Points]] -> wiki:Performance Points (https://osu.ppy.sh/wiki/Performance_Points) + private static readonly Regex wiki_regex = new Regex(@"\[\[([^\]]+)\]\]"); + + // (test)[https://osu.ppy.sh/b/1234] -> test (https://osu.ppy.sh/b/1234) + private static readonly Regex old_link_regex = new Regex(@"\(([^\)]*)\)\[([a-z]+://[^ ]+)\]"); + + // [https://osu.ppy.sh/b/1234 Beatmap [Hard] (poop)] -> Beatmap [hard] (poop) (https://osu.ppy.sh/b/1234) + private static readonly Regex new_link_regex = new Regex(@"\[([a-z]+://[^ ]+) ([^\[\]]*(((?\[)[^\[\]]*)+((?\])[^\[\]]*)+)*(?(open)(?!)))\]"); + + // [test](https://osu.ppy.sh/b/1234) -> test (https://osu.ppy.sh/b/1234) aka correct markdown format + private static readonly Regex markdown_link_regex = new Regex(@"\[([^\]]*)\]\(([a-z]+://[^ ]+)\)"); + + // advanced, RFC-compatible regular expression that matches any possible URL, *but* allows certain invalid characters that are widely used + // This is in the format (, [optional]): + // http[s]://.[:port][/path][?query][#fragment] + private static readonly Regex advanced_link_regex = new Regex( + // protocol + @"(?[a-z]*?:\/\/" + + // domain + tld + @"(?(?:[a-z0-9]\.|[a-z0-9][a-z0-9-]*[a-z0-9]\.)*[a-z0-9-]*[a-z0-9]" + + // port (optional) + @"(?::\d+)?)" + + // path (optional) + @"(?(?:(?:\/+(?:[a-z0-9$_\.\+!\*\',;:\(\)@&~=-]|%[0-9a-f]{2})*)*" + + // query (optional) + @"(?:\?(?:[a-z0-9$_\+!\*\',;:\(\)@&=\/~-]|%[0-9a-f]{2})*)?)?" + + // fragment (optional) + @"(?:#(?:[a-z0-9$_\+!\*\',;:\(\)@&=\/~-]|%[0-9a-f]{2})*)?)?)", + RegexOptions.IgnoreCase); + + // 00:00:000 (1,2,3) - test + private static readonly Regex time_regex = new Regex(@"\d\d:\d\d:\d\d\d? [^-]*"); + + // #osu + private static readonly Regex channel_regex = new Regex(@"(#[a-zA-Z]+[a-zA-Z0-9]+)"); + + // Unicode emojis + private static readonly Regex emoji_regex = new Regex(@"(\uD83D[\uDC00-\uDE4F])"); + + private static void handleMatches(Regex regex, string display, string link, MessageFormatterResult result, int startIndex = 0, LinkAction? linkActionOverride = null) + { + int captureOffset = 0; + foreach (Match m in regex.Matches(result.Text, startIndex)) + { + var index = m.Index - captureOffset; + + var displayText = string.Format(display, + m.Groups[0], + m.Groups.Count > 1 ? m.Groups[1].Value : "", + m.Groups.Count > 2 ? m.Groups[2].Value : "").Trim(); + + var linkText = string.Format(link, + m.Groups[0], + m.Groups.Count > 1 ? m.Groups[1].Value : "", + m.Groups.Count > 2 ? m.Groups[2].Value : "").Trim(); + + if (displayText.Length == 0 || linkText.Length == 0) continue; + + // Check for encapsulated links + if (result.Links.Find(l => l.Index <= index && l.Index + l.Length >= index + m.Length || index <= l.Index && index + m.Length >= l.Index + l.Length) == null) + { + result.Text = result.Text.Remove(index, m.Length).Insert(index, displayText); + + //since we just changed the line display text, offset any already processed links. + result.Links.ForEach(l => l.Index -= l.Index > index ? m.Length - displayText.Length : 0); + + var details = getLinkDetails(linkText); + result.Links.Add(new Link(linkText, index, displayText.Length, linkActionOverride ?? details.Action, details.Argument)); + + //adjust the offset for processing the current matches group. + captureOffset += m.Length - displayText.Length; + } + } + } + + private static void handleAdvanced(Regex regex, MessageFormatterResult result, int startIndex = 0) + { + foreach (Match m in regex.Matches(result.Text, startIndex)) + { + var index = m.Index; + var link = m.Groups["link"].Value; + var indexLength = link.Length; + + var details = getLinkDetails(link); + result.Links.Add(new Link(link, index, indexLength, details.Action, details.Argument)); + } + } + + private static LinkDetails getLinkDetails(string url) + { + var args = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + args[0] = args[0].TrimEnd(':'); + + switch (args[0]) + { + case "http": + case "https": + // length > 3 since all these links need another argument to work + if (args.Length > 3 && (args[1] == "osu.ppy.sh" || args[1] == "new.ppy.sh")) + { + switch (args[2]) + { + case "b": + case "beatmaps": + return new LinkDetails(LinkAction.OpenBeatmap, args[3]); + case "s": + case "beatmapsets": + case "d": + return new LinkDetails(LinkAction.OpenBeatmapSet, args[3]); + } + } + + return new LinkDetails(LinkAction.External, null); + case "osu": + // every internal link also needs some kind of argument + if (args.Length < 3) + return new LinkDetails(LinkAction.External, null); + + LinkAction linkType; + switch (args[1]) + { + case "chan": + linkType = LinkAction.OpenChannel; + break; + case "edit": + linkType = LinkAction.OpenEditorTimestamp; + break; + case "b": + linkType = LinkAction.OpenBeatmap; + break; + case "s": + case "dl": + linkType = LinkAction.OpenBeatmapSet; + break; + case "spectate": + linkType = LinkAction.Spectate; + break; + default: + linkType = LinkAction.External; + break; + } + + return new LinkDetails(linkType, args[2]); + case "osump": + return new LinkDetails(LinkAction.JoinMultiplayerMatch, args[1]); + default: + return new LinkDetails(LinkAction.External, null); + } + } + + private static MessageFormatterResult format(string toFormat, int startIndex = 0, int space = 3) + { + var result = new MessageFormatterResult(toFormat); + + // handle the [link display] format + handleMatches(new_link_regex, "{2}", "{1}", result, startIndex); + + // handle the standard markdown []() format + handleMatches(markdown_link_regex, "{1}", "{2}", result, startIndex); + + // handle the ()[] link format + handleMatches(old_link_regex, "{1}", "{2}", result, startIndex); + + // handle wiki links + handleMatches(wiki_regex, "{1}", "https://osu.ppy.sh/wiki/{1}", result, startIndex); + + // handle bare links + handleAdvanced(advanced_link_regex, result, startIndex); + + // handle editor times + handleMatches(time_regex, "{0}", "osu://edit/{0}", result, startIndex, LinkAction.OpenEditorTimestamp); + + // handle channels + handleMatches(channel_regex, "{0}", "osu://chan/{0}", result, startIndex, LinkAction.OpenChannel); + + var empty = ""; + while (space-- > 0) + empty += "\0"; + + handleMatches(emoji_regex, empty, "{0}", result, startIndex); + + return result; + } + + public static Message FormatMessage(Message inputMessage) + { + var result = format(inputMessage.Content); + + inputMessage.DisplayContent = result.Text; + + // Sometimes, regex matches are not in order + result.Links.Sort(); + inputMessage.Links = result.Links; + return inputMessage; + } + + public class MessageFormatterResult + { + public List Links = new List(); + public string Text; + public string OriginalText; + + public MessageFormatterResult(string text) + { + OriginalText = Text = text; + } + } + + public class LinkDetails + { + public LinkAction Action; + public string Argument; + + public LinkDetails(LinkAction action, string argument) + { + Action = action; + Argument = argument; + } + } + } + + public enum LinkAction + { + External, + OpenBeatmap, + OpenBeatmapSet, + OpenChannel, + OpenEditorTimestamp, + JoinMultiplayerMatch, + Spectate, + } + + public class Link : IComparable + { + public string Url; + public int Index; + public int Length; + public LinkAction Action; + public string Argument; + + public Link(string url, int startIndex, int length, LinkAction action, string argument) + { + Url = url; + Index = startIndex; + Length = length; + Action = action; + Argument = argument; + } + + public int CompareTo(Link otherLink) => Index > otherLink.Index ? 1 : -1; + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index bd71d37f97..624179cfe1 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -19,6 +19,7 @@ using OpenTK; using System.Linq; using System.Threading; using System.Threading.Tasks; +using osu.Framework.Audio; using osu.Framework.Input.Bindings; using osu.Framework.Platform; using osu.Framework.Threading; @@ -121,10 +122,24 @@ namespace osu.Game configRuleset = LocalConfig.GetBindable(OsuSetting.Ruleset); Ruleset.Value = RulesetStore.GetRuleset(configRuleset.Value) ?? RulesetStore.AvailableRulesets.First(); Ruleset.ValueChanged += r => configRuleset.Value = r.ID ?? 0; + + LocalConfig.BindWith(OsuSetting.VolumeInactive, inactiveVolumeAdjust); } private ScheduledDelegate scoreLoad; + /// + /// Open chat to a channel matching the provided name, if present. + /// + /// The name of the channel. + public void OpenChannel(string channelName) => chat.OpenChannel(chat.AvailableChannels.Find(c => c.Name == channelName)); + + /// + /// Show a beatmap set as an overlay. + /// + /// The set to display. + public void ShowBeatmapSet(int setId) => beatmapSetOverlay.ShowBeatmapSet(setId); + protected void LoadScore(Score s) { scoreLoad?.Cancel(); @@ -398,6 +413,20 @@ namespace osu.Game return false; } + private readonly BindableDouble inactiveVolumeAdjust = new BindableDouble(); + + protected override void OnDeactivated() + { + base.OnDeactivated(); + Audio.AddAdjustment(AdjustableProperty.Volume, inactiveVolumeAdjust); + } + + protected override void OnActivated() + { + base.OnActivated(); + Audio.RemoveAdjustment(AdjustableProperty.Volume, inactiveVolumeAdjust); + } + public bool OnReleased(GlobalAction action) => false; private Container mainContent; diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 4895c3a37c..dd41dd5428 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Linq; using OpenTK; using OpenTK.Graphics; using osu.Framework.Allocation; @@ -82,16 +83,18 @@ namespace osu.Game.Overlays.Chat private Message message; private OsuSpriteText username; - private OsuTextFlowContainer contentFlow; + private LinkFlowContainer contentFlow; + + public LinkFlowContainer ContentFlow => contentFlow; public Message Message { - get { return message; } + get => message; set { if (message == value) return; - message = value; + message = MessageFormatter.FormatMessage(value); if (!IsLoaded) return; @@ -101,8 +104,9 @@ namespace osu.Game.Overlays.Chat } [BackgroundDependencyLoader(true)] - private void load(OsuColour colours) + private void load(OsuColour colours, ChatOverlay chat) { + this.chat = chat; customUsernameColour = colours.ChatBlue; } @@ -187,7 +191,18 @@ namespace osu.Game.Overlays.Chat Padding = new MarginPadding { Left = message_padding + padding }, Children = new Drawable[] { - contentFlow = new OsuTextFlowContainer(t => { t.TextSize = text_size; }) + contentFlow = new LinkFlowContainer(t => + { + if (Message.IsAction) + { + t.Font = @"Exo2.0-MediumItalic"; + + if (senderHasBackground) + t.Colour = OsuColour.FromHex(message.Sender.Colour); + } + + t.TextSize = text_size; + }) { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, @@ -195,30 +210,26 @@ namespace osu.Game.Overlays.Chat } } }; - if (message.IsAction && senderHasBackground) - contentFlow.Colour = OsuColour.FromHex(message.Sender.Colour); updateMessageContent(); FinishTransforms(true); } + private ChatOverlay chat; + private void updateMessageContent() { this.FadeTo(message is LocalEchoMessage ? 0.4f : 1.0f, 500, Easing.OutQuint); timestamp.FadeTo(message is LocalEchoMessage ? 0 : 1, 500, Easing.OutQuint); timestamp.Text = $@"{message.Timestamp.LocalDateTime:HH:mm:ss}"; - username.Text = $@"{message.Sender.Username}" + (senderHasBackground ? "" : ":"); + username.Text = $@"{message.Sender.Username}" + (senderHasBackground || message.IsAction ? "" : ":"); - if (message.IsAction) - { - contentFlow.Clear(); - contentFlow.AddText("[", sprite => sprite.Padding = new MarginPadding { Right = action_padding }); - contentFlow.AddText(message.Content, sprite => sprite.Font = @"Exo2.0-MediumItalic"); - contentFlow.AddText("]", sprite => sprite.Padding = new MarginPadding { Left = action_padding }); - } - else - contentFlow.Text = message.Content; + // remove non-existent channels from the link list + message.Links.RemoveAll(link => link.Action == LinkAction.OpenChannel && chat?.AvailableChannels.Any(c => c.Name == link.Argument) != true); + + contentFlow.Clear(); + contentFlow.AddLinks(message.DisplayContent, message.Links); } private class MessageSender : OsuClickableContainer, IHasContextMenu diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 15c373356f..beb2b3b746 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -60,6 +60,7 @@ namespace osu.Game.Overlays public Bindable ChatHeight { get; set; } + public List AvailableChannels { get; private set; } = new List(); private readonly Container channelSelectionContainer; private readonly ChannelSelectionOverlay channelSelection; @@ -190,6 +191,8 @@ namespace osu.Game.Overlays private double startDragChatHeight; private bool isDragging; + public void OpenChannel(Channel channel) => addChannel(channel); + protected override bool OnDragStart(InputState state) { isDragging = tabsArea.IsHovered; @@ -298,6 +301,8 @@ namespace osu.Game.Overlays ListChannelsRequest req = new ListChannelsRequest(); req.Success += delegate (List channels) { + AvailableChannels = channels; + Scheduler.Add(delegate { addChannel(channels.Find(c => c.Name == @"#lazer")); diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index 924b5d6c9d..d085800f41 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Diagnostics; using OpenTK; using OpenTK.Graphics; using osu.Framework.Allocation; @@ -9,6 +10,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; @@ -16,9 +18,6 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Users; -using System.Diagnostics; -using System.Collections.Generic; -using osu.Framework.Graphics.Cursor; namespace osu.Game.Overlays.Profile { @@ -103,7 +102,7 @@ namespace osu.Game.Overlays.Profile Y = -75, Size = new Vector2(25, 25) }, - new LinkFlowContainer.ProfileLink(user) + new ProfileLink(user) { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, @@ -329,12 +328,14 @@ namespace osu.Game.Overlays.Profile { infoTextLeft.AddText($"{user.Age} years old ", boldItalic); } + if (user.Country != null) { infoTextLeft.AddText("from "); infoTextLeft.AddText(user.Country.FullName, boldItalic); countryFlag.Country = user.Country; } + infoTextLeft.NewParagraph(); if (user.JoinDate.ToUniversalTime().Year < 2008) @@ -346,6 +347,7 @@ namespace osu.Game.Overlays.Profile infoTextLeft.AddText("Joined "); infoTextLeft.AddText(user.JoinDate.LocalDateTime.ToShortDateString(), boldItalic); } + infoTextLeft.NewLine(); infoTextLeft.AddText("Last seen "); infoTextLeft.AddText(user.LastVisit.LocalDateTime.ToShortDateString(), boldItalic); @@ -394,16 +396,17 @@ namespace osu.Game.Overlays.Profile scoreText.Add(createScoreText("Replays Watched by Others")); scoreNumberText.Add(createScoreNumberText(user.Statistics.ReplaysWatched.ToString(@"#,0"))); + gradeSSPlus.DisplayCount = user.Statistics.GradesCount.SSPlus; + gradeSSPlus.Show(); gradeSS.DisplayCount = user.Statistics.GradesCount.SS; gradeSS.Show(); + gradeSPlus.DisplayCount = user.Statistics.GradesCount.SPlus; + gradeSPlus.Show(); gradeS.DisplayCount = user.Statistics.GradesCount.S; gradeS.Show(); gradeA.DisplayCount = user.Statistics.GradesCount.A; gradeA.Show(); - gradeSPlus.DisplayCount = 0; - gradeSSPlus.DisplayCount = 0; - rankGraph.User.Value = user; } } @@ -430,10 +433,38 @@ namespace osu.Game.Overlays.Profile if (string.IsNullOrEmpty(str)) return; infoTextRight.AddIcon(icon); - infoTextRight.AddLink(" " + str, url); + if (url != null) + { + infoTextRight.AddLink(" " + str, url); + } else + { + infoTextRight.AddText(" " + str); + } infoTextRight.NewLine(); } + private class ProfileLink : OsuHoverContainer, IHasTooltip + { + public string TooltipText => "View Profile in Browser"; + + public override bool HandleMouseInput => true; + + public ProfileLink(User user) + { + Action = () => Process.Start($@"https://osu.ppy.sh/users/{user.Id}"); + + AutoSizeAxes = Axes.Both; + + Child = new OsuSpriteText + { + Text = user.Username, + Font = @"Exo2.0-RegularItalic", + TextSize = 30, + }; + } + } + + private class GradeBadge : Container { private const float width = 50; @@ -471,61 +502,5 @@ namespace osu.Game.Overlays.Profile badge.Texture = textures.Get($"Grades/{grade}"); } } - - private class LinkFlowContainer : OsuTextFlowContainer - { - public override bool HandleKeyboardInput => true; - public override bool HandleMouseInput => true; - - public LinkFlowContainer(Action defaultCreationParameters = null) : base(defaultCreationParameters) - { - } - - protected override SpriteText CreateSpriteText() => new LinkText(); - - public void AddLink(string text, string url) => AddText(text, link => ((LinkText)link).Url = url); - - public class LinkText : OsuSpriteText - { - private readonly OsuHoverContainer content; - - public override bool HandleKeyboardInput => content.Action != null; - public override bool HandleMouseInput => content.Action != null; - - protected override Container Content => content ?? (Container)this; - - protected override IEnumerable FlowingChildren => Children; - - public string Url - { - set - { - if (value != null) - content.Action = () => Process.Start(value); - } - } - - public LinkText() - { - AddInternal(content = new OsuHoverContainer - { - AutoSizeAxes = Axes.Both, - }); - } - } - - public class ProfileLink : LinkText, IHasTooltip - { - public string TooltipText => "View Profile in Browser"; - - public ProfileLink(User user) - { - Text = user.Username; - Url = $@"https://osu.ppy.sh/users/{user.Id}"; - Font = @"Exo2.0-RegularItalic"; - TextSize = 30; - } - } - } } } diff --git a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs index 40b9ff069b..92ee01dd7a 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; +using osu.Game.Configuration; namespace osu.Game.Overlays.Settings.Sections.Audio { @@ -12,11 +13,12 @@ namespace osu.Game.Overlays.Settings.Sections.Audio protected override string Header => "Volume"; [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load(AudioManager audio, OsuConfigManager config) { Children = new Drawable[] { new SettingsSlider { LabelText = "Master", Bindable = audio.Volume, KeyboardStep = 0.1f }, + new SettingsSlider { LabelText = "Master (Window Inactive)", Bindable = config.GetBindable(OsuSetting.VolumeInactive), KeyboardStep = 0.1f }, new SettingsSlider { LabelText = "Effect", Bindable = audio.VolumeSample, KeyboardStep = 0.1f }, new SettingsSlider { LabelText = "Music", Bindable = audio.VolumeTrack, KeyboardStep = 0.1f }, }; diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index b58b99f1e3..adb7c509c0 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -7,7 +7,6 @@ using OpenTK.Graphics; using osu.Framework.Configuration; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; @@ -16,6 +15,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using OpenTK; namespace osu.Game.Overlays.Settings { @@ -33,22 +33,10 @@ namespace osu.Game.Overlays.Settings private SpriteText text; - private readonly RestoreDefaultValueButton restoreDefaultValueButton = new RestoreDefaultValueButton(); + private readonly RestoreDefaultValueButton restoreDefaultButton; public bool ShowsDefaultIndicator = true; - private Color4? restoreDefaultValueColour; - - public Color4 RestoreDefaultValueColour - { - get { return restoreDefaultValueColour ?? Color4.White; } - set - { - restoreDefaultValueColour = value; - restoreDefaultValueButton?.SetButtonColour(RestoreDefaultValueColour); - } - } - public virtual string LabelText { get { return text?.Text ?? string.Empty; } @@ -69,10 +57,7 @@ namespace osu.Game.Overlays.Settings public virtual Bindable Bindable { - get - { - return bindable; - } + get { return bindable; } set { @@ -80,8 +65,8 @@ namespace osu.Game.Overlays.Settings controlWithCurrent?.Current.BindTo(bindable); if (ShowsDefaultIndicator) { - restoreDefaultValueButton.Bindable = bindable.GetBoundCopy(); - restoreDefaultValueButton.Bindable.TriggerChange(); + restoreDefaultButton.Bindable = bindable.GetBoundCopy(); + restoreDefaultButton.Bindable.TriggerChange(); } } } @@ -103,38 +88,30 @@ namespace osu.Game.Overlays.Settings AutoSizeAxes = Axes.Y; Padding = new MarginPadding { Right = SettingsOverlay.CONTENT_MARGINS }; - FlowContent = new FillFlowContainer + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Left = SettingsOverlay.CONTENT_MARGINS, Right = 5 }, + restoreDefaultButton = new RestoreDefaultValueButton(), + FlowContent = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Left = SettingsOverlay.CONTENT_MARGINS }, + Child = Control = CreateControl() + }, }; - - if ((Control = CreateControl()) != null) - { - if (controlWithCurrent != null) - controlWithCurrent.Current.DisabledChanged += disabled => { Colour = disabled ? Color4.Gray : Color4.White; }; - FlowContent.Add(Control); - } } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { - AddInternal(FlowContent); - - if (restoreDefaultValueButton != null) - { - if (!restoreDefaultValueColour.HasValue) - restoreDefaultValueColour = colours.Yellow; - restoreDefaultValueButton.SetButtonColour(RestoreDefaultValueColour); - AddInternal(restoreDefaultValueButton); - } + if (controlWithCurrent != null) + controlWithCurrent.Current.DisabledChanged += disabled => { Colour = disabled ? Color4.Gray : Color4.White; }; } - private class RestoreDefaultValueButton : Box, IHasTooltip + private class RestoreDefaultValueButton : Container, IHasTooltip { private Bindable bindable; + public Bindable Bindable { get { return bindable; } @@ -157,6 +134,36 @@ namespace osu.Game.Overlays.Settings Alpha = 0f; } + [BackgroundDependencyLoader] + private void load(OsuColour colour) + { + buttonColour = colour.Yellow; + + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + CornerRadius = 3, + Masking = true, + Colour = buttonColour, + EdgeEffect = new EdgeEffectParameters + { + Colour = buttonColour.Opacity(0.1f), + Type = EdgeEffectType.Glow, + Radius = 2, + }, + Size = new Vector2(0.33f, 0.8f), + Child = new Box { RelativeSizeAxes = Axes.Both }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + UpdateState(); + } + public string TooltipText => "Revert to default"; protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true; @@ -193,9 +200,10 @@ namespace osu.Game.Overlays.Settings { if (bindable == null) return; - var colour = bindable.Disabled ? Color4.Gray : buttonColour; - this.FadeTo(bindable.IsDefault ? 0f : hovering && !bindable.Disabled ? 1f : 0.5f, 200, Easing.OutQuint); - this.FadeColour(ColourInfo.GradientHorizontal(colour.Opacity(0.8f), colour.Opacity(0)), 200, Easing.OutQuint); + + this.FadeTo(bindable.IsDefault ? 0f : + hovering && !bindable.Disabled ? 1f : 0.65f, 200, Easing.OutQuint); + this.FadeColour(bindable.Disabled ? Color4.Gray : buttonColour, 200, Easing.OutQuint); } } } diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 277ed81ce8..c003046242 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays { public abstract class SettingsOverlay : OsuFocusedOverlayContainer { - public const float CONTENT_MARGINS = 10; + public const float CONTENT_MARGINS = 15; public const float TRANSITION_LENGTH = 600; diff --git a/osu.Game/Rulesets/Edit/ToolboxGroup.cs b/osu.Game/Rulesets/Edit/ToolboxGroup.cs index 3b13cc38ab..e153607f47 100644 --- a/osu.Game/Rulesets/Edit/ToolboxGroup.cs +++ b/osu.Game/Rulesets/Edit/ToolboxGroup.cs @@ -2,11 +2,11 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Graphics; -using osu.Game.Screens.Play.ReplaySettings; +using osu.Game.Screens.Play.PlayerSettings; namespace osu.Game.Rulesets.Edit { - public class ToolboxGroup : ReplayGroup + public class ToolboxGroup : PlayerSettingsGroup { protected override string Title => "toolbox"; diff --git a/osu.Game/Rulesets/Objects/CatmullApproximator.cs b/osu.Game/Rulesets/Objects/CatmullApproximator.cs new file mode 100644 index 0000000000..364b4e995a --- /dev/null +++ b/osu.Game/Rulesets/Objects/CatmullApproximator.cs @@ -0,0 +1,70 @@ +// 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 OpenTK; + +namespace osu.Game.Rulesets.Objects +{ + public class CatmullApproximator + { + /// + /// The amount of pieces to calculate for each controlpoint quadruplet. + /// + private const int detail = 50; + + private readonly List controlPoints; + + public CatmullApproximator(List controlPoints) + { + this.controlPoints = controlPoints; + } + + + /// + /// Creates a piecewise-linear approximation of a Catmull-Rom spline. + /// + /// A list of vectors representing the piecewise-linear approximation. + public List CreateCatmull() + { + var result = new List(); + + for (int i = 0; i < controlPoints.Count - 1; i++) + { + var v1 = i > 0 ? controlPoints[i - 1] : controlPoints[i]; + var v2 = controlPoints[i]; + var v3 = i < controlPoints.Count - 1 ? controlPoints[i + 1] : v2 + v2 - v1; + var v4 = i < controlPoints.Count - 2 ? controlPoints[i + 2] : v3 + v3 - v2; + + for (int c = 0; c < detail; c++) + { + result.Add(findPoint(ref v1, ref v2, ref v3, ref v4, (float)c / detail)); + result.Add(findPoint(ref v1, ref v2, ref v3, ref v4, (float)(c + 1) / detail)); + } + } + + return result; + } + + /// + /// Finds a point on the spline at the position of a parameter. + /// + /// The first vector. + /// The second vector. + /// The third vector. + /// The fourth vector. + /// The parameter at which to find the point on the spline, in the range [0, 1]. + /// The point on the spline at . + private Vector2 findPoint(ref Vector2 vec1, ref Vector2 vec2, ref Vector2 vec3, ref Vector2 vec4, float t) + { + float t2 = t * t; + float t3 = t * t2; + + Vector2 result; + result.X = 0.5f * (2f * vec2.X + (-vec1.X + vec3.X) * t + (2f * vec1.X - 5f * vec2.X + 4f * vec3.X - vec4.X) * t2 + (-vec1.X + 3f * vec2.X - 3f * vec3.X + vec4.X) * t3); + result.Y = 0.5f * (2f * vec2.Y + (-vec1.Y + vec3.Y) * t + (2f * vec1.Y - 5f * vec2.Y + 4f * vec3.Y - vec4.Y) * t2 + (-vec1.Y + 3f * vec2.Y - 3f * vec3.Y + vec4.Y) * t3); + + return result; + } + } +} diff --git a/osu.Game/Rulesets/Objects/SliderCurve.cs b/osu.Game/Rulesets/Objects/SliderCurve.cs index ae79d62ec8..2b16f463df 100644 --- a/osu.Game/Rulesets/Objects/SliderCurve.cs +++ b/osu.Game/Rulesets/Objects/SliderCurve.cs @@ -41,6 +41,8 @@ namespace osu.Game.Rulesets.Objects break; return subpath; + case CurveType.Catmull: + return new CatmullApproximator(subControlPoints).CreateCatmull(); } return new BezierApproximator(subControlPoints).CreateBezier(); diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index 913214abfb..c245407bbf 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -102,8 +102,14 @@ namespace osu.Game.Rulesets.Replays if (hasFrames) { - //if we changed frames, we want to execute once *exactly* on the frame's time. - if (currentDirection == time.CompareTo(NextFrame.Time) && advanceFrame()) + // check if the next frame is in the "future" for the current playback direction + if (currentDirection != time.CompareTo(NextFrame.Time)) + { + // if we didn't change frames, we need to ensure we are allowed to run frames in between, else return null. + if (inImportantSection) + return null; + } + else if (advanceFrame()) { // If going backwards, we need to execute once _before_ the frame time to reverse any judgements // that would occur as a result of this frame in forward playback @@ -111,10 +117,6 @@ namespace osu.Game.Rulesets.Replays return currentTime = CurrentFrame.Time - 1; return currentTime = CurrentFrame.Time; } - - //if we didn't change frames, we need to ensure we are allowed to run frames in between, else return null. - if (inImportantSection) - return null; } return currentTime = time; diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index b321123b74..e8271059ca 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -171,10 +171,10 @@ namespace osu.Game.Rulesets.Scoring public readonly Bindable Mode = new Bindable(); - protected sealed override bool HasCompleted => Hits == MaxHits; + protected sealed override bool HasCompleted => JudgedHits == MaxHits; protected int MaxHits { get; private set; } - protected int Hits { get; private set; } + protected int JudgedHits { get; private set; } private double maxHighestCombo; @@ -259,7 +259,7 @@ namespace osu.Game.Rulesets.Scoring baseScore += judgement.NumericResult; rollingMaxBaseScore += judgement.MaxNumericResult; - Hits++; + JudgedHits++; } else if (judgement.IsHit) bonusScore += judgement.NumericResult; @@ -279,7 +279,7 @@ namespace osu.Game.Rulesets.Scoring baseScore -= judgement.NumericResult; rollingMaxBaseScore -= judgement.MaxNumericResult; - Hits--; + JudgedHits--; } else if (judgement.IsHit) bonusScore -= judgement.NumericResult; @@ -305,14 +305,14 @@ namespace osu.Game.Rulesets.Scoring { if (storeResults) { - MaxHits = Hits; + MaxHits = JudgedHits; maxHighestCombo = HighestCombo; maxBaseScore = baseScore; } base.Reset(storeResults); - Hits = 0; + JudgedHits = 0; baseScore = 0; rollingMaxBaseScore = 0; bonusScore = 0; diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 037f9136a8..6a978a3eb3 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -140,6 +140,8 @@ namespace osu.Game.Rulesets.UI if (!base.UpdateSubTree()) return false; + UpdateSubTreeMasking(ScreenSpaceDrawQuad.AABBFloat); + if (isAttached) { // When handling replay input, we need to consider the possibility of fast-forwarding, which may cause the clock to be updated diff --git a/osu.Game/Screens/Play/HUD/ReplaySettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs similarity index 75% rename from osu.Game/Screens/Play/HUD/ReplaySettingsOverlay.cs rename to osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index cb30222831..e6cf1f7982 100644 --- a/osu.Game/Screens/Play/HUD/ReplaySettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -3,29 +3,30 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Screens.Play.ReplaySettings; using OpenTK; using osu.Framework.Input; +using osu.Game.Screens.Play.PlayerSettings; using OpenTK.Input; namespace osu.Game.Screens.Play.HUD { - public class ReplaySettingsOverlay : VisibilityContainer + public class PlayerSettingsOverlay : VisibilityContainer { private const int fade_duration = 200; public bool ReplayLoaded; public readonly PlaybackSettings PlaybackSettings; + public readonly VisualSettings VisualSettings; //public readonly CollectionSettings CollectionSettings; //public readonly DiscussionSettings DiscussionSettings; - public ReplaySettingsOverlay() + public PlayerSettingsOverlay() { AlwaysPresent = true; RelativeSizeAxes = Axes.Both; - Child = new FillFlowContainer + Child = new FillFlowContainer { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, @@ -33,11 +34,12 @@ namespace osu.Game.Screens.Play.HUD Direction = FillDirection.Vertical, Spacing = new Vector2(0, 20), Margin = new MarginPadding { Top = 100, Right = 10 }, - Children = new[] + Children = new PlayerSettingsGroup[] { //CollectionSettings = new CollectionSettings(), //DiscussionSettings = new DiscussionSettings(), PlaybackSettings = new PlaybackSettings(), + VisualSettings = new VisualSettings() } }; @@ -47,6 +49,9 @@ namespace osu.Game.Screens.Play.HUD protected override void PopIn() => this.FadeIn(fade_duration); protected override void PopOut() => this.FadeOut(fade_duration); + //We want to handle keyboard inputs all the time in order to trigger ToggleVisibility() when not visible + public override bool HandleKeyboardInput => true; + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) { if (args.Repeat) return false; diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 5fb867e151..e68a17f014 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Play public readonly HealthDisplay HealthDisplay; public readonly SongProgress Progress; public readonly ModDisplay ModDisplay; - public readonly ReplaySettingsOverlay ReplaySettingsOverlay; + public readonly PlayerSettingsOverlay PlayerSettingsOverlay; private Bindable showHud; private readonly BindableBool replayLoaded = new BindableBool(); @@ -58,7 +58,7 @@ namespace osu.Game.Screens.Play HealthDisplay = CreateHealthDisplay(), Progress = CreateProgress(), ModDisplay = CreateModsContainer(), - ReplaySettingsOverlay = CreateReplaySettingsOverlay(), + PlayerSettingsOverlay = CreatePlayerSettingsOverlay() } }); @@ -72,7 +72,7 @@ namespace osu.Game.Screens.Play ModDisplay.Current.BindTo(working.Mods); - ReplaySettingsOverlay.PlaybackSettings.AdjustableClock = adjustableSourceClock; + PlayerSettingsOverlay.PlaybackSettings.AdjustableClock = adjustableSourceClock; } [BackgroundDependencyLoader(true)] @@ -115,16 +115,16 @@ namespace osu.Game.Screens.Play private void replayLoadedValueChanged(bool loaded) { - ReplaySettingsOverlay.ReplayLoaded = loaded; + PlayerSettingsOverlay.ReplayLoaded = loaded; if (loaded) { - ReplaySettingsOverlay.Show(); + PlayerSettingsOverlay.Show(); ModDisplay.FadeIn(200); } else { - ReplaySettingsOverlay.Hide(); + PlayerSettingsOverlay.Hide(); ModDisplay.Delay(2000).FadeOut(200); } } @@ -213,7 +213,7 @@ namespace osu.Game.Screens.Play Margin = new MarginPadding { Top = 20, Right = 10 }, }; - protected virtual ReplaySettingsOverlay CreateReplaySettingsOverlay() => new ReplaySettingsOverlay(); + protected virtual PlayerSettingsOverlay CreatePlayerSettingsOverlay() => new PlayerSettingsOverlay(); protected virtual void BindProcessor(ScoreProcessor processor) { diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index cf6c252bec..2950990779 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -13,6 +13,7 @@ using osu.Game.Screens.Backgrounds; using OpenTK; using osu.Framework.Localisation; using osu.Game.Screens.Menu; +using osu.Game.Screens.Play.PlayerSettings; namespace osu.Game.Screens.Play { @@ -21,6 +22,7 @@ namespace osu.Game.Screens.Play private Player player; private BeatmapMetadataDisplay info; + private VisualSettings visualSettings; private bool showOverlays = true; public override bool ShowOverlaysOnEnter => showOverlays; @@ -49,6 +51,12 @@ namespace osu.Game.Screens.Play Anchor = Anchor.Centre, Origin = Anchor.Centre, }); + Add(visualSettings = new VisualSettings + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Margin = new MarginPadding(25) + }); LoadComponentAsync(player); } @@ -110,7 +118,7 @@ namespace osu.Game.Screens.Play private void pushWhenLoaded() { - if (player.LoadState != LoadState.Ready) + if (player.LoadState != LoadState.Ready || visualSettings.IsHovered) { Schedule(pushWhenLoaded); return; diff --git a/osu.Game/Screens/Play/ReplaySettings/CollectionSettings.cs b/osu.Game/Screens/Play/PlayerSettings/CollectionSettings.cs similarity index 87% rename from osu.Game/Screens/Play/ReplaySettings/CollectionSettings.cs rename to osu.Game/Screens/Play/PlayerSettings/CollectionSettings.cs index 9f29e085d1..f18b0876a2 100644 --- a/osu.Game/Screens/Play/ReplaySettings/CollectionSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/CollectionSettings.cs @@ -1,15 +1,15 @@ // 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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Overlays.Music; -using System.Collections.Generic; -namespace osu.Game.Screens.Play.ReplaySettings +namespace osu.Game.Screens.Play.PlayerSettings { - public class CollectionSettings : ReplayGroup + public class CollectionSettings : PlayerSettingsGroup { protected override string Title => @"collections"; diff --git a/osu.Game/Screens/Play/ReplaySettings/DiscussionSettings.cs b/osu.Game/Screens/Play/PlayerSettings/DiscussionSettings.cs similarity index 84% rename from osu.Game/Screens/Play/ReplaySettings/DiscussionSettings.cs rename to osu.Game/Screens/Play/PlayerSettings/DiscussionSettings.cs index cc7ccac7f5..e6d4fe3e09 100644 --- a/osu.Game/Screens/Play/ReplaySettings/DiscussionSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/DiscussionSettings.cs @@ -6,9 +6,9 @@ using osu.Framework.Graphics; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; -namespace osu.Game.Screens.Play.ReplaySettings +namespace osu.Game.Screens.Play.PlayerSettings { - public class DiscussionSettings : ReplayGroup + public class DiscussionSettings : PlayerSettingsGroup { protected override string Title => @"discussions"; @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Play.ReplaySettings { Children = new Drawable[] { - new ReplayCheckbox + new PlayerCheckbox { LabelText = "Show floating comments", Bindable = config.GetBindable(OsuSetting.FloatingComments) diff --git a/osu.Game/Screens/Play/ReplaySettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs similarity index 88% rename from osu.Game/Screens/Play/ReplaySettings/PlaybackSettings.cs rename to osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index f8ac653f69..15d8e73a76 100644 --- a/osu.Game/Screens/Play/ReplaySettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -1,15 +1,15 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Timing; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Timing; using osu.Game.Graphics.Sprites; -namespace osu.Game.Screens.Play.ReplaySettings +namespace osu.Game.Screens.Play.PlayerSettings { - public class PlaybackSettings : ReplayGroup + public class PlaybackSettings : PlayerSettingsGroup { private const int padding = 10; @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Play.ReplaySettings public IAdjustableClock AdjustableClock { set; get; } - private readonly ReplaySliderBar sliderbar; + private readonly PlayerSliderBar sliderbar; public PlaybackSettings() { @@ -47,7 +47,7 @@ namespace osu.Game.Screens.Play.ReplaySettings } }, }, - sliderbar = new ReplaySliderBar + sliderbar = new PlayerSliderBar { Bindable = new BindableDouble(1) { diff --git a/osu.Game/Screens/Play/ReplaySettings/ReplayCheckbox.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs similarity index 82% rename from osu.Game/Screens/Play/ReplaySettings/ReplayCheckbox.cs rename to osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs index f0b9ff623a..89fbbb8bf2 100644 --- a/osu.Game/Screens/Play/ReplaySettings/ReplayCheckbox.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs @@ -5,9 +5,9 @@ using osu.Framework.Allocation; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; -namespace osu.Game.Screens.Play.ReplaySettings +namespace osu.Game.Screens.Play.PlayerSettings { - public class ReplayCheckbox : OsuCheckbox + public class PlayerCheckbox : OsuCheckbox { [BackgroundDependencyLoader] private void load(OsuColour colours) diff --git a/osu.Game/Screens/Play/ReplaySettings/ReplayGroup.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs similarity index 94% rename from osu.Game/Screens/Play/ReplaySettings/ReplayGroup.cs rename to osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs index bf22250e12..e8a4bc6b27 100644 --- a/osu.Game/Screens/Play/ReplaySettings/ReplayGroup.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs @@ -1,8 +1,6 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK; -using OpenTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -10,10 +8,12 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using OpenTK; +using OpenTK.Graphics; -namespace osu.Game.Screens.Play.ReplaySettings +namespace osu.Game.Screens.Play.PlayerSettings { - public abstract class ReplayGroup : Container + public abstract class PlayerSettingsGroup : Container { /// /// The title to be displayed in the header of this group. @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Play.ReplaySettings private Color4 buttonActiveColour; - protected ReplayGroup() + protected PlayerSettingsGroup() { AutoSizeAxes = Axes.Y; Width = container_width; diff --git a/osu.Game/Screens/Play/ReplaySettings/ReplaySliderBar.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs similarity index 88% rename from osu.Game/Screens/Play/ReplaySettings/ReplaySliderBar.cs rename to osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs index e755e6bfd9..946669e3dd 100644 --- a/osu.Game/Screens/Play/ReplaySettings/ReplaySliderBar.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs @@ -1,16 +1,16 @@ // 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.Game.Graphics.UserInterface; using System; -using osu.Game.Graphics; +using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; -namespace osu.Game.Screens.Play.ReplaySettings +namespace osu.Game.Screens.Play.PlayerSettings { - public class ReplaySliderBar : SettingsSlider + public class PlayerSliderBar : SettingsSlider where T : struct, IEquatable, IComparable, IConvertible { protected override Drawable CreateControl() => new Sliderbar diff --git a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs new file mode 100644 index 0000000000..1a7b80ec9a --- /dev/null +++ b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs @@ -0,0 +1,52 @@ +// 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.Game.Configuration; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Screens.Play.PlayerSettings +{ + public class VisualSettings : PlayerSettingsGroup + { + protected override string Title => "Visual settings"; + + private readonly PlayerSliderBar dimSliderBar; + private readonly PlayerSliderBar blurSliderBar; + private readonly PlayerCheckbox showStoryboardToggle; + private readonly PlayerCheckbox mouseWheelDisabledToggle; + + public VisualSettings() + { + Children = new Drawable[] + { + new OsuSpriteText + { + Text = "Background dim:" + }, + dimSliderBar = new PlayerSliderBar(), + new OsuSpriteText + { + Text = "Background blur:" + }, + blurSliderBar = new PlayerSliderBar(), + new OsuSpriteText + { + Text = "Toggles:" + }, + showStoryboardToggle = new PlayerCheckbox { LabelText = "Storyboards" }, + mouseWheelDisabledToggle = new PlayerCheckbox { LabelText = "Disable mouse wheel" } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + dimSliderBar.Bindable = config.GetBindable(OsuSetting.DimLevel); + blurSliderBar.Bindable = config.GetBindable(OsuSetting.BlurLevel); + showStoryboardToggle.Bindable = config.GetBindable(OsuSetting.ShowStoryboard); + mouseWheelDisabledToggle.Bindable = config.GetBindable(OsuSetting.MouseDisableWheel); + } + } +} diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index 6f814e43bd..73d20eafb9 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -51,9 +51,15 @@ namespace osu.Game.Users public struct Grades { + [JsonProperty(@"ssh")] + public int SSPlus; + [JsonProperty(@"ss")] public int SS; + [JsonProperty(@"sh")] + public int SPlus; + [JsonProperty(@"s")] public int S; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 05cf61c23c..4944613828 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -275,7 +275,9 @@ + + 20180125143340_Settings.cs @@ -306,6 +308,8 @@ + + @@ -335,6 +339,8 @@ + + @@ -471,7 +477,6 @@ - @@ -766,13 +771,13 @@ - - - - - - - + + + + + + +