diff --git a/osu-framework b/osu-framework index fdea70aee3..e125c03d8c 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit fdea70aee37b040d56fac5e9b27a18ed77f2bfb9 +Subproject commit e125c03d8c39fd86e02e872a8d46654d2ea2759f diff --git a/osu.Desktop.VisualTests/Tests/TestCasePlaySongSelect.cs b/osu.Desktop.VisualTests/Tests/TestCasePlaySongSelect.cs index 5d85bf0a5d..a291f4f65f 100644 --- a/osu.Desktop.VisualTests/Tests/TestCasePlaySongSelect.cs +++ b/osu.Desktop.VisualTests/Tests/TestCasePlaySongSelect.cs @@ -40,7 +40,8 @@ namespace osu.Desktop.VisualTests.Tests protected override void Dispose(bool isDisposing) { - Dependencies.Cache(oldDb, true); + if (oldDb != null) + Dependencies.Cache(oldDb, true); base.Dispose(isDisposing); } diff --git a/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs b/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs index da21b38ec8..89bacb9b97 100644 --- a/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs +++ b/osu.Desktop.VisualTests/Tests/TestCasePlayer.cs @@ -2,6 +2,7 @@ //Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using osu.Framework.Allocation; using osu.Framework.GameModes.Testing; using osu.Framework.MathUtils; using osu.Framework.Timing; @@ -9,6 +10,8 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; using OpenTK; using osu.Framework.Graphics.Sprites; +using osu.Game.Database; +using osu.Game.Modes; using osu.Game.Modes.Objects; using osu.Game.Modes.Osu.Objects; using osu.Game.Screens.Play; @@ -18,10 +21,17 @@ namespace osu.Desktop.VisualTests.Tests { class TestCasePlayer : TestCase { + private WorkingBeatmap beatmap; public override string Name => @"Player"; public override string Description => @"Showing everything to play the game."; + [BackgroundDependencyLoader] + private void load(BeatmapDatabase db) + { + beatmap = db.GetWorkingBeatmap(db.Query().Where(b => b.Mode == PlayMode.Osu).FirstOrDefault()); + } + public override void Reset() { base.Reset(); @@ -29,31 +39,37 @@ namespace osu.Desktop.VisualTests.Tests //ensure we are at offset 0 Clock = new FramedClock(); - var objects = new List(); - - int time = 1500; - for (int i = 0; i < 50; i++) + if (beatmap == null) { - objects.Add(new HitCircle() + + var objects = new List(); + + int time = 1500; + for (int i = 0; i < 50; i++) { - StartTime = time, - Position = new Vector2(i % 4 == 0 || i % 4 == 2 ? 0 : 512, - i % 4 < 2 ? 0 : 384), - NewCombo = i % 4 == 0 - }); + objects.Add(new HitCircle() + { + StartTime = time, + Position = new Vector2(i % 4 == 0 || i % 4 == 2 ? 0 : 512, + i % 4 < 2 ? 0 : 384), + NewCombo = i % 4 == 0 + }); - time += 500; + time += 500; + } + + var decoder = new ConstructableBeatmapDecoder(); + + Beatmap b = new Beatmap + { + HitObjects = objects + }; + + decoder.Process(b); + + beatmap = new WorkingBeatmap(b); } - var decoder = new ConstructableBeatmapDecoder(); - - Beatmap b = new Beatmap - { - HitObjects = objects - }; - - decoder.Process(b); - Add(new Box { RelativeSizeAxes = Framework.Graphics.Axes.Both, @@ -62,7 +78,8 @@ namespace osu.Desktop.VisualTests.Tests Add(new Player { - Beatmap = new WorkingBeatmap(b) + PreferredPlayMode = PlayMode.Osu, + Beatmap = beatmap }); } diff --git a/osu.Game.Mode.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Mode.Osu/Objects/Drawables/DrawableHitCircle.cs index 8bead0fe53..94e9a41c5a 100644 --- a/osu.Game.Mode.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Mode.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -59,15 +59,15 @@ namespace osu.Game.Modes.Osu.Objects.Drawables Colour = osuObject.Colour, } }; + + //may not be so correct + Size = circle.DrawSize; } protected override void LoadComplete() { base.LoadComplete(); - //may not be so correct - Size = circle.DrawSize; - //force application of the state that was set before we loaded. UpdateState(State); } @@ -104,16 +104,9 @@ namespace osu.Game.Modes.Osu.Objects.Drawables Judgement.Result = HitResult.Miss; } - protected override void UpdateState(ArmedState state) + protected override void UpdateInitialState() { - if (!IsLoaded) return; - - Flush(true); //move to DrawableHitObject - ApproachCircle.Flush(true); - - double t = osuObject.EndTime + Judgement.TimeOffset; - - Alpha = 0; + base.UpdateInitialState(); //sane defaults ring.Alpha = circle.Alpha = number.Alpha = glow.Alpha = 1; @@ -121,29 +114,30 @@ namespace osu.Game.Modes.Osu.Objects.Drawables ApproachCircle.Scale = new Vector2(2); explode.Alpha = 0; Scale = new Vector2(0.5f); //this will probably need to be moved to DrawableHitObject at some point. + } - const float preempt = 600; + protected override void UpdatePreemptState() + { + base.UpdatePreemptState(); - const float fadein = 400; + ApproachCircle.FadeIn(Math.Min(TIME_FADEIN * 2, TIME_PREEMPT)); + ApproachCircle.ScaleTo(0.6f, TIME_PREEMPT); + } - Delay(t - Time.Current - preempt, true); + protected override void UpdateState(ArmedState state) + { + if (!IsLoaded) return; - FadeIn(fadein); - - ApproachCircle.FadeIn(Math.Min(fadein * 2, preempt)); - ApproachCircle.ScaleTo(0.6f, preempt); - - Delay(preempt, true); + base.UpdateState(state); ApproachCircle.FadeOut(); - glow.FadeOut(400); switch (state) { case ArmedState.Idle: - Delay(osuObject.Duration + 500); - FadeOut(500); + Delay(osuObject.Duration + TIME_PREEMPT); + FadeOut(TIME_FADEOUT); explosion?.Expire(); explosion = null; diff --git a/osu.Game.Mode.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Mode.Osu/Objects/Drawables/DrawableOsuHitObject.cs index d416732327..0e3a03665c 100644 --- a/osu.Game.Mode.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Mode.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -11,6 +11,10 @@ namespace osu.Game.Modes.Osu.Objects.Drawables { public class DrawableOsuHitObject : DrawableHitObject { + protected const float TIME_PREEMPT = 600; + protected const float TIME_FADEIN = 400; + protected const float TIME_FADEOUT = 500; + public DrawableOsuHitObject(OsuHitObject hitObject) : base(hitObject) { @@ -20,7 +24,27 @@ namespace osu.Game.Modes.Osu.Objects.Drawables protected override void UpdateState(ArmedState state) { - throw new NotImplementedException(); + if (!IsLoaded) return; + + Flush(true); + + UpdateInitialState(); + + Delay(HitObject.StartTime - Time.Current - TIME_PREEMPT + Judgement.TimeOffset, true); + + UpdatePreemptState(); + + Delay(TIME_PREEMPT, true); + } + + protected virtual void UpdatePreemptState() + { + FadeIn(TIME_FADEIN); + } + + protected virtual void UpdateInitialState() + { + Alpha = 0; } } diff --git a/osu.Game.Mode.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Mode.Osu/Objects/Drawables/DrawableSlider.cs index f779f9948f..1d55d704c4 100644 --- a/osu.Game.Mode.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Mode.Osu/Objects/Drawables/DrawableSlider.cs @@ -1,26 +1,52 @@ -using osu.Framework.Graphics; +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Modes.Objects.Drawables; using osu.Game.Modes.Osu.Objects.Drawables.Pieces; using OpenTK; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Transformations; +using OpenTK.Graphics; +using osu.Framework.Input; +using OpenTK.Graphics.ES30; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Textures; namespace osu.Game.Modes.Osu.Objects.Drawables { class DrawableSlider : DrawableOsuHitObject { - public DrawableSlider(Slider h) : base(h) + private Slider slider; + + private DrawableHitCircle startCircle; + private Container ball; + private Body body; + + public DrawableSlider(Slider s) : base(s) { - Origin = Anchor.Centre; - Position = new Vector2(h.Position.X, h.Position.Y); + slider = s; - Path sliderPath; - Add(sliderPath = new Path()); + Origin = Anchor.TopLeft; + Position = Vector2.Zero; + RelativeSizeAxes = Axes.Both; - for (int i = 0; i < h.Curve.Path.Count; ++i) - sliderPath.Positions.Add(h.Curve.Path[i] - h.Position); - - h.Position = Vector2.Zero; - Add(new DrawableHitCircle(h)); + Children = new Drawable[] + { + body = new Body(s) + { + Position = s.Position, + }, + ball = new Ball(), + startCircle = new DrawableHitCircle(new HitCircle + { + StartTime = s.StartTime, + Position = s.Position, + Colour = s.Colour, + }) + { + Depth = 1 //override time-based depth. + }, + }; } protected override void LoadComplete() @@ -31,19 +57,222 @@ namespace osu.Game.Modes.Osu.Objects.Drawables UpdateState(State); } + protected override void Update() + { + base.Update(); + + ball.Alpha = Time.Current >= slider.StartTime && Time.Current <= slider.EndTime ? 1 : 0; + + double t = (Time.Current - slider.StartTime) / slider.Duration; + if (slider.RepeatCount > 1) + { + int currentRepeat = (int)(t * slider.RepeatCount); + t = (t * slider.RepeatCount) % 1; + if (currentRepeat % 2 == 1) + t = 1 - t; + } + + ball.Position = slider.Curve.PositionAt(t); + } + protected override void UpdateState(ArmedState state) { - if (!IsLoaded) return; + base.UpdateState(state); - Flush(true); //move to DrawableHitObject + Delay(HitObject.Duration); + FadeOut(100); + } - Alpha = 0; + private class Ball : Container + { + private Box follow; - Delay(HitObject.StartTime - 450 - Time.Current, true); + public Ball() + { + Masking = true; + AutoSizeAxes = Axes.Both; + BlendingMode = BlendingMode.Additive; + Origin = Anchor.Centre; - FadeIn(200); - Delay(450 + HitObject.Duration); - FadeOut(200); + Children = new Drawable[] + { + follow = new Box + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Colour = Color4.Orange, + Width = 64, + Height = 64, + }, + new Container + { + Masking = true, + AutoSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Colour = Color4.Cyan, + CornerRadius = 32, + Children = new[] + { + new Box + { + + Width = 64, + Height = 64, + }, + } + } + + }; + } + + private InputState lastState; + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + lastState = state; + return base.OnMouseDown(state, args); + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + lastState = state; + return base.OnMouseUp(state, args); + } + + protected override bool OnMouseMove(InputState state) + { + lastState = state; + return base.OnMouseMove(state); + } + + bool tracking; + protected bool Tracking + { + get { return tracking; } + set + { + if (value == tracking) return; + + tracking = value; + + follow.ScaleTo(tracking ? 2.4f : 1, 140, EasingTypes.Out); + follow.FadeTo(tracking ? 0.8f : 0, 140, EasingTypes.Out); + } + } + + protected override void Update() + { + base.Update(); + + CornerRadius = DrawWidth / 2; + Tracking = lastState != null && Contains(lastState.Mouse.NativeState.Position) && lastState.Mouse.HasMainButtonPressed; + } + } + + private class Body : Container + { + private Path path; + private BufferedContainer container; + + private double? drawnProgress; + + private Slider slider; + public Body(Slider s) + { + slider = s; + + Children = new Drawable[] + { + container = new BufferedContainer + { + CacheDrawnFrameBuffer = true, + Children = new Drawable[] + { + path = new Path + { + Colour = s.Colour, + BlendingMode = BlendingMode.None, + }, + } + } + }; + + container.Attach(RenderbufferInternalFormat.DepthComponent16); + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + // Surprisingly, this looks somewhat okay and works well as a test for self-overlaps. + // TODO: Don't do this. + path.Texture = textures.Get(@"Menu/logo"); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + path.PathWidth = 32; + } + + protected override void Update() + { + base.Update(); + + if (updateSnaking()) + { + // 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.HeadPosition; + + container.ForceRedraw(); + } + } + + private bool updateSnaking() + { + double progress = MathHelper.Clamp((Time.Current - slider.StartTime + TIME_PREEMPT) / TIME_FADEIN, 0, 1); + + if (progress == drawnProgress) return false; + + bool madeChanges = false; + if (progress == 0) + { + //if we have gone backwards, just clear the path for now. + drawnProgress = 0; + path.ClearVertices(); + madeChanges = true; + } + + Vector2 startPosition = slider.Curve.PositionAt(0); + + if (drawnProgress == null) + { + drawnProgress = 0; + path.AddVertex(slider.Curve.PositionAt(drawnProgress.Value) - startPosition); + madeChanges = true; + } + + double segmentSize = 1 / (slider.Curve.Length / 5); + + while (drawnProgress + segmentSize < progress) + { + drawnProgress += segmentSize; + path.AddVertex(slider.Curve.PositionAt(drawnProgress.Value) - startPosition); + madeChanges = true; + } + + if (progress == 1 && drawnProgress != progress) + { + drawnProgress = progress; + path.AddVertex(slider.Curve.PositionAt(drawnProgress.Value) - startPosition); + madeChanges = true; + } + + return madeChanges; + } } } } diff --git a/osu.Game.Mode.Osu/Objects/Slider.cs b/osu.Game.Mode.Osu/Objects/Slider.cs index b52a7c7623..7ee131659a 100644 --- a/osu.Game.Mode.Osu/Objects/Slider.cs +++ b/osu.Game.Mode.Osu/Objects/Slider.cs @@ -2,18 +2,27 @@ //Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using osu.Game.Database; using OpenTK; +using osu.Game.Beatmaps; +using System; namespace osu.Game.Modes.Osu.Objects { public class Slider : OsuHitObject { - public override double EndTime => StartTime + (RepeatCount + 1) * Curve.Length; + public override double EndTime => StartTime + RepeatCount * Curve.Length / Velocity; + + public double Velocity; + + public override void SetDefaultsFromBeatmap(Beatmap beatmap) + { + Velocity = 100 / beatmap.BeatLengthAt(StartTime, true) * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier; + } public int RepeatCount; public SliderCurve Curve; - } public class SliderCurve @@ -42,11 +51,14 @@ namespace osu.Game.Modes.Osu.Objects public Vector2 PositionAt(double progress) { - int index = (int)(progress * (calculatedPath.Count - 1)); + progress = MathHelper.Clamp(progress, 0, 1); - Vector2 pos = calculatedPath[index]; - if (index != progress) - pos += (calculatedPath[index + 1] - pos) * (float)(progress - index); + double index = progress * (calculatedPath.Count - 1); + int flooredIndex = (int)index; + + Vector2 pos = calculatedPath[flooredIndex]; + if (index != flooredIndex) + pos += (calculatedPath[flooredIndex + 1] - pos) * (float)(index - flooredIndex); return pos; } @@ -201,5 +213,5 @@ namespace osu.Game.Modes.Osu.Objects Bezier, Linear, PerfectCurve - }; + } } diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 4d6c6a8056..3f8ae0e33a 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -16,5 +16,27 @@ namespace osu.Game.Beatmaps public List HitObjects { get; set; } public List ControlPoints { get; set; } public List ComboColors { get; set; } + + public double BeatLengthAt(double time, bool applyMultipliers = false) + { + int point = 0; + int samplePoint = 0; + + for (int i = 0; i < ControlPoints.Count; i++) + if (ControlPoints[i].Time <= time) + { + if (ControlPoints[i].TimingChange) + point = i; + else + samplePoint = i; + } + + double mult = 1; + + if (applyMultipliers && samplePoint > point && ControlPoints[samplePoint].BeatLength < 0) + mult = ControlPoints[samplePoint].VelocityAdjustment; + + return ControlPoints[point].BeatLength * mult; + } } } diff --git a/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs b/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs index 5468e616f0..b1eaeb467d 100644 --- a/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs @@ -187,7 +187,25 @@ namespace osu.Game.Beatmaps.Formats private void handleTimingPoints(Beatmap beatmap, string val) { - // TODO + ControlPoint cp = null; + + string[] split = val.Split(','); + + if (split.Length > 2) + { + int kiai_flags = split.Length > 7 ? Convert.ToInt32(split[7], NumberFormatInfo.InvariantInfo) : 0; + double beatLength = double.Parse(split[1].Trim(), NumberFormatInfo.InvariantInfo); + cp = new ControlPoint + { + Time = double.Parse(split[0].Trim(), NumberFormatInfo.InvariantInfo), + BeatLength = beatLength > 0 ? beatLength : 0, + VelocityAdjustment = beatLength < 0 ? -beatLength / 100.0 : 1, + TimingChange = split.Length <= 6 || split[6][0] == '1', + }; + } + + if (cp != null) + beatmap.ControlPoints.Add(cp); } private void handleColours(Beatmap beatmap, string key, string val) @@ -275,8 +293,12 @@ namespace osu.Game.Beatmaps.Formats break; case Section.HitObjects: var obj = parser?.Parse(val); + if (obj != null) + { + obj.SetDefaultsFromBeatmap(beatmap); beatmap.HitObjects.Add(obj); + } break; } } diff --git a/osu.Game/Beatmaps/Timing/ControlPoint.cs b/osu.Game/Beatmaps/Timing/ControlPoint.cs index 89eac572ec..6e83760c8d 100644 --- a/osu.Game/Beatmaps/Timing/ControlPoint.cs +++ b/osu.Game/Beatmaps/Timing/ControlPoint.cs @@ -12,5 +12,14 @@ namespace osu.Game.Beatmaps.Timing public class ControlPoint { public double Time; + public double BeatLength; + public double VelocityAdjustment; + public bool TimingChange; + } + + internal enum TimeSignatures + { + SimpleQuadruple = 4, + SimpleTriple = 3 } } diff --git a/osu.Game/Modes/Objects/HitObject.cs b/osu.Game/Modes/Objects/HitObject.cs index abb88726b6..71839e7036 100644 --- a/osu.Game/Modes/Objects/HitObject.cs +++ b/osu.Game/Modes/Objects/HitObject.cs @@ -1,6 +1,7 @@ //Copyright (c) 2007-2016 ppy Pty Ltd . //Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Beatmaps; using osu.Game.Beatmaps.Samples; using OpenTK.Graphics; @@ -21,5 +22,7 @@ namespace osu.Game.Modes.Objects public double Duration => EndTime - StartTime; public HitSampleInfo Sample; + + public virtual void SetDefaultsFromBeatmap(Beatmap beatmap) { } } }