diff --git a/appveyor.yml b/appveyor.yml index 9048428590..9cf68803a2 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,17 +9,17 @@ cache: - inspectcode -> appveyor.yml - packages -> **\packages.config install: - - cmd: git submodule update --init --recursive + - cmd: git submodule update --init --recursive --depth=5 - cmd: choco install resharper-clt -y - cmd: choco install nvika -y - cmd: appveyor DownloadFile https://github.com/peppy/CodeFileSanity/releases/download/v0.2.3/CodeFileSanity.exe before_build: - cmd: CodeFileSanity.exe - - cmd: nuget restore + - cmd: nuget restore -verbosity quiet build: project: osu.sln parallel: true verbosity: minimal after_build: - - cmd: inspectcode /o="inspectcodereport.xml" /caches-home="inspectcode" osu.sln + - cmd: inspectcode --o="inspectcodereport.xml" --projects:osu.Game* --caches-home="inspectcode" osu.sln > NUL - cmd: NVika parsereport "inspectcodereport.xml" --treatwarningsaserrors \ No newline at end of file diff --git a/osu-framework b/osu-framework index d92cec7645..797a351db2 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit d92cec764538da2e7ed95bfb566f6bc81a9667c8 +Subproject commit 797a351db2e852fef5296453641ffbf6b2f6dc11 diff --git a/osu.Desktop.Deploy/osu.Desktop.Deploy.csproj b/osu.Desktop.Deploy/osu.Desktop.Deploy.csproj index 6727a86a91..3bec56d322 100644 --- a/osu.Desktop.Deploy/osu.Desktop.Deploy.csproj +++ b/osu.Desktop.Deploy/osu.Desktop.Deploy.csproj @@ -1,6 +1,6 @@  - + Debug AnyCPU @@ -22,7 +22,6 @@ DEBUG;TRACE prompt 4 - 6 AnyCPU @@ -102,9 +101,6 @@ - - osu.licenseheader - PreserveNewest diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 91c0da6f65..e4e9807754 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -1,5 +1,6 @@  - + + {419659FD-72EA-4678-9EB8-B22A746CED70} Debug @@ -62,7 +63,6 @@ false - 6 none @@ -98,7 +98,6 @@ full AnyCPU false - 6 prompt --tests @@ -174,9 +173,6 @@ - - osu.licenseheader - diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index 7126b6586d..6b9ec8b9a4 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -11,11 +11,11 @@ using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Catch.Beatmaps { - internal class CatchBeatmapConverter : BeatmapConverter + internal class CatchBeatmapConverter : BeatmapConverter { protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(IHasXPosition) }; - protected override IEnumerable ConvertHitObject(HitObject obj, Beatmap beatmap) + protected override IEnumerable ConvertHitObject(HitObject obj, Beatmap beatmap) { var curveData = obj as IHasCurve; var positionData = obj as IHasXPosition; diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index 7fac19d135..9901dbde18 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -1,14 +1,20 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; +using System.Collections.Generic; +using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Objects.Types; +using OpenTK; namespace osu.Game.Rulesets.Catch.Beatmaps { - internal class CatchBeatmapProcessor : BeatmapProcessor + internal class CatchBeatmapProcessor : BeatmapProcessor { - public override void PostProcess(Beatmap beatmap) + public override void PostProcess(Beatmap beatmap) { if (beatmap.ComboColors.Count == 0) return; @@ -16,7 +22,9 @@ namespace osu.Game.Rulesets.Catch.Beatmaps int comboIndex = 0; int colourIndex = 0; - CatchBaseHit lastObj = null; + CatchHitObject lastObj = null; + + initialiseHyperDash(beatmap.HitObjects); foreach (var obj in beatmap.HitObjects) { @@ -34,5 +42,49 @@ namespace osu.Game.Rulesets.Catch.Beatmaps lastObj = obj; } } + + private void initialiseHyperDash(List objects) + { + // todo: add difficulty adjust. + double halfCatcherWidth = CatcherArea.CATCHER_SIZE * (objects.FirstOrDefault()?.Scale ?? 1) / CatchPlayfield.BASE_WIDTH / 2; + + int lastDirection = 0; + double lastExcess = halfCatcherWidth; + + int objCount = objects.Count; + + for (int i = 0; i < objCount - 1; i++) + { + CatchHitObject currentObject = objects[i]; + + // not needed? + // if (currentObject is TinyDroplet) continue; + + CatchHitObject nextObject = objects[i + 1]; + + // while (nextObject is TinyDroplet) + // { + // if (++i == objCount - 1) break; + // nextObject = objects[i + 1]; + // } + + int thisDirection = nextObject.X > currentObject.X ? 1 : -1; + double timeToNext = nextObject.StartTime - ((currentObject as IHasEndTime)?.EndTime ?? currentObject.StartTime) - 4; + double distanceToNext = Math.Abs(nextObject.X - currentObject.X) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth); + + if (timeToNext * CatcherArea.Catcher.BASE_SPEED < distanceToNext) + { + currentObject.HyperDashTarget = nextObject; + lastExcess = halfCatcherWidth; + } + else + { + //currentObject.DistanceToHyperDash = timeToNext - distanceToNext; + lastExcess = MathHelper.Clamp(timeToNext - distanceToNext, 0, halfCatcherWidth); + } + + lastDirection = thisDirection; + } + } } } diff --git a/osu.Game.Rulesets.Catch/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/CatchDifficultyCalculator.cs index b77be9d1f0..e9524a867d 100644 --- a/osu.Game.Rulesets.Catch/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/CatchDifficultyCalculator.cs @@ -8,14 +8,14 @@ using System.Collections.Generic; namespace osu.Game.Rulesets.Catch { - public class CatchDifficultyCalculator : DifficultyCalculator + public class CatchDifficultyCalculator : DifficultyCalculator { public CatchDifficultyCalculator(Beatmap beatmap) : base(beatmap) { } - public override double Calculate(Dictionary categoryDifficulty = null) => 0; + public override double Calculate(Dictionary categoryDifficulty = null) => 0; - protected override BeatmapConverter CreateBeatmapConverter(Beatmap beatmap) => new CatchBeatmapConverter(); + protected override BeatmapConverter CreateBeatmapConverter(Beatmap beatmap) => new CatchBeatmapConverter(); } } diff --git a/osu.Game.Rulesets.Catch/Objects/CatchBaseHit.cs b/osu.Game.Rulesets.Catch/Objects/CatchBaseHit.cs deleted file mode 100644 index 2f33cf1093..0000000000 --- a/osu.Game.Rulesets.Catch/Objects/CatchBaseHit.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Types; -using OpenTK.Graphics; - -namespace osu.Game.Rulesets.Catch.Objects -{ - public abstract class CatchBaseHit : HitObject, IHasXPosition, IHasCombo - { - public float X { get; set; } - - public Color4 ComboColour { get; set; } = Color4.Gray; - public int ComboIndex { get; set; } - - public virtual bool NewCombo { get; set; } - - /// - /// The next fruit starts a new combo. Used for explodey. - /// - public virtual bool LastInCombo { get; set; } - } -} diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs new file mode 100644 index 0000000000..38757d4928 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -0,0 +1,47 @@ +// Copyright (c) 2007-2017 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; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using OpenTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Objects +{ + public abstract class CatchHitObject : HitObject, IHasXPosition, IHasCombo + { + public const double OBJECT_RADIUS = 44; + + public float X { get; set; } + + public Color4 ComboColour { get; set; } = Color4.Gray; + public int ComboIndex { get; set; } + + public virtual bool NewCombo { get; set; } + + /// + /// The next fruit starts a new combo. Used for explodey. + /// + public virtual bool LastInCombo { get; set; } + + public float Scale { get; set; } = 1; + + /// + /// Whether this fruit can initiate a hyperdash. + /// + public bool HyperDash => HyperDashTarget != null; + + /// + /// The target fruit if we are to initiate a hyperdash. + /// + public CatchHitObject HyperDashTarget; + + public override void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + { + base.ApplyDefaults(controlPointInfo, difficulty); + + Scale = 1.0f - 0.7f * (difficulty.CircleSize - 5) / 5; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs index e057bf3d8e..b90a06b94e 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs @@ -5,11 +5,12 @@ using System; using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; +using OpenTK; namespace osu.Game.Rulesets.Catch.Objects.Drawable { public abstract class DrawableCatchHitObject : DrawableCatchHitObject - where TObject : CatchBaseHit + where TObject : CatchHitObject { public new TObject HitObject; @@ -17,12 +18,14 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable : base(hitObject) { HitObject = hitObject; + + Scale = new Vector2(HitObject.Scale); } } - public abstract class DrawableCatchHitObject : DrawableScrollingHitObject + public abstract class DrawableCatchHitObject : DrawableScrollingHitObject { - protected DrawableCatchHitObject(CatchBaseHit hitObject) + protected DrawableCatchHitObject(CatchHitObject hitObject) : base(hitObject) { RelativePositionAxes = Axes.Both; @@ -30,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable Y = (float)HitObject.StartTime; } - public Func CheckPosition; + public Func CheckPosition; protected override void CheckForJudgements(bool userTriggered, double timeOffset) { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs index 4c28a9d021..9f46bbd3a4 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.MathUtils; using osu.Game.Rulesets.Catch.Objects.Drawable.Pieces; using OpenTK; +using OpenTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawable { @@ -70,6 +71,20 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable } } }; + + if (HitObject.HyperDash) + { + Add(new Pulp + { + RelativePositionAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AccentColour = Color4.Red, + Blending = BlendingMode.Additive, + Alpha = 0.5f, + Scale = new Vector2(2) + }); + } } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs index afda91d0b4..bfb674d1b4 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable RelativeChildSize = new Vector2(1, (float)HitObject.Duration) }; - foreach (CatchBaseHit tick in s.Ticks) + foreach (CatchHitObject tick in s.Ticks) { TinyDroplet tiny = tick as TinyDroplet; if (tiny != null) @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable } } - protected override void AddNested(DrawableHitObject h) + protected override void AddNested(DrawableHitObject h) { ((DrawableCatchHitObject)h).CheckPosition = o => CheckPosition?.Invoke(o) ?? false; dropletContainer.Add(h); diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs index 00ddd365e3..2de266b3f0 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/Pieces/Pulp.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable.Pieces { public class Pulp : Circle, IHasAccentColour { - public const float PULP_SIZE = 20; + public const float PULP_SIZE = (float)CatchHitObject.OBJECT_RADIUS / 2.2f; public Pulp() { diff --git a/osu.Game.Rulesets.Catch/Objects/Droplet.cs b/osu.Game.Rulesets.Catch/Objects/Droplet.cs index b1206e0d75..a2bdf830e5 100644 --- a/osu.Game.Rulesets.Catch/Objects/Droplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Droplet.cs @@ -3,7 +3,7 @@ namespace osu.Game.Rulesets.Catch.Objects { - public class Droplet : CatchBaseHit + public class Droplet : CatchHitObject { } } diff --git a/osu.Game.Rulesets.Catch/Objects/Fruit.cs b/osu.Game.Rulesets.Catch/Objects/Fruit.cs index fc55f83969..5f1060fb51 100644 --- a/osu.Game.Rulesets.Catch/Objects/Fruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Fruit.cs @@ -3,7 +3,7 @@ namespace osu.Game.Rulesets.Catch.Objects { - public class Fruit : CatchBaseHit + public class Fruit : CatchHitObject { } } diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 6462f6f6a8..bf9f0bd44b 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -15,7 +15,7 @@ using osu.Framework.Lists; namespace osu.Game.Rulesets.Catch.Objects { - public class JuiceStream : CatchBaseHit, IHasCurve + public class JuiceStream : CatchHitObject, IHasCurve { /// /// Positional distance that results in a duration of one second, before any speed adjustments. @@ -42,11 +42,11 @@ namespace osu.Game.Rulesets.Catch.Objects TickDistance = scoringDistance / difficulty.SliderTickRate; } - public IEnumerable Ticks + public IEnumerable Ticks { get { - SortedList ticks = new SortedList((a, b) => a.StartTime.CompareTo(b.StartTime)); + SortedList ticks = new SortedList((a, b) => a.StartTime.CompareTo(b.StartTime)); if (TickDistance == 0) return ticks; diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index 66a5636b74..0806c4b29d 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -10,14 +10,14 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Catch.Scoring { - internal class CatchScoreProcessor : ScoreProcessor + internal class CatchScoreProcessor : ScoreProcessor { - public CatchScoreProcessor(RulesetContainer rulesetContainer) + public CatchScoreProcessor(RulesetContainer rulesetContainer) : base(rulesetContainer) { } - protected override void SimulateAutoplay(Beatmap beatmap) + protected override void SimulateAutoplay(Beatmap beatmap) { foreach (var obj in beatmap.HitObjects) { diff --git a/osu.Game.Rulesets.Catch/Tests/TestCaseCatchStacker.cs b/osu.Game.Rulesets.Catch/Tests/TestCaseCatchStacker.cs index a890a8a386..586de17f15 100644 --- a/osu.Game.Rulesets.Catch/Tests/TestCaseCatchStacker.cs +++ b/osu.Game.Rulesets.Catch/Tests/TestCaseCatchStacker.cs @@ -11,16 +11,26 @@ namespace osu.Game.Rulesets.Catch.Tests [Ignore("getting CI working")] public class TestCaseCatchStacker : Game.Tests.Visual.TestCasePlayer { - public TestCaseCatchStacker() : base(typeof(CatchRuleset)) + public TestCaseCatchStacker() + : base(typeof(CatchRuleset)) { } protected override Beatmap CreateBeatmap() { - var beatmap = new Beatmap(); + var beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + BaseDifficulty = new BeatmapDifficulty + { + CircleSize = 6, + } + } + }; - for (int i = 0; i < 256; i++) - beatmap.HitObjects.Add(new Fruit { X = 0.5f, StartTime = i * 100, NewCombo = i % 8 == 0 }); + for (int i = 0; i < 512; i++) + beatmap.HitObjects.Add(new Fruit { X = 0.5f + i / 2048f * (i % 10 - 5), StartTime = i * 100, NewCombo = i % 8 == 0 }); return beatmap; } diff --git a/osu.Game.Rulesets.Catch/Tests/TestCaseCatcher.cs b/osu.Game.Rulesets.Catch/Tests/TestCaseCatcher.cs deleted file mode 100644 index 341612b760..0000000000 --- a/osu.Game.Rulesets.Catch/Tests/TestCaseCatcher.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.UI; -using osu.Game.Tests.Visual; -using OpenTK; - -namespace osu.Game.Rulesets.Catch.Tests -{ - [TestFixture] - [Ignore("getting CI working")] - internal class TestCaseCatcher : OsuTestCase - { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(Catcher), - }; - - [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) - { - Children = new Drawable[] - { - new CatchInputManager(rulesets.GetRuleset(2)) - { - RelativeSizeAxes = Axes.Both, - Child = new Catcher - { - RelativePositionAxes = Axes.Both, - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Size = new Vector2(1, 0.2f), - } - }, - }; - } - } -} diff --git a/osu.Game.Rulesets.Catch/Tests/TestCaseCatcherArea.cs b/osu.Game.Rulesets.Catch/Tests/TestCaseCatcherArea.cs new file mode 100644 index 0000000000..daa3e12800 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Tests/TestCaseCatcherArea.cs @@ -0,0 +1,62 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Catch.Tests +{ + [TestFixture] + [Ignore("getting CI working")] + internal class TestCaseCatcherArea : OsuTestCase + { + private RulesetInfo catchRuleset; + private TestCatcherArea catcherArea; + + public override IReadOnlyList RequiredTypes => new[] + { + typeof(CatcherArea), + }; + + public TestCaseCatcherArea() + { + AddSliderStep("CircleSize", 0, 8, 5, createCatcher); + AddToggleStep("Hyperdash", t => catcherArea.ToggleHyperDash(t)); + } + + private void createCatcher(float size) + { + Child = new CatchInputManager(catchRuleset) + { + RelativeSizeAxes = Axes.Both, + Child = catcherArea = new TestCatcherArea(new BeatmapDifficulty { CircleSize = size }) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.BottomLeft + }, + }; + } + + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + catchRuleset = rulesets.GetRuleset(2); + } + + private class TestCatcherArea : CatcherArea + { + public TestCatcherArea(BeatmapDifficulty beatmapDifficulty) + : base(beatmapDifficulty) + { + } + + public void ToggleHyperDash(bool status) => MovableCatcher.HyperDashModifier = status ? 2 : 1; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Tests/TestCaseHyperdash.cs b/osu.Game.Rulesets.Catch/Tests/TestCaseHyperdash.cs new file mode 100644 index 0000000000..ce3f79bae2 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Tests/TestCaseHyperdash.cs @@ -0,0 +1,30 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Objects; + +namespace osu.Game.Rulesets.Catch.Tests +{ + [TestFixture] + [Ignore("getting CI working")] + public class TestCaseHyperdash : Game.Tests.Visual.TestCasePlayer + { + public TestCaseHyperdash() + : base(typeof(CatchRuleset)) + { + } + + protected override Beatmap CreateBeatmap() + { + var beatmap = new Beatmap(); + + for (int i = 0; i < 512; i++) + if (i % 5 < 3) + beatmap.HitObjects.Add(new Fruit { X = i % 10 < 5 ? 0.02f : 0.98f, StartTime = i * 100, NewCombo = i % 8 == 0 }); + + return beatmap; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Tests/TestCasePerformancePoints.cs b/osu.Game.Rulesets.Catch/Tests/TestCasePerformancePoints.cs new file mode 100644 index 0000000000..0d2dc14160 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Tests/TestCasePerformancePoints.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; + +namespace osu.Game.Rulesets.Catch.Tests +{ + [Ignore("getting CI working")] + public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints + { + public TestCasePerformancePoints() + : base(new CatchRuleset(new RulesetInfo())) + { + } + } +} diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 987eef5e45..76dbfa77c6 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -1,11 +1,11 @@ // Copyright (c) 2007-2017 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.UI; using OpenTK; using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawable; using osu.Game.Rulesets.Judgements; @@ -15,15 +15,14 @@ namespace osu.Game.Rulesets.Catch.UI { public class CatchPlayfield : ScrollingPlayfield { - public static readonly float BASE_WIDTH = 512; + public const float BASE_WIDTH = 512; protected override Container Content => content; private readonly Container content; - private readonly Container catcherContainer; - private readonly Catcher catcher; + private readonly CatcherArea catcherArea; - public CatchPlayfield() + public CatchPlayfield(BeatmapDifficulty difficulty) : base(Axes.Y) { Container explodingFruitContainer; @@ -43,30 +42,16 @@ namespace osu.Game.Rulesets.Catch.UI { RelativeSizeAxes = Axes.Both, }, - catcherContainer = new Container + catcherArea = new CatcherArea(difficulty) { - RelativeSizeAxes = Axes.X, + ExplodingFruitTarget = explodingFruitContainer, Anchor = Anchor.BottomLeft, Origin = Anchor.TopLeft, - Height = 180, - Child = catcher = new Catcher - { - ExplodingFruitTarget = explodingFruitContainer, - RelativePositionAxes = Axes.Both, - Origin = Anchor.TopCentre, - X = 0.5f, - } } }; } - protected override void Update() - { - base.Update(); - catcher.Size = new Vector2(catcherContainer.DrawSize.Y); - } - - public bool CheckIfWeCanCatch(CatchBaseHit obj) => Math.Abs(catcher.Position.X - obj.X) < catcher.DrawSize.X / DrawSize.X / 2; + public bool CheckIfWeCanCatch(CatchHitObject obj) => catcherArea.AttemptCatch(obj); public override void Add(DrawableHitObject h) { @@ -88,7 +73,7 @@ namespace osu.Game.Rulesets.Catch.UI (judgedObject.Parent as Container)?.Remove(judgedObject); (judgedObject.Parent as Container)?.Remove(judgedObject); - catcher.Add(judgedObject, screenPosition); + catcherArea.Add(judgedObject, screenPosition); } } } diff --git a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs index 92912eb177..3ed9090098 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs @@ -13,7 +13,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Catch.UI { - public class CatchRulesetContainer : ScrollingRulesetContainer + public class CatchRulesetContainer : ScrollingRulesetContainer { public CatchRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset) : base(ruleset, beatmap, isForCurrentRuleset) @@ -22,15 +22,15 @@ namespace osu.Game.Rulesets.Catch.UI public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(this); - protected override BeatmapProcessor CreateBeatmapProcessor() => new CatchBeatmapProcessor(); + protected override BeatmapProcessor CreateBeatmapProcessor() => new CatchBeatmapProcessor(); - protected override BeatmapConverter CreateBeatmapConverter() => new CatchBeatmapConverter(); + protected override BeatmapConverter CreateBeatmapConverter() => new CatchBeatmapConverter(); - protected override Playfield CreatePlayfield() => new CatchPlayfield(); + protected override Playfield CreatePlayfield() => new CatchPlayfield(Beatmap.BeatmapInfo.BaseDifficulty); public override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo); - protected override DrawableHitObject GetVisualRepresentation(CatchBaseHit h) + protected override DrawableHitObject GetVisualRepresentation(CatchHitObject h) { var fruit = h as Fruit; if (fruit != null) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs deleted file mode 100644 index 87fe95ed2f..0000000000 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; -using osu.Framework.Input.Bindings; -using osu.Framework.MathUtils; -using osu.Game.Rulesets.Catch.Objects; -using osu.Game.Rulesets.Objects.Drawables; -using OpenTK; - -namespace osu.Game.Rulesets.Catch.UI -{ - public class Catcher : Container, IKeyBindingHandler - { - private Texture texture; - - private Container caughtFruit; - - public Container ExplodingFruitTarget; - - [BackgroundDependencyLoader] - private void load(TextureStore textures) - { - texture = textures.Get(@"Play/Catch/fruit-catcher-idle"); - - Children = new Drawable[] - { - createCatcherSprite(), - caughtFruit = new Container - { - Anchor = Anchor.TopCentre, - Origin = Anchor.BottomCentre, - } - }; - } - - private int currentDirection; - - private bool dashing; - - protected bool Dashing - { - get { return dashing; } - set - { - if (value == dashing) return; - - dashing = value; - - if (dashing) - Schedule(addAdditiveSprite); - } - } - - private void addAdditiveSprite() - { - if (!dashing) return; - - var additive = createCatcherSprite(); - - additive.RelativePositionAxes = Axes.Both; - additive.Blending = BlendingMode.Additive; - additive.Position = Position; - additive.Scale = Scale; - - ((Container)Parent).Add(additive); - - additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint).Expire(); - - Scheduler.AddDelayed(addAdditiveSprite, 50); - } - - private Sprite createCatcherSprite() => new Sprite - { - RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fit, - Texture = texture, - OriginPosition = new Vector2(DrawWidth / 2, 10) //temporary until the sprite is aligned correctly. - }; - - public bool OnPressed(CatchAction action) - { - switch (action) - { - case CatchAction.MoveLeft: - currentDirection--; - return true; - case CatchAction.MoveRight: - currentDirection++; - return true; - case CatchAction.Dash: - Dashing = true; - return true; - } - - return false; - } - - public bool OnReleased(CatchAction action) - { - switch (action) - { - case CatchAction.MoveLeft: - currentDirection++; - return true; - case CatchAction.MoveRight: - currentDirection--; - return true; - case CatchAction.Dash: - Dashing = false; - return true; - } - - return false; - } - - /// - /// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable. - /// - private const double base_speed = 1.0 / 512; - - protected override void Update() - { - base.Update(); - - if (currentDirection == 0) return; - - double dashModifier = Dashing ? 1 : 0.5; - - Scale = new Vector2(Math.Sign(currentDirection), 1); - X = (float)MathHelper.Clamp(X + Math.Sign(currentDirection) * Clock.ElapsedFrameTime * base_speed * dashModifier, 0, 1); - } - - public void Add(DrawableHitObject fruit, Vector2 absolutePosition) - { - fruit.RelativePositionAxes = Axes.None; - fruit.Position = new Vector2(ToLocalSpace(absolutePosition).X - DrawSize.X / 2, 0); - - fruit.Anchor = Anchor.TopCentre; - fruit.Origin = Anchor.BottomCentre; - fruit.Scale *= 0.7f; - fruit.LifetimeEnd = double.MaxValue; - - float distance = fruit.DrawSize.X / 2 * fruit.Scale.X; - - while (caughtFruit.Any(f => f.LifetimeEnd == double.MaxValue && Vector2Extensions.DistanceSquared(f.Position, fruit.Position) < distance * distance)) - { - fruit.X += RNG.Next(-5, 5); - fruit.Y -= RNG.Next(0, 5); - } - - caughtFruit.Add(fruit); - - if (((CatchBaseHit)fruit.HitObject).LastInCombo) - explode(); - } - - private void explode() - { - var fruit = caughtFruit.ToArray(); - - foreach (var f in fruit) - { - var originalX = f.X * Scale.X; - - if (ExplodingFruitTarget != null) - { - f.Anchor = Anchor.TopLeft; - f.Position = caughtFruit.ToSpaceOfOtherDrawable(f.DrawPosition, ExplodingFruitTarget); - - caughtFruit.Remove(f); - - ExplodingFruitTarget.Add(f); - } - - f.MoveToY(f.Y - 50, 250, Easing.OutSine) - .Then() - .MoveToY(f.Y + 50, 500, Easing.InSine); - - f.MoveToX(f.X + originalX * 6, 1000); - f.FadeOut(750); - - f.Expire(); - } - } - } -} diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs new file mode 100644 index 0000000000..2bb0f3cd18 --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -0,0 +1,342 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Input.Bindings; +using osu.Framework.MathUtils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Rulesets.Catch.UI +{ + public class CatcherArea : Container + { + public const float CATCHER_SIZE = 172; + + protected readonly Catcher MovableCatcher; + + public Container ExplodingFruitTarget + { + set { MovableCatcher.ExplodingFruitTarget = value; } + } + + public CatcherArea(BeatmapDifficulty difficulty = null) + { + RelativeSizeAxes = Axes.X; + Height = CATCHER_SIZE; + Child = MovableCatcher = new Catcher(difficulty) + { + AdditiveTarget = this, + }; + } + + public void Add(DrawableHitObject fruit, Vector2 absolutePosition) + { + fruit.RelativePositionAxes = Axes.None; + fruit.Position = new Vector2(MovableCatcher.ToLocalSpace(absolutePosition).X - MovableCatcher.DrawSize.X / 2, 0); + + fruit.Anchor = Anchor.TopCentre; + fruit.Origin = Anchor.BottomCentre; + fruit.Scale *= 0.7f; + fruit.LifetimeEnd = double.MaxValue; + + MovableCatcher.Add(fruit); + } + + public bool AttemptCatch(CatchHitObject obj) => MovableCatcher.AttemptCatch(obj); + + public class Catcher : Container, IKeyBindingHandler + { + private Texture texture; + + private Container caughtFruit; + + public Container ExplodingFruitTarget; + + public Container AdditiveTarget; + + public Catcher(BeatmapDifficulty difficulty = null) + { + RelativePositionAxes = Axes.X; + X = 0.5f; + + Origin = Anchor.TopCentre; + Anchor = Anchor.TopLeft; + + Size = new Vector2(CATCHER_SIZE); + if (difficulty != null) + Scale = new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + texture = textures.Get(@"Play/Catch/fruit-catcher-idle"); + + Children = new Drawable[] + { + createCatcherSprite(), + caughtFruit = new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + } + }; + } + + private int currentDirection; + + private bool dashing; + + protected bool Dashing + { + get { return dashing; } + set + { + if (value == dashing) return; + + dashing = value; + + Trail |= dashing; + } + } + + private bool trail; + + /// + /// Activate or deactive the trail. Will be automatically deactivated when conditions to keep the trail displayed are no longer met. + /// + protected bool Trail + { + get { return trail; } + set + { + if (value == trail) return; + + trail = value; + + if (Trail) + beginTrail(); + } + } + + private void beginTrail() + { + Trail &= dashing || HyperDashing; + Trail &= AdditiveTarget != null; + + if (!Trail) return; + + var additive = createCatcherSprite(); + + additive.Anchor = Anchor; + additive.OriginPosition = additive.OriginPosition + new Vector2(DrawWidth / 2, 0); // also temporary to align sprite correctly. + additive.Position = Position; + additive.Scale = Scale; + additive.Colour = HyperDashing ? Color4.Red : Color4.White; + additive.RelativePositionAxes = RelativePositionAxes; + additive.Blending = BlendingMode.Additive; + + AdditiveTarget.Add(additive); + + additive.FadeTo(0.4f).FadeOut(800, Easing.OutQuint).Expire(); + + Scheduler.AddDelayed(beginTrail, HyperDashing ? 25 : 50); + } + + private Sprite createCatcherSprite() => new Sprite + { + Size = new Vector2(CATCHER_SIZE), + FillMode = FillMode.Fill, + Texture = texture, + OriginPosition = new Vector2(-3, 10) // temporary until the sprite is aligned correctly. + }; + + /// + /// Add a caught fruit to the catcher's stack. + /// + /// The fruit that was caught. + public void Add(DrawableHitObject fruit) + { + float distance = fruit.DrawSize.X / 2 * fruit.Scale.X; + + while (caughtFruit.Any(f => f.LifetimeEnd == double.MaxValue && Vector2Extensions.DistanceSquared(f.Position, fruit.Position) < distance * distance)) + { + fruit.X += RNG.Next(-5, 5); + fruit.Y -= RNG.Next(0, 5); + } + + caughtFruit.Add(fruit); + + var catchObject = (CatchHitObject)fruit.HitObject; + + if (catchObject.LastInCombo) + explode(); + } + + /// + /// Let the catcher attempt to catch a fruit. + /// + /// The fruit to catch. + /// Whether the catch is possible. + public bool AttemptCatch(CatchHitObject fruit) + { + const double relative_catcher_width = CATCHER_SIZE / 2; + + // this stuff wil disappear once we move fruit to non-relative coordinate space in the future. + var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH; + var catcherPosition = Position.X * CatchPlayfield.BASE_WIDTH; + + var validCatch = + catchObjectPosition >= catcherPosition - relative_catcher_width / 2 && + catchObjectPosition <= catcherPosition + relative_catcher_width / 2; + + if (validCatch && fruit.HyperDash) + { + HyperDashModifier = Math.Abs(fruit.HyperDashTarget.X - fruit.X) / Math.Abs(fruit.HyperDashTarget.StartTime - fruit.StartTime) / BASE_SPEED; + HyperDashDirection = fruit.HyperDashTarget.X - fruit.X; + } + else + HyperDashModifier = 1; + + return validCatch; + } + + /// + /// Whether we are hypderdashing or not. + /// + public bool HyperDashing => hyperDashModifier != 1; + + private double hyperDashModifier = 1; + + /// + /// The direction in which hyperdash is allowed. 0 allows both directions. + /// + public double HyperDashDirection; + + /// + /// The speed modifier resultant from hyperdash. Will trigger hyperdash when not equal to 1. + /// + public double HyperDashModifier + { + get { return hyperDashModifier; } + set + { + if (value == hyperDashModifier) return; + hyperDashModifier = value; + + const float transition_length = 180; + + if (HyperDashing) + { + this.FadeColour(Color4.OrangeRed, transition_length, Easing.OutQuint); + this.FadeTo(0.2f, transition_length, Easing.OutQuint); + Trail = true; + } + else + { + HyperDashDirection = 0; + this.FadeColour(Color4.White, transition_length, Easing.OutQuint); + this.FadeTo(1, transition_length, Easing.OutQuint); + } + } + } + + public bool OnPressed(CatchAction action) + { + switch (action) + { + case CatchAction.MoveLeft: + currentDirection--; + return true; + case CatchAction.MoveRight: + currentDirection++; + return true; + case CatchAction.Dash: + Dashing = true; + return true; + } + + return false; + } + + public bool OnReleased(CatchAction action) + { + switch (action) + { + case CatchAction.MoveLeft: + currentDirection++; + return true; + case CatchAction.MoveRight: + currentDirection--; + return true; + case CatchAction.Dash: + Dashing = false; + return true; + } + + return false; + } + + /// + /// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable. + /// + public const double BASE_SPEED = 1.0 / 512; + + protected override void Update() + { + base.Update(); + + if (currentDirection == 0) return; + + var direction = Math.Sign(currentDirection); + + double dashModifier = Dashing ? 1 : 0.5; + + if (hyperDashModifier != 1 && (HyperDashDirection == 0 || direction == Math.Sign(HyperDashDirection))) + dashModifier = hyperDashModifier; + + Scale = new Vector2(Math.Abs(Scale.X) * direction, Scale.Y); + X = (float)MathHelper.Clamp(X + direction * Clock.ElapsedFrameTime * BASE_SPEED * dashModifier, 0, 1); + } + + private void explode() + { + var fruit = caughtFruit.ToArray(); + + foreach (var f in fruit) + { + var originalX = f.X * Scale.X; + + if (ExplodingFruitTarget != null) + { + f.Anchor = Anchor.TopLeft; + f.Position = caughtFruit.ToSpaceOfOtherDrawable(f.DrawPosition, ExplodingFruitTarget); + + caughtFruit.Remove(f); + + ExplodingFruitTarget.Add(f); + } + + f.MoveToY(f.Y - 50, 250, Easing.OutSine) + .Then() + .MoveToY(f.Y + 50, 500, Easing.InSine); + + f.MoveToX(f.X + originalX * 6, 1000); + f.FadeOut(750); + + f.Expire(); + } + } + } + } +} diff --git a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj index a666984b95..969ee702e3 100644 --- a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj +++ b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj @@ -1,6 +1,6 @@  - + Debug AnyCPU @@ -21,7 +21,6 @@ prompt 4 false - 6 pdbonly @@ -57,25 +56,24 @@ - + - + + - + + - - osu.licenseheader - diff --git a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs index 67bc347535..e0763284a6 100644 --- a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania { } - public override double Calculate(Dictionary categoryDifficulty = null) => 0; + public override double Calculate(Dictionary categoryDifficulty = null) => 0; protected override BeatmapConverter CreateBeatmapConverter(Beatmap beatmap) => new ManiaBeatmapConverter(true, (int)Math.Max(1, Math.Round(beatmap.BeatmapInfo.BaseDifficulty.CircleSize))); } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaMod.cs b/osu.Game.Rulesets.Mania/Mods/ManiaMod.cs index 164309c227..dfc9993bde 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaMod.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaMod.cs @@ -176,22 +176,10 @@ namespace osu.Game.Rulesets.Mania.Mods public class ManiaModAutoplay : ModAutoplay { - private int availableColumns; - - public override void ApplyToRulesetContainer(RulesetContainer rulesetContainer) - { - // Todo: This shouldn't be done, we should be getting a ManiaBeatmap which should store AvailableColumns - // But this is dependent on a _lot_ of refactoring - var maniaRulesetContainer = (ManiaRulesetContainer)rulesetContainer; - availableColumns = maniaRulesetContainer.AvailableColumns; - - base.ApplyToRulesetContainer(rulesetContainer); - } - protected override Score CreateReplayScore(Beatmap beatmap) => new Score { User = new User { Username = "osu!topus!" }, - Replay = new ManiaAutoGenerator(beatmap, availableColumns).Generate(), + Replay = new ManiaAutoGenerator(beatmap).Generate(), }; } } diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs index 64982532a7..153fee3ab6 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs @@ -1,7 +1,7 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; +using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Objects; @@ -13,15 +13,11 @@ namespace osu.Game.Rulesets.Mania.Replays { internal class ManiaAutoGenerator : AutoGenerator { - private const double release_delay = 20; + public const double RELEASE_DELAY = 20; - private readonly int availableColumns; - - public ManiaAutoGenerator(Beatmap beatmap, int availableColumns) + public ManiaAutoGenerator(Beatmap beatmap) : base(beatmap) { - this.availableColumns = availableColumns; - Replay = new Replay { User = new User { Username = @"Autoplay" } }; } @@ -30,104 +26,52 @@ namespace osu.Game.Rulesets.Mania.Replays public override Replay Generate() { // Todo: Realistically this shouldn't be needed, but the first frame is skipped with the way replays are currently handled - Replay.Frames.Add(new ReplayFrame(-100000, null, null, ReplayButtonState.None)); + Replay.Frames.Add(new ManiaReplayFrame(-100000, 0)); - double[] holdEndTimes = new double[availableColumns]; - for (int i = 0; i < availableColumns; i++) - holdEndTimes[i] = double.NegativeInfinity; + var pointGroups = generateActionPoints().GroupBy(a => a.Time).OrderBy(g => g.First().Time); - // Notes are handled row-by-row - foreach (var objGroup in Beatmap.HitObjects.GroupBy(h => h.StartTime)) + int activeColumns = 0; + foreach (var group in pointGroups) { - double groupTime = objGroup.Key; - - int activeColumns = 0; - - // Get the previously held-down active columns - for (int i = 0; i < availableColumns; i++) + foreach (var point in group) { - if (holdEndTimes[i] > groupTime) - activeColumns |= 1 << i; + if (point is HitPoint) + activeColumns |= 1 << point.Column; + if (point is ReleasePoint) + activeColumns ^= 1 << point.Column; } - // Add on the group columns, keeping track of the held notes for the next rows - foreach (var obj in objGroup) - { - var holdNote = obj as HoldNote; - if (holdNote != null) - holdEndTimes[obj.Column] = Math.Max(holdEndTimes[obj.Column], holdNote.EndTime); - - activeColumns |= 1 << obj.Column; - } - - Replay.Frames.Add(new ReplayFrame(groupTime, activeColumns, null, ReplayButtonState.None)); - - // Add the release frames. We can't do this with the loop above because we need activeColumns to be fully populated - foreach (var obj in objGroup.GroupBy(h => (h as IHasEndTime)?.EndTime ?? h.StartTime + release_delay).OrderBy(h => h.Key)) - { - var groupEndTime = obj.Key; - - int activeColumnsAtEnd = 0; - for (int i = 0; i < availableColumns; i++) - { - if (holdEndTimes[i] > groupEndTime) - activeColumnsAtEnd |= 1 << i; - } - - Replay.Frames.Add(new ReplayFrame(groupEndTime, activeColumnsAtEnd, 0, ReplayButtonState.None)); - } + Replay.Frames.Add(new ManiaReplayFrame(group.First().Time, activeColumns)); } - Replay.Frames = Replay.Frames - // Pick the maximum activeColumns for all frames at the same time - .GroupBy(f => f.Time) - .Select(g => new ReplayFrame(g.First().Time, maxMouseX(g), 0, ReplayButtonState.None)) - // The addition of release frames above maybe result in unordered frames, but we need them ordered - .OrderBy(f => f.Time) - .ToList(); - return Replay; } - /// - /// Finds the maximum by count of bits from a grouping of s. - /// - /// The grouping to search. - /// The maximum by count of bits. - private float maxMouseX(IGrouping group) + private IEnumerable generateActionPoints() { - int currentCount = -1; - int currentMax = 0; - - foreach (var val in group) + foreach (var obj in Beatmap.HitObjects) { - int newCount = countBits((int)(val.MouseX ?? 0)); - if (newCount > currentCount) - { - currentCount = newCount; - currentMax = (int)(val.MouseX ?? 0); - } + yield return new HitPoint { Time = obj.StartTime, Column = obj.Column }; + yield return new ReleasePoint { Time = ((obj as IHasEndTime)?.EndTime ?? obj.StartTime) + RELEASE_DELAY, Column = obj.Column }; } - - return currentMax; } - /// - /// Counts the number of bits set in a value. - /// - /// The value to count. - /// The number of set bits. - private int countBits(int value) + private interface IActionPoint { - int count = 0; - while (value > 0) - { - if ((value & 1) > 0) - count++; - value >>= 1; - } + double Time { get; set; } + int Column { get; set; } + } - return count; + private struct HitPoint : IActionPoint + { + public double Time { get; set; } + public int Column { get; set; } + } + + private struct ReleasePoint : IActionPoint + { + public double Time { get; set; } + public int Column { get; set; } } } } diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs b/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs index e352997f2c..12534d6eb4 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs @@ -2,29 +2,37 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using System.Linq; using osu.Framework.Input; +using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Replays; namespace osu.Game.Rulesets.Mania.Replays { internal class ManiaFramedReplayInputHandler : FramedReplayInputHandler { - public ManiaFramedReplayInputHandler(Replay replay) + private readonly ManiaRulesetContainer container; + + public ManiaFramedReplayInputHandler(Replay replay, ManiaRulesetContainer container) : base(replay) { + this.container = container; } + private ManiaPlayfield playfield; public override List GetPendingStates() { var actions = new List(); - int activeColumns = (int)(CurrentFrame.MouseX ?? 0); + if (playfield == null) + playfield = (ManiaPlayfield)container.Playfield; + int activeColumns = (int)(CurrentFrame.MouseX ?? 0); int counter = 0; while (activeColumns > 0) { if ((activeColumns & 1) > 0) - actions.Add(ManiaAction.Key1 + counter); + actions.Add(playfield.Columns.ElementAt(counter).Action); counter++; activeColumns >>= 1; } diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs new file mode 100644 index 0000000000..d1bc7da911 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs @@ -0,0 +1,17 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Replays; + +namespace osu.Game.Rulesets.Mania.Replays +{ + public class ManiaReplayFrame : ReplayFrame + { + public override bool IsImportant => MouseX > 0; + + public ManiaReplayFrame(double time, int activeColumns) + : base(time, activeColumns, null, ReplayButtonState.None) + { + } + } +} diff --git a/osu.Game.Rulesets.Mania/Tests/TestCaseAutoGeneration.cs b/osu.Game.Rulesets.Mania/Tests/TestCaseAutoGeneration.cs new file mode 100644 index 0000000000..805553eafc --- /dev/null +++ b/osu.Game.Rulesets.Mania/Tests/TestCaseAutoGeneration.cs @@ -0,0 +1,173 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [Ignore("getting CI working")] + public class TestCaseAutoGeneration : OsuTestCase + { + [Test] + public void TestSingleNote() + { + // | | + // | - | + // | | + + var beatmap = new Beatmap(); + beatmap.HitObjects.Add(new Note { StartTime = 1000 }); + + var generated = new ManiaAutoGenerator(beatmap).Generate(); + + Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames"); + Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time"); + Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time"); + Assert.AreEqual(1, generated.Frames[1].MouseX, "Key 0 has not been pressed"); + Assert.AreEqual(0, generated.Frames[2].MouseX, "Key 0 has not been released"); + } + + [Test] + public void TestSingleHoldNote() + { + // | | + // | * | + // | * | + // | * | + // | | + + var beatmap = new Beatmap(); + beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 }); + + var generated = new ManiaAutoGenerator(beatmap).Generate(); + + Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames"); + Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time"); + Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time"); + Assert.AreEqual(1, generated.Frames[1].MouseX, "Key 0 has not been pressed"); + Assert.AreEqual(0, generated.Frames[2].MouseX, "Key 0 has not been released"); + } + + [Test] + public void TestSingleNoteChord() + { + // | | | + // | - | - | + // | | | + + var beatmap = new Beatmap(); + beatmap.HitObjects.Add(new Note { StartTime = 1000 }); + beatmap.HitObjects.Add(new Note { StartTime = 1000, Column = 1 }); + + var generated = new ManiaAutoGenerator(beatmap).Generate(); + + Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames"); + Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time"); + Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time"); + Assert.AreEqual(3, generated.Frames[1].MouseX, "Keys 1 and 2 have not been pressed"); + Assert.AreEqual(0, generated.Frames[2].MouseX, "Keys 1 and 2 have not been released"); + } + + [Test] + public void TestHoldNoteChord() + { + // | | | + // | * | * | + // | * | * | + // | * | * | + // | | | + + var beatmap = new Beatmap(); + beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 }); + beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000, Column = 1 }); + + var generated = new ManiaAutoGenerator(beatmap).Generate(); + + Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames"); + Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time"); + Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time"); + Assert.AreEqual(3, generated.Frames[1].MouseX, "Keys 1 and 2 have not been pressed"); + Assert.AreEqual(0, generated.Frames[2].MouseX, "Keys 1 and 2 have not been released"); + } + + [Test] + public void TestSingleNoteStair() + { + // | | | + // | | - | + // | - | | + // | | | + + var beatmap = new Beatmap(); + beatmap.HitObjects.Add(new Note { StartTime = 1000 }); + beatmap.HitObjects.Add(new Note { StartTime = 2000, Column = 1 }); + + var generated = new ManiaAutoGenerator(beatmap).Generate(); + + Assert.IsTrue(generated.Frames.Count == 5, "Replay must have 5 frames"); + Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect first note hit time"); + Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect first note release time"); + Assert.AreEqual(2000, generated.Frames[3].Time, "Incorrect second note hit time"); + Assert.AreEqual(2000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[4].Time, "Incorrect second note release time"); + Assert.AreEqual(1, generated.Frames[1].MouseX, "Key 1 has not been pressed"); + Assert.AreEqual(0, generated.Frames[2].MouseX, "Key 1 has not been released"); + Assert.AreEqual(2, generated.Frames[3].MouseX, "Key 2 has not been pressed"); + Assert.AreEqual(0, generated.Frames[4].MouseX, "Key 2 has not been released"); + } + + [Test] + public void TestHoldNoteStair() + { + // | | | + // | | * | + // | * | * | + // | * | * | + // | * | | + // | | | + + var beatmap = new Beatmap(); + beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 }); + beatmap.HitObjects.Add(new HoldNote { StartTime = 2000, Duration = 2000, Column = 1 }); + + var generated = new ManiaAutoGenerator(beatmap).Generate(); + + Assert.IsTrue(generated.Frames.Count == 5, "Replay must have 5 frames"); + Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect first note hit time"); + Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[3].Time, "Incorrect first note release time"); + Assert.AreEqual(2000, generated.Frames[2].Time, "Incorrect second note hit time"); + Assert.AreEqual(4000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[4].Time, "Incorrect second note release time"); + Assert.AreEqual(1, generated.Frames[1].MouseX, "Key 1 has not been pressed"); + Assert.AreEqual(3, generated.Frames[2].MouseX, "Keys 1 and 2 have not been pressed"); + Assert.AreEqual(2, generated.Frames[3].MouseX, "Key 1 has not been released"); + Assert.AreEqual(0, generated.Frames[4].MouseX, "Key 2 has not been released"); + } + + [Test] + public void TestHoldNoteWithReleasePress() + { + // | | | + // | * | - | + // | * | | + // | * | | + // | | | + + var beatmap = new Beatmap(); + beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 - ManiaAutoGenerator.RELEASE_DELAY }); + beatmap.HitObjects.Add(new Note { StartTime = 3000, Column = 1 }); + + var generated = new ManiaAutoGenerator(beatmap).Generate(); + + Assert.IsTrue(generated.Frames.Count == 4, "Replay must have 4 frames"); + Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect first note hit time"); + Assert.AreEqual(3000, generated.Frames[2].Time, "Incorrect second note press time + first note release time"); + Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[3].Time, "Incorrect second note release time"); + Assert.AreEqual(1, generated.Frames[1].MouseX, "Key 1 has not been pressed"); + Assert.AreEqual(2, generated.Frames[2].MouseX, "Key 1 has not been released or key 2 has not been pressed"); + Assert.AreEqual(0, generated.Frames[3].MouseX, "Keys 1 and 2 have not been released"); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Tests/TestCasePerformancePoints.cs b/osu.Game.Rulesets.Mania/Tests/TestCasePerformancePoints.cs new file mode 100644 index 0000000000..8aa8c6b799 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Tests/TestCasePerformancePoints.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; + +namespace osu.Game.Rulesets.Mania.Tests +{ + [Ignore("getting CI working")] + public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints + { + public TestCasePerformancePoints() + : base(new ManiaRuleset(new RulesetInfo())) + { + } + } +} diff --git a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs index 08acd46c57..cbbcb84b31 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs @@ -124,6 +124,6 @@ namespace osu.Game.Rulesets.Mania.UI protected override SpeedAdjustmentContainer CreateSpeedAdjustmentContainer(MultiplierControlPoint controlPoint) => new ManiaSpeedAdjustmentContainer(controlPoint, ScrollingAlgorithm.Basic); - protected override FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay); + protected override FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay, this); } } diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj index 6f45a64d92..ec6f59b5be 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -1,6 +1,6 @@  - + Debug AnyCPU @@ -21,7 +21,6 @@ prompt 4 false - 6 pdbonly @@ -72,6 +71,7 @@ + @@ -80,8 +80,10 @@ + + @@ -96,9 +98,6 @@ - - osu.licenseheader - diff --git a/osu.Game.Rulesets.Osu/Edit/OsuEditPlayfield.cs b/osu.Game.Rulesets.Osu/Edit/OsuEditPlayfield.cs new file mode 100644 index 0000000000..d5fc1b606b --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/OsuEditPlayfield.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics.Cursor; +using osu.Game.Rulesets.Osu.UI; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public class OsuEditPlayfield : OsuPlayfield + { + protected override CursorContainer CreateCursor() => null; + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs new file mode 100644 index 0000000000..1e9e4b4686 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public class OsuEditRulesetContainer : OsuRulesetContainer + { + public OsuEditRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset) + : base(ruleset, beatmap, isForCurrentRuleset) + { + } + + protected override Playfield CreatePlayfield() => new OsuEditPlayfield(); + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs new file mode 100644 index 0000000000..ec3aa4661c --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -0,0 +1,29 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public class OsuHitObjectComposer : HitObjectComposer + { + public OsuHitObjectComposer(Ruleset ruleset) + : base(ruleset) + { + } + + protected override RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => new OsuEditRulesetContainer(ruleset, beatmap, true); + + protected override IReadOnlyList CompositionTools => new ICompositionTool[] + { + new HitObjectCompositionTool(), + new HitObjectCompositionTool(), + new HitObjectCompositionTool() + }; + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 112fcb1a30..39ec753fe1 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -45,6 +45,18 @@ namespace osu.Game.Rulesets.Osu.Objects set { Curve.Distance = value; } } + /// + /// The position of the cursor at the point of completion of this if it was hit + /// with as few movements as possible. This is set and used by difficulty calculation. + /// + internal Vector2? LazyEndPosition; + + /// + /// The distance travelled by the cursor upon completion of this if it was hit + /// with as few movements as possible. This is set and used by difficulty calculation. + /// + internal float LazyTravelDistance; + public List RepeatSamples { get; set; } = new List(); public int RepeatCount { get; set; } = 1; diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs index 537874f643..3d185ab694 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/OsuDifficultyCalculator.cs @@ -33,9 +33,9 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty (h as Slider)?.Curve?.Calculate(); } - public override double Calculate(Dictionary categoryDifficulty = null) + public override double Calculate(Dictionary categoryDifficulty = null) { - OsuDifficultyBeatmap beatmap = new OsuDifficultyBeatmap(Beatmap.HitObjects); + OsuDifficultyBeatmap beatmap = new OsuDifficultyBeatmap(Beatmap.HitObjects, TimeRate); Skill[] skills = { new Aim(), @@ -67,8 +67,8 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty if (categoryDifficulty != null) { - categoryDifficulty.Add("Aim", aimRating.ToString("0.00")); - categoryDifficulty.Add("Speed", speedRating.ToString("0.00")); + categoryDifficulty.Add("Aim", aimRating); + categoryDifficulty.Add("Speed", speedRating); } return starRating; diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs index c6ecc3a506..f8e9423e29 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyBeatmap.cs @@ -20,12 +20,12 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing /// Creates an enumerator, which preprocesses a list of s recieved as input, wrapping them as /// which contains extra data required for difficulty calculation. /// - public OsuDifficultyBeatmap(List objects) + public OsuDifficultyBeatmap(List objects, double timeRate) { // Sort OsuHitObjects by StartTime - they are not correctly ordered in some cases. // This should probably happen before the objects reach the difficulty calculator. objects.Sort((a, b) => a.StartTime.CompareTo(b.StartTime)); - difficultyObjects = createDifficultyObjectEnumerator(objects); + difficultyObjects = createDifficultyObjectEnumerator(objects, timeRate); } /// @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - private IEnumerator createDifficultyObjectEnumerator(List objects) + private IEnumerator createDifficultyObjectEnumerator(List objects, double timeRate) { // We will process OsuHitObjects in groups of three to form a triangle, so we can calculate an angle for each object. OsuHitObject[] triangle = new OsuHitObject[3]; @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing triangle[1] = triangle[0]; triangle[0] = objects[i]; - yield return new OsuDifficultyHitObject(triangle); + yield return new OsuDifficultyHitObject(triangle, timeRate); } } } diff --git a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs index bdeb62df3e..972677a6f1 100644 --- a/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/OsuDifficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -2,6 +2,8 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Linq; +using OpenTK; using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing @@ -33,13 +35,17 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing private const int normalized_radius = 52; + private readonly double timeRate; + private readonly OsuHitObject[] t; /// /// Initializes the object calculating extra data required for difficulty calculation. /// - public OsuDifficultyHitObject(OsuHitObject[] triangle) + public OsuDifficultyHitObject(OsuHitObject[] triangle, double timeRate) { + this.timeRate = timeRate; + t = triangle; BaseObject = t[0]; setDistances(); @@ -57,14 +63,53 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing scalingFactor *= 1 + smallCircleBonus; } - Distance = (t[0].StackedPosition - t[1].StackedPosition).Length * scalingFactor; + Vector2 lastCursorPosition = t[1].StackedPosition; + float lastTravelDistance = 0; + + var lastSlider = t[1] as Slider; + if (lastSlider != null) + { + computeSliderCursorPosition(lastSlider); + lastCursorPosition = lastSlider.LazyEndPosition ?? lastCursorPosition; + lastTravelDistance = lastSlider.LazyTravelDistance; + } + + Distance = (lastTravelDistance + (BaseObject.StackedPosition - lastCursorPosition).Length) * scalingFactor; } private void setTimingValues() { // Every timing inverval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure. - DeltaTime = Math.Max(40, t[0].StartTime - t[1].StartTime); + DeltaTime = Math.Max(40, (t[0].StartTime - t[1].StartTime) / timeRate); TimeUntilHit = 450; // BaseObject.PreEmpt; } + + private void computeSliderCursorPosition(Slider slider) + { + if (slider.LazyEndPosition != null) + return; + slider.LazyEndPosition = slider.StackedPosition; + + float approxFollowCircleRadius = (float)(slider.Radius * 3); + var computeVertex = new Action(t => + { + var diff = slider.PositionAt(t) - slider.LazyEndPosition.Value; + float dist = diff.Length; + + if (dist > approxFollowCircleRadius) + { + // The cursor would be outside the follow circle, we need to move it + diff.Normalize(); // Obtain direction of diff + dist -= approxFollowCircleRadius; + slider.LazyEndPosition += diff * dist; + slider.LazyTravelDistance += dist; + } + }); + + var scoringTimes = slider.Ticks.Select(t => t.StartTime).Concat(slider.RepeatPoints.Select(r => r.StartTime)).OrderBy(t => t); + foreach (var time in scoringTimes) + computeVertex(time); + computeVertex(slider.EndTime); + } } } diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs index a65d28cec0..56990d1351 100644 --- a/osu.Game.Rulesets.Osu/OsuInputManager.cs +++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Collections.Generic; using System.ComponentModel; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.UI; @@ -9,6 +10,8 @@ namespace osu.Game.Rulesets.Osu { public class OsuInputManager : RulesetInputManager { + public IEnumerable PressedActions => KeyBindingContainer.PressedActions; + public OsuInputManager(RulesetInfo ruleset) : base(ruleset, 0, SimultaneousBindingMode.Unique) { } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index c87328d87c..0d2343a33e 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -14,6 +14,10 @@ using System.Linq; using osu.Framework.Graphics; using osu.Game.Overlays.Settings; using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Rulesets.Osu.Edit; +using osu.Game.Rulesets.Edit; namespace osu.Game.Rulesets.Osu { @@ -114,6 +118,10 @@ namespace osu.Game.Rulesets.Osu public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new OsuDifficultyCalculator(beatmap, mods); + public override PerformanceCalculator CreatePerformanceCalculator(Beatmap beatmap, Score score) => new OsuPerformanceCalculator(this, beatmap, score); + + public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this); + public override string Description => "osu!"; public override SettingsSubsection CreateSettings() => new OsuSettings(); diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Scoring/OsuPerformanceCalculator.cs new file mode 100644 index 0000000000..cd6b6c5e27 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Scoring/OsuPerformanceCalculator.cs @@ -0,0 +1,199 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Osu.Scoring +{ + public class OsuPerformanceCalculator : PerformanceCalculator + { + private readonly int countHitCircles; + private readonly int beatmapMaxCombo; + + private Mod[] mods; + private double realApproachRate; + private double accuracy; + private int scoreMaxCombo; + private int count300; + private int count100; + private int count50; + private int countMiss; + + public OsuPerformanceCalculator(Ruleset ruleset, Beatmap beatmap, Score score) + : base(ruleset, beatmap, score) + { + countHitCircles = Beatmap.HitObjects.Count(h => h is HitCircle); + + beatmapMaxCombo = Beatmap.HitObjects.Count; + beatmapMaxCombo += Beatmap.HitObjects.OfType().Sum(s => s.RepeatCount + s.Ticks.Count()); + } + + public override double Calculate(Dictionary categoryRatings = null) + { + mods = Score.Mods; + accuracy = Score.Accuracy; + scoreMaxCombo = Score.MaxCombo; + count300 = Convert.ToInt32(Score.Statistics["300"]); + count100 = Convert.ToInt32(Score.Statistics["100"]); + count50 = Convert.ToInt32(Score.Statistics["50"]); + countMiss = Convert.ToInt32(Score.Statistics["x"]); + + // Don't count scores made with supposedly unranked mods + if (mods.Any(m => !m.Ranked)) + return 0; + + // Todo: In the future we should apply changes to PreEmpt/AR at an OsuHitObject/BaseDifficulty level, but this is done + // locally for now as doing so would modify animations and other things unexpectedly + // DO NOT MODIFY THIS + double ar = Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate; + if (mods.Any(m => m is OsuModHardRock)) + ar = Math.Min(10, ar * 1.4); + if (mods.Any(m => m is OsuModEasy)) + ar = Math.Max(0, ar / 2); + double preEmpt = BeatmapDifficulty.DifficultyRange(ar, 1800, 1200, 450); + realApproachRate = preEmpt > 1200 ? (1800 - preEmpt) / 120 : (1200 - preEmpt) / 150 + 5; + + // Custom multipliers for NoFail and SpunOut. + double multiplier = 1.12f; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things + + if (mods.Any(m => m is OsuModNoFail)) + multiplier *= 0.90f; + + if (mods.Any(m => m is OsuModSpunOut)) + multiplier *= 0.95f; + + double aimValue = computeAimValue(); + double speedValue = computeSpeedValue(); + double accuracyValue = computeAccuracyValue(); + double totalValue = + Math.Pow( + Math.Pow(aimValue, 1.1f) + + Math.Pow(speedValue, 1.1f) + + Math.Pow(accuracyValue, 1.1f), 1.0f / 1.1f + ) * multiplier; + + if (categoryRatings != null) + { + categoryRatings.Add("Aim", aimValue); + categoryRatings.Add("Speed", speedValue); + categoryRatings.Add("Accuracy", accuracyValue); + } + + return totalValue; + } + + private double computeAimValue() + { + double aimValue = Math.Pow(5.0f * Math.Max(1.0f, Attributes["Aim"] / 0.0675f) - 4.0f, 3.0f) / 100000.0f; + + // Longer maps are worth more + double lengthBonus = 0.95f + 0.4f * Math.Min(1.0f, totalHits / 2000.0f) + + (totalHits > 2000 ? Math.Log10(totalHits / 2000.0f) * 0.5f : 0.0f); + + aimValue *= lengthBonus; + + // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available + aimValue *= Math.Pow(0.97f, countMiss); + + // Combo scaling + if (beatmapMaxCombo > 0) + aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f); + + double approachRateFactor = 1.0f; + if (realApproachRate > 10.33f) + approachRateFactor += 0.45f * (realApproachRate - 10.33f); + else if (realApproachRate < 8.0f) + { + // HD is worth more with lower ar! + if (mods.Any(h => h is OsuModHidden)) + approachRateFactor += 0.02f * (8.0f - realApproachRate); + else + approachRateFactor += 0.01f * (8.0f - realApproachRate); + } + + aimValue *= approachRateFactor; + + if (mods.Any(h => h is OsuModHidden)) + aimValue *= 1.18f; + + if (mods.Any(h => h is OsuModFlashlight)) + { + // Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps. + aimValue *= 1.45f * lengthBonus; + } + + // Scale the aim value with accuracy _slightly_ + aimValue *= 0.5f + accuracy / 2.0f; + // It is important to also consider accuracy difficulty when doing that + aimValue *= 0.98f + Math.Pow(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 2) / 2500; + + return aimValue; + } + + private double computeSpeedValue() + { + double speedValue = Math.Pow(5.0f * Math.Max(1.0f, Attributes["Speed"] / 0.0675f) - 4.0f, 3.0f) / 100000.0f; + + // Longer maps are worth more + speedValue *= 0.95f + 0.4f * Math.Min(1.0f, totalHits / 2000.0f) + + (totalHits > 2000 ? Math.Log10(totalHits / 2000.0f) * 0.5f : 0.0f); + + // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available + speedValue *= Math.Pow(0.97f, countMiss); + + // Combo scaling + if (beatmapMaxCombo > 0) + speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f); + + // Scale the speed value with accuracy _slightly_ + speedValue *= 0.5f + accuracy / 2.0f; + // It is important to also consider accuracy difficulty when doing that + speedValue *= 0.98f + Math.Pow(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 2) / 2500; + + return speedValue; + } + + private double computeAccuracyValue() + { + // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window + double betterAccuracyPercentage; + int amountHitObjectsWithAccuracy = countHitCircles; + + if (amountHitObjectsWithAccuracy > 0) + betterAccuracyPercentage = ((count300 - (totalHits - amountHitObjectsWithAccuracy)) * 6 + count100 * 2 + count50) / (amountHitObjectsWithAccuracy * 6); + else + betterAccuracyPercentage = 0; + + // It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points + if (betterAccuracyPercentage < 0) + betterAccuracyPercentage = 0; + + // Lots of arbitrary values from testing. + // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution + double accuracyValue = Math.Pow(1.52163f, Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83f; + + // Bonus for many hitcircles - it's harder to keep good accuracy up for longer + accuracyValue *= Math.Min(1.15f, Math.Pow(amountHitObjectsWithAccuracy / 1000.0f, 0.3f)); + + if (mods.Any(m => m is OsuModHidden)) + accuracyValue *= 1.02f; + if (mods.Any(m => m is OsuModFlashlight)) + accuracyValue *= 1.02f; + + return accuracyValue; + } + + private double totalHits => count300 + count100 + count50 + countMiss; + private double totalSuccessfulHits => count300 + count100 + count50; + + protected override BeatmapConverter CreateBeatmapConverter() => new OsuBeatmapConverter(); + } +} diff --git a/osu.Game.Rulesets.Osu/Tests/TestCasePerformancePoints.cs b/osu.Game.Rulesets.Osu/Tests/TestCasePerformancePoints.cs new file mode 100644 index 0000000000..25a6110459 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Tests/TestCasePerformancePoints.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [Ignore("getting CI working")] + public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints + { + public TestCasePerformancePoints() + : base(new OsuRuleset(new RulesetInfo())) + { + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 89f6a4e255..387a098a5a 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -13,6 +13,7 @@ using System.Linq; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.UI.Cursor; +using osu.Framework.Graphics.Cursor; namespace osu.Game.Rulesets.Osu.UI { @@ -65,7 +66,10 @@ namespace osu.Game.Rulesets.Osu.UI protected override void LoadComplete() { base.LoadComplete(); - AddInternal(new GameplayCursor()); + + var cursor = CreateCursor(); + if (cursor != null) + AddInternal(cursor); } public override void Add(DrawableHitObject h) @@ -102,5 +106,7 @@ namespace osu.Game.Rulesets.Osu.UI judgementLayer.Add(explosion); } + + protected virtual CursorContainer CreateCursor() => new GameplayCursor(); } } diff --git a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj index 3c90749777..819b6d52a9 100644 --- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj +++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj @@ -1,6 +1,6 @@  - + Debug AnyCPU @@ -22,7 +22,6 @@ prompt 4 false - 6 pdbonly @@ -49,6 +48,9 @@ + + + @@ -85,9 +87,11 @@ + + @@ -103,9 +107,6 @@ - - osu.licenseheader - diff --git a/osu.Game.Rulesets.Taiko/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/TaikoDifficultyCalculator.cs index e881942fbf..e74c12fa5d 100644 --- a/osu.Game.Rulesets.Taiko/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/TaikoDifficultyCalculator.cs @@ -5,7 +5,6 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Taiko.Beatmaps; using osu.Game.Rulesets.Taiko.Objects; using System.Collections.Generic; -using System.Globalization; using System; namespace osu.Game.Rulesets.Taiko @@ -36,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko { } - public override double Calculate(Dictionary categoryDifficulty = null) + public override double Calculate(Dictionary categoryDifficulty = null) { // Fill our custom DifficultyHitObject class, that carries additional information difficultyHitObjects.Clear(); @@ -53,8 +52,8 @@ namespace osu.Game.Rulesets.Taiko if (categoryDifficulty != null) { - categoryDifficulty.Add("Strain", starRating.ToString("0.00", CultureInfo.InvariantCulture)); - categoryDifficulty.Add("Hit window 300", (35 /*HitObjectManager.HitWindow300*/ / TimeRate).ToString("0.00", CultureInfo.InvariantCulture)); + categoryDifficulty.Add("Strain", starRating); + categoryDifficulty.Add("Hit window 300", 35 /*HitObjectManager.HitWindow300*/ / TimeRate); } return starRating; diff --git a/osu.Game.Rulesets.Taiko/Tests/TestCasePerformancePoints.cs b/osu.Game.Rulesets.Taiko/Tests/TestCasePerformancePoints.cs new file mode 100644 index 0000000000..96d5b20b6e --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Tests/TestCasePerformancePoints.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + [Ignore("getting CI working")] + public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints + { + public TestCasePerformancePoints() + : base(new TaikoRuleset(new RulesetInfo())) + { + } + } +} diff --git a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj index bf627d205a..72e9e6a061 100644 --- a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj +++ b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj @@ -1,6 +1,6 @@  - + Debug AnyCPU @@ -21,7 +21,6 @@ prompt 4 false - 6 pdbonly @@ -83,6 +82,7 @@ + @@ -95,9 +95,6 @@ - - osu.licenseheader - diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs new file mode 100644 index 0000000000..86413af4b6 --- /dev/null +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -0,0 +1,214 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.IO; +using NUnit.Framework; +using OpenTK; +using OpenTK.Graphics; +using osu.Game.Tests.Resources; +using System.Linq; +using osu.Game.Audio; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Beatmaps.Formats; +using osu.Game.Beatmaps.Timing; + +namespace osu.Game.Tests.Beatmaps.Formats +{ + [TestFixture] + public class LegacyBeatmapDecoderTest + { + [Test] + public void TestDecodeBeatmapGeneral() + { + var decoder = new LegacyBeatmapDecoder(); + using (var resStream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) + using (var stream = new StreamReader(resStream)) + { + var beatmap = decoder.DecodeBeatmap(stream); + var beatmapInfo = beatmap.BeatmapInfo; + var metadata = beatmap.Metadata; + + Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", metadata.AudioFile); + Assert.AreEqual(0, beatmapInfo.AudioLeadIn); + Assert.AreEqual(164471, metadata.PreviewTime); + Assert.IsFalse(beatmapInfo.Countdown); + Assert.AreEqual(0.7f, beatmapInfo.StackLeniency); + Assert.IsTrue(beatmapInfo.RulesetID == 0); + Assert.IsFalse(beatmapInfo.LetterboxInBreaks); + Assert.IsFalse(beatmapInfo.SpecialStyle); + Assert.IsFalse(beatmapInfo.WidescreenStoryboard); + } + } + + [Test] + public void TestDecodeBeatmapEditor() + { + var decoder = new LegacyBeatmapDecoder(); + using (var resStream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) + using (var stream = new StreamReader(resStream)) + { + var beatmapInfo = decoder.DecodeBeatmap(stream).BeatmapInfo; + + int[] expectedBookmarks = + { + 11505, 22054, 32604, 43153, 53703, 64252, 74802, 85351, + 95901, 106450, 116999, 119637, 130186, 140735, 151285, + 161834, 164471, 175020, 185570, 196119, 206669, 209306 + }; + Assert.AreEqual(expectedBookmarks.Length, beatmapInfo.Bookmarks.Length); + for (int i = 0; i < expectedBookmarks.Length; i++) + Assert.AreEqual(expectedBookmarks[i], beatmapInfo.Bookmarks[i]); + Assert.AreEqual(1.8, beatmapInfo.DistanceSpacing); + Assert.AreEqual(4, beatmapInfo.BeatDivisor); + Assert.AreEqual(4, beatmapInfo.GridSize); + Assert.AreEqual(2, beatmapInfo.TimelineZoom); + } + } + + [Test] + public void TestDecodeBeatmapMetadata() + { + var decoder = new LegacyBeatmapDecoder(); + using (var resStream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) + using (var stream = new StreamReader(resStream)) + { + var beatmap = decoder.DecodeBeatmap(stream); + var beatmapInfo = beatmap.BeatmapInfo; + var metadata = beatmap.Metadata; + + Assert.AreEqual("Renatus", metadata.Title); + Assert.AreEqual("Renatus", metadata.TitleUnicode); + Assert.AreEqual("Soleily", metadata.Artist); + Assert.AreEqual("Soleily", metadata.ArtistUnicode); + Assert.AreEqual("Gamu", metadata.AuthorString); + Assert.AreEqual("Insane", beatmapInfo.Version); + Assert.AreEqual(string.Empty, metadata.Source); + Assert.AreEqual("MBC7 Unisphere 地球ヤバイEP Chikyu Yabai", metadata.Tags); + Assert.AreEqual(557821, beatmapInfo.OnlineBeatmapID); + Assert.AreEqual(241526, metadata.OnlineBeatmapSetID); + } + } + + [Test] + public void TestDecodeBeatmapDifficulty() + { + var decoder = new LegacyBeatmapDecoder(); + using (var resStream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) + using (var stream = new StreamReader(resStream)) + { + var difficulty = decoder.DecodeBeatmap(stream).BeatmapInfo.BaseDifficulty; + + Assert.AreEqual(6.5f, difficulty.DrainRate); + Assert.AreEqual(4, difficulty.CircleSize); + Assert.AreEqual(8, difficulty.OverallDifficulty); + Assert.AreEqual(9, difficulty.ApproachRate); + Assert.AreEqual(1.8f, difficulty.SliderMultiplier); + Assert.AreEqual(2, difficulty.SliderTickRate); + } + } + + [Test] + public void TestDecodeBeatmapEvents() + { + var decoder = new LegacyBeatmapDecoder(); + using (var resStream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) + using (var stream = new StreamReader(resStream)) + { + var beatmap = decoder.DecodeBeatmap(stream); + var metadata = beatmap.Metadata; + var breakPoint = beatmap.Breaks[0]; + + Assert.AreEqual("machinetop_background.jpg", metadata.BackgroundFile); + Assert.AreEqual(122474, breakPoint.StartTime); + Assert.AreEqual(140135, breakPoint.EndTime); + Assert.IsTrue(breakPoint.HasEffect); + } + } + + [Test] + public void TestDecodeBeatmapTimingPoints() + { + var decoder = new LegacyBeatmapDecoder(); + using (var resStream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) + using (var stream = new StreamReader(resStream)) + { + var beatmap = decoder.DecodeBeatmap(stream); + var controlPoints = beatmap.ControlPointInfo; + + Assert.AreEqual(4, controlPoints.TimingPoints.Count); + var timingPoint = controlPoints.TimingPoints[0]; + Assert.AreEqual(956, timingPoint.Time); + Assert.AreEqual(329.67032967033d, timingPoint.BeatLength); + Assert.AreEqual(TimeSignatures.SimpleQuadruple, timingPoint.TimeSignature); + + Assert.AreEqual(5, controlPoints.DifficultyPoints.Count); + var difficultyPoint = controlPoints.DifficultyPoints[0]; + Assert.AreEqual(116999, difficultyPoint.Time); + Assert.AreEqual(0.75000000000000189d, difficultyPoint.SpeedMultiplier); + + Assert.AreEqual(34, controlPoints.SoundPoints.Count); + var soundPoint = controlPoints.SoundPoints[0]; + Assert.AreEqual(956, soundPoint.Time); + Assert.AreEqual("soft", soundPoint.SampleBank); + Assert.AreEqual(60, soundPoint.SampleVolume); + + Assert.AreEqual(8, controlPoints.EffectPoints.Count); + var effectPoint = controlPoints.EffectPoints[0]; + Assert.AreEqual(53703, effectPoint.Time); + Assert.IsTrue(effectPoint.KiaiMode); + Assert.IsFalse(effectPoint.OmitFirstBarLine); + } + } + + [Test] + public void TestDecodeBeatmapColors() + { + var decoder = new LegacyBeatmapDecoder(); + using (var resStream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) + using (var stream = new StreamReader(resStream)) + { + var comboColors = decoder.DecodeBeatmap(stream).ComboColors; + + Color4[] expectedColors = + { + new Color4(142, 199, 255, 255), + new Color4(255, 128, 128, 255), + new Color4(128, 255, 255, 255), + new Color4(128, 255, 128, 255), + new Color4(255, 187, 255, 255), + new Color4(255, 177, 140, 255), + }; + Assert.AreEqual(expectedColors.Length, comboColors.Count); + for (int i = 0; i < expectedColors.Length; i++) + Assert.AreEqual(expectedColors[i], comboColors[i]); + } + } + + [Test] + public void TestDecodeBeatmapHitObjects() + { + var decoder = new LegacyBeatmapDecoder(); + using (var resStream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) + using (var stream = new StreamReader(resStream)) + { + var hitObjects = decoder.DecodeBeatmap(stream).HitObjects; + + var curveData = hitObjects[0] as IHasCurve; + var positionData = hitObjects[0] as IHasPosition; + + Assert.IsNotNull(positionData); + Assert.IsNotNull(curveData); + Assert.AreEqual(new Vector2(192, 168), positionData.Position); + Assert.AreEqual(956, hitObjects[0].StartTime); + Assert.IsTrue(hitObjects[0].Samples.Any(s => s.Name == SampleInfo.HIT_NORMAL)); + + positionData = hitObjects[1] as IHasPosition; + + Assert.IsNotNull(positionData); + Assert.AreEqual(new Vector2(304, 56), positionData.Position); + Assert.AreEqual(1285, hitObjects[1].StartTime); + Assert.IsTrue(hitObjects[1].Samples.Any(s => s.Name == SampleInfo.HIT_CLAP)); + } + } + } +} diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs new file mode 100644 index 0000000000..839932c640 --- /dev/null +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -0,0 +1,90 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.IO; +using System.Linq; +using NUnit.Framework; +using OpenTK; +using osu.Framework.Graphics; +using osu.Game.Beatmaps.Formats; +using osu.Game.Storyboards; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Beatmaps.Formats +{ + [TestFixture] + public class LegacyStoryboardDecoderTest + { + [Test] + public void TestDecodeStoryboardEvents() + { + var decoder = new LegacyBeatmapDecoder(); + using (var resStream = Resource.OpenResource("Himeringo - Yotsuya-san ni Yoroshiku (RLC) [Winber1's Extreme].osu")) + using (var stream = new StreamReader(resStream)) + { + var storyboard = decoder.GetStoryboardDecoder().DecodeStoryboard(stream); + + Assert.IsTrue(storyboard.HasDrawable); + Assert.AreEqual(4, storyboard.Layers.Count()); + + StoryboardLayer background = storyboard.Layers.FirstOrDefault(l => l.Depth == 3); + Assert.IsNotNull(background); + Assert.AreEqual(16, background.Elements.Count()); + Assert.IsTrue(background.EnabledWhenFailing); + Assert.IsTrue(background.EnabledWhenPassing); + Assert.AreEqual("Background", background.Name); + + StoryboardLayer fail = storyboard.Layers.FirstOrDefault(l => l.Depth == 2); + Assert.IsNotNull(fail); + Assert.AreEqual(0, fail.Elements.Count()); + Assert.IsTrue(fail.EnabledWhenFailing); + Assert.IsFalse(fail.EnabledWhenPassing); + Assert.AreEqual("Fail", fail.Name); + + StoryboardLayer pass = storyboard.Layers.FirstOrDefault(l => l.Depth == 1); + Assert.IsNotNull(pass); + Assert.AreEqual(0, pass.Elements.Count()); + Assert.IsFalse(pass.EnabledWhenFailing); + Assert.IsTrue(pass.EnabledWhenPassing); + Assert.AreEqual("Pass", pass.Name); + + StoryboardLayer foreground = storyboard.Layers.FirstOrDefault(l => l.Depth == 0); + Assert.IsNotNull(foreground); + Assert.AreEqual(151, foreground.Elements.Count()); + Assert.IsTrue(foreground.EnabledWhenFailing); + Assert.IsTrue(foreground.EnabledWhenPassing); + Assert.AreEqual("Foreground", foreground.Name); + + int spriteCount = background.Elements.Count(x => x.GetType() == typeof(StoryboardSprite)); + int animationCount = background.Elements.Count(x => x.GetType() == typeof(StoryboardAnimation)); + int sampleCount = background.Elements.Count(x => x.GetType() == typeof(StoryboardSample)); + + Assert.AreEqual(15, spriteCount); + Assert.AreEqual(1, animationCount); + Assert.AreEqual(0, sampleCount); + Assert.AreEqual(background.Elements.Count(), spriteCount + animationCount + sampleCount); + + var sprite = background.Elements.ElementAt(0) as StoryboardSprite; + Assert.NotNull(sprite); + Assert.IsTrue(sprite.HasCommands); + Assert.AreEqual(new Vector2(320, 240), sprite.InitialPosition); + Assert.IsTrue(sprite.IsDrawable); + Assert.AreEqual(Anchor.Centre, sprite.Origin); + Assert.AreEqual("SB/lyric/ja-21.png", sprite.Path); + + var animation = background.Elements.ElementAt(12) as StoryboardAnimation; + Assert.NotNull(animation); + Assert.AreEqual(141175, animation.EndTime); + Assert.AreEqual(10, animation.FrameCount); + Assert.AreEqual(30, animation.FrameDelay); + Assert.IsTrue(animation.HasCommands); + Assert.AreEqual(new Vector2(320, 240), animation.InitialPosition); + Assert.IsTrue(animation.IsDrawable); + Assert.AreEqual(AnimationLoopType.LoopForever, animation.LoopType); + Assert.AreEqual(Anchor.Centre, animation.Origin); + Assert.AreEqual("SB/red jitter/red_0000.jpg", animation.Path); + Assert.AreEqual(78993, animation.StartTime); + } + } + } +} diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuLegacyDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuLegacyDecoderTest.cs deleted file mode 100644 index 95b691e07f..0000000000 --- a/osu.Game.Tests/Beatmaps/Formats/OsuLegacyDecoderTest.cs +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System.IO; -using NUnit.Framework; -using OpenTK; -using OpenTK.Graphics; -using osu.Game.Beatmaps.Formats; -using osu.Game.Tests.Resources; -using System.Linq; -using osu.Game.Audio; -using osu.Game.Rulesets.Objects.Types; - -namespace osu.Game.Tests.Beatmaps.Formats -{ - [TestFixture] - public class OsuLegacyDecoderTest - { - [Test] - public void TestDecodeMetadata() - { - var decoder = new OsuLegacyDecoder(); - using (var stream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) - { - var beatmap = decoder.Decode(new StreamReader(stream)); - var meta = beatmap.BeatmapInfo.Metadata; - Assert.AreEqual(241526, meta.OnlineBeatmapSetID); - Assert.AreEqual("Soleily", meta.Artist); - Assert.AreEqual("Soleily", meta.ArtistUnicode); - Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", meta.AudioFile); - Assert.AreEqual("Gamu", meta.AuthorString); - Assert.AreEqual("machinetop_background.jpg", meta.BackgroundFile); - Assert.AreEqual(164471, meta.PreviewTime); - Assert.AreEqual(string.Empty, meta.Source); - Assert.AreEqual("MBC7 Unisphere 地球ヤバイEP Chikyu Yabai", meta.Tags); - Assert.AreEqual("Renatus", meta.Title); - Assert.AreEqual("Renatus", meta.TitleUnicode); - } - } - - [Test] - public void TestDecodeGeneral() - { - var decoder = new OsuLegacyDecoder(); - using (var stream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) - { - var beatmapInfo = decoder.Decode(new StreamReader(stream)).BeatmapInfo; - Assert.AreEqual(0, beatmapInfo.AudioLeadIn); - Assert.AreEqual(false, beatmapInfo.Countdown); - Assert.AreEqual(0.7f, beatmapInfo.StackLeniency); - Assert.AreEqual(false, beatmapInfo.SpecialStyle); - Assert.IsTrue(beatmapInfo.RulesetID == 0); - Assert.AreEqual(false, beatmapInfo.LetterboxInBreaks); - Assert.AreEqual(false, beatmapInfo.WidescreenStoryboard); - } - } - - [Test] - public void TestDecodeEditor() - { - var decoder = new OsuLegacyDecoder(); - using (var stream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) - { - var beatmap = decoder.Decode(new StreamReader(stream)).BeatmapInfo; - int[] expectedBookmarks = - { - 11505, 22054, 32604, 43153, 53703, 64252, 74802, 85351, - 95901, 106450, 116999, 119637, 130186, 140735, 151285, - 161834, 164471, 175020, 185570, 196119, 206669, 209306 - }; - Assert.AreEqual(expectedBookmarks.Length, beatmap.Bookmarks.Length); - for (int i = 0; i < expectedBookmarks.Length; i++) - Assert.AreEqual(expectedBookmarks[i], beatmap.Bookmarks[i]); - Assert.AreEqual(1.8, beatmap.DistanceSpacing); - Assert.AreEqual(4, beatmap.BeatDivisor); - Assert.AreEqual(4, beatmap.GridSize); - Assert.AreEqual(2, beatmap.TimelineZoom); - } - } - - [Test] - public void TestDecodeDifficulty() - { - var decoder = new OsuLegacyDecoder(); - using (var stream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) - { - var beatmap = decoder.Decode(new StreamReader(stream)); - var difficulty = beatmap.BeatmapInfo.BaseDifficulty; - Assert.AreEqual(6.5f, difficulty.DrainRate); - Assert.AreEqual(4, difficulty.CircleSize); - Assert.AreEqual(8, difficulty.OverallDifficulty); - Assert.AreEqual(9, difficulty.ApproachRate); - Assert.AreEqual(1.8f, difficulty.SliderMultiplier); - Assert.AreEqual(2, difficulty.SliderTickRate); - } - } - - [Test] - public void TestDecodeColors() - { - var decoder = new OsuLegacyDecoder(); - using (var stream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) - { - var beatmap = decoder.Decode(new StreamReader(stream)); - Color4[] expected = - { - new Color4(142, 199, 255, 255), - new Color4(255, 128, 128, 255), - new Color4(128, 255, 255, 255), - new Color4(128, 255, 128, 255), - new Color4(255, 187, 255, 255), - new Color4(255, 177, 140, 255), - }; - Assert.AreEqual(expected.Length, beatmap.ComboColors.Count); - for (int i = 0; i < expected.Length; i++) - Assert.AreEqual(expected[i], beatmap.ComboColors[i]); - } - } - - [Test] - public void TestDecodeHitObjects() - { - var decoder = new OsuLegacyDecoder(); - using (var stream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) - { - var beatmap = decoder.Decode(new StreamReader(stream)); - - var curveData = beatmap.HitObjects[0] as IHasCurve; - var positionData = beatmap.HitObjects[0] as IHasPosition; - - Assert.IsNotNull(positionData); - Assert.IsNotNull(curveData); - Assert.AreEqual(new Vector2(192, 168), positionData.Position); - Assert.AreEqual(956, beatmap.HitObjects[0].StartTime); - Assert.IsTrue(beatmap.HitObjects[0].Samples.Any(s => s.Name == SampleInfo.HIT_NORMAL)); - - positionData = beatmap.HitObjects[1] as IHasPosition; - - Assert.IsNotNull(positionData); - Assert.AreEqual(new Vector2(304, 56), positionData.Position); - Assert.AreEqual(1285, beatmap.HitObjects[1].StartTime); - Assert.IsTrue(beatmap.HitObjects[1].Samples.Any(s => s.Name == SampleInfo.HIT_CLAP)); - } - } - } -} diff --git a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs index 12bbde5b57..ffe735c89f 100644 --- a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Beatmaps.IO BeatmapMetadata meta; using (var stream = new StreamReader(reader.GetStream("Soleily - Renatus (Deif) [Platter].osu"))) - meta = BeatmapDecoder.GetDecoder(stream).Decode(stream).Metadata; + meta = Decoder.GetDecoder(stream).DecodeBeatmap(stream).Metadata; Assert.AreEqual(241526, meta.OnlineBeatmapSetID); Assert.AreEqual("Soleily", meta.Artist); diff --git a/osu.Game.Tests/Resources/Himeringo - Yotsuya-san ni Yoroshiku (RLC) [Winber1's Extreme].osu b/osu.Game.Tests/Resources/Himeringo - Yotsuya-san ni Yoroshiku (RLC) [Winber1's Extreme].osu new file mode 100644 index 0000000000..f7b33fa6a8 --- /dev/null +++ b/osu.Game.Tests/Resources/Himeringo - Yotsuya-san ni Yoroshiku (RLC) [Winber1's Extreme].osu @@ -0,0 +1,1997 @@ +osu file format v13 + +[General] +AudioFilename: yotsuya192.mp3 +AudioLeadIn: 0 +PreviewTime: 71220 +Countdown: 0 +SampleSet: Soft +StackLeniency: 0.7 +Mode: 0 +LetterboxInBreaks: 0 +WidescreenStoryboard: 1 + +[Editor] +Bookmarks: 24456,34275,43002,53911,71366,88820,99729,117184,143366,152093,160820,169547 +DistanceSpacing: 1.4 +BeatDivisor: 4 +GridSize: 4 +TimelineZoom: 1 + +[Metadata] +Title:Yotsuya-san ni Yoroshiku +TitleUnicode:四ツ谷さんによろしく +Artist:Himeringo +ArtistUnicode:ひめりんご +Creator:RLC +Version:Winber1's Extreme +Source: +Tags:flask nyquill winber1 skystar amamiya yuko cheesiest onosakihito utaite leave it to eight hatsune miku vocaloid +BeatmapID:378781 +BeatmapSetID:100049 + +[Difficulty] +HPDrainRate:6 +CircleSize:4 +OverallDifficulty:8.5 +ApproachRate:9.5 +SliderMultiplier:1.9 +SliderTickRate:1 + +[Events] +//Background and Video events +0,0,"primary.jpg",0,0 +//Break Periods +//Storyboard Layer 0 (Background) +Sprite,Background,Centre,"SB\lyric\ja-21.png",320,240 + S,0,117175,,0.8 + F,0,117175,117584,0,1 + M,0,117175,118675,192,393,-12,393 + F,0,117584,118402,1 + F,0,118402,118675,1,0 +Sprite,Background,Centre,"SB\lyric\ja-22.png",320,240 + S,0,118675,,0.8 + F,0,118675,119084,0,1 + M,0,118675,119629,150,393,23,393 + F,0,119084,119357,1 + F,0,119357,119629,1,0 +Sprite,Background,Centre,"SB\lyric\ja-22-repeat.png",320,240 + S,0,119357,,0.8 + M,0,119629,,196,393 + F,0,119629,119902,0,1 + M,0,119629,121947,196,393,-58,393 + F,0,119902,121675,1 + F,0,121675,121947,1,0 +Sprite,Background,Centre,"SB\lyric\ja-23.png",320,240 + S,0,121947,,0.8 + F,0,121947,122357,0,1 + M,0,121947,123038,158,393,23,393 + F,0,122357,122766,1 + F,0,122766,123038,1,0 +Sprite,Background,Centre,"SB\lyric\ja-24.png",320,240 + S,0,123038,,0.8 + F,0,123038,123447,0,1 + M,0,123038,123720,249,393,187,393 + F,0,123447,123720,1 +Sprite,Background,Centre,"SB\lyric\en-21.png",320,240 + S,0,117175,,0.6 + F,0,117175,117584,0,1 + M,0,117175,117925,641,425,551,425 + F,0,117584,118402,1 + M,0,117925,118675,551,425,461,425 + F,0,118402,118675,1,0 +Sprite,Background,Centre,"SB\lyric\en-22.png",320,240 + S,0,118675,,0.6 + F,0,118675,119084,0,1 + M,0,118675,119152,616,425,554,425 + F,0,119084,119357,1 + M,0,119152,119629,554,425,489,425 + F,0,119357,119629,1,0 +Sprite,Background,Centre,"SB\lyric\en-22-repeat.png",320,240 + S,0,119629,,0.6 + F,0,119629,120038,0,1 + M,0,119629,121947,683,425,417,425 + F,0,120038,121675,1 + F,0,121675,121947,1,0 +Sprite,Background,Centre,"SB\lyric\en-23.png",320,240 + S,0,121947,,0.6 + F,0,121947,122357,0,1 + M,0,121947,123038,617,425,487,425 + F,0,122357,122766,1 + F,0,122766,123038,1,0 +Sprite,Background,Centre,"SB\lyric\en-24.png",320,240 + S,0,123038,,0.6 + F,0,123038,123447,0,1 + M,0,123038,123720,515,425,446,425 +Sprite,Background,Centre,"SB\lower mask.png",320,240 + S,0,117175,,0.625 + R,0,117175,,0.0001 + M,0,117175,123720,320,330 +Sprite,Background,Centre,"SB\black.jpg",320,240 + F,0,-97,,1 + F,0,20084,23357,1,0 + F,0,23357,24447,0 + F,0,24447,25470,0,1 + F,0,25538,26629,0,1 + F,0,27720,28811,0.5,1 + F,0,29902,30993,0,1 + F,0,32084,33175,0.5,1 + F,0,34266,,0 + F,0,70266,,1 + S,0,70266,,1.267815 + R,0,70266,,0.0001 + F,0,71357,,0 + F,0,71357,,0 + F,0,86629,,1 + F,0,87720,88538,1,0 + F,0,88538,,0 + M,0,108877,116493,314,289,312,322 + F,0,116084,,1 + F,0,117175,117720,0,0.5 + F,0,117720,118266,0.5,0 + F,0,118266,118811,0,0.5 + F,0,118811,119357,0.5,0 + F,0,119357,119902,0,0.5 + F,0,119902,120447,0.5,0 + F,0,120447,120993,0,0.5 + F,0,120993,121538,0.5,0 + F,0,121538,122084,0,0.5 + F,0,122084,122629,0.5,0 + F,0,122629,123175,0,0.5 + F,0,123175,123686,0.5,0 + F,0,123720,,1 + F,0,126175,,1,0.07840011 + F,0,141175,,1 + F,0,143357,144447,0,1 + F,0,145538,146629,0.5,1 + F,0,147720,148811,0,1 + F,0,149902,150993,0.5,1 + F,0,150993,,1 + F,0,152084,,0 + F,0,171447,,1 + F,0,171447,177720,1 +Animation,Background,Centre,"SB\red jitter\red_0000.jpg",320,240,10,30,LoopForever + S,0,78993,,1.001 + R,0,78993,,0.0001 + F,0,78993,80084,0,1 + F,0,80084,,1 + F,0,86629,,0 + F,0,133538,134629,0.9180929,1 + F,0,141175,,1 +Sprite,Background,Centre,"SB\brown.jpg",320,240 + M,0,71357,,320,240 + S,0,71357,,1.001 + R,0,71357,,0.0001 + F,0,71357,80084,1 + F,0,80084,81175,1,0 +Sprite,Background,Centre,"SB\blue.jpg",320,240 + S,0,126175,,1.001 + R,0,126175,,0.0001 + F,0,126175,134629,1 + F,0,134629,135720,1,0 +Sprite,Background,Centre,"SB\cloud2.png",320,240 + S,0,126175,,4.659974 + R,0,126175,,0.0001 + M,0,126175,135175,72,29,474,29 + F,0,133538,135175,0.4690581,0 +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +Sprite,Foreground,Centre,"SB\lyric\ja-1.png",320,240 + S,0,53902,,0.8 + F,0,53902,54993,0,1 + M,0,53902,58266,232,393,263,393 + F,0,54993,57175,1 + F,0,57175,58266,1,0 +Sprite,Foreground,Centre,"SB\lyric\en-1.png",320,240 + S,0,53902,,0.6 + F,0,53902,54993,0,1 + M,0,53902,58266,404,425,373,425 + F,0,54993,57175,1 + F,0,57175,58266,1,0 +Sprite,Foreground,Centre,"SB\lyric\ja-2.png",320,240 + S,0,58266,,0.8 + F,0,58266,59357,0,1 + M,0,58266,62629,232,393,263,393 + F,0,59357,61538,1 + F,0,61538,,1 + F,0,61538,,1 + F,0,61538,62357,1,0 +Sprite,Foreground,Centre,"SB\lyric\ja-3.png",320,240 + S,0,62629,,0.8 + F,0,62629,63720,0,1 + M,0,62629,66447,232,393,263,393 + F,0,63720,65357,1 + F,0,65357,66447,1,0 +Sprite,Foreground,Centre,"SB\lyric\ja-4.png",320,240 + S,0,66447,,0.8 + F,0,66447,67538,0,1 + M,0,66447,70266,232,393,263,393 + F,0,67538,69175,1 + F,0,69175,70266,1,0 +Sprite,Foreground,Centre,"SB\lyric\ja-5.png",320,240 + S,0,70266,,0.5 + M,0,70266,70402,179,335 +Sprite,Foreground,Centre,"SB\lyric\ja-5.png",320,240 + S,0,70402,,0.8 + M,0,70402,70538,469,297 +Sprite,Foreground,Centre,"SB\lyric\ja-5.png",320,240 + S,0,70538,,1.1 + M,0,70538,70675,255,158 +Sprite,Foreground,Centre,"SB\lyric\ja-5RED.png",320,240 + S,0,70675,,1.3 + F,0,70675,70947,1 + M,0,70675,71220,320,240 + F,0,70947,71357,1,0 +Sprite,Foreground,Centre,"SB\lyric\ja-6-1.png",320,240 + S,0,71357,,0.8 + M,0,71357,71902,320,393 +Sprite,Foreground,Centre,"SB\lyric\ja-6-2.png",320,240 + S,0,71902,,0.8 + M,0,71902,72447,320,393 +Sprite,Foreground,Centre,"SB\lyric\ja-6-3.png",320,240 + S,0,72447,,0.8 + M,0,72447,72857,320,393 +Sprite,Foreground,Centre,"SB\lyric\ja-7.png",320,240 + S,0,72857,,0.8 + M,0,72857,73947,320,393 +Sprite,Foreground,Centre,"SB\lyric\ja-8.png",320,240 + S,0,73947,,0.8 + M,0,73947,76129,320,393 +Sprite,Foreground,Centre,"SB\lyric\ja-9.png",320,240 + S,0,76129,,0.8 + M,0,76129,77220,320,393 +Sprite,Foreground,Centre,"SB\lyric\ja-10-2.png",320,240 + S,0,78311,,0.8 + M,0,78311,80084,376,393 +Sprite,Foreground,Centre,"SB\lyric\ja-10-1.png",320,240 + S,0,77220,,0.8 + M,0,77220,80084,264,393 +Animation,Foreground,Centre,"SB\lyric\11-1\ja-11-1_0000.png",320,240,10,30,LoopForever + S,0,80084,,0.8 + M,0,80084,81584,230,326 +Animation,Foreground,Centre,"SB\lyric\11-2\ja-11-2_0000.png",320,240,10,30,LoopForever + S,0,80629,,1.1 + M,0,80629,81584,325,305 +Animation,Foreground,Centre,"SB\lyric\11-3\ja-11-3_0000.png",320,240,10,30,LoopForever + S,0,81175,,1.3 + M,0,81175,81584,425,284 +Animation,Foreground,Centre,"SB\lyric\12\ja-12_0000.png",320,240,10,30,LoopForever + S,0,81584,,1.1 + M,0,81584,82675,306,214 +Animation,Foreground,Centre,"SB\lyric\13\ja-13_0000.png",320,240,10,30,LoopForever + S,0,82675,,0.8 + M,0,82675,84857,315,294 +Animation,Foreground,Centre,"SB\lyric\14\ja-14_0000.png",320,240,10,30,LoopForever + S,0,84857,,1.1 + M,0,84857,85947,320,222 +Animation,Foreground,Centre,"SB\lyric\15-1\ja-15-1_0000.png",320,240,10,30,LoopForever + S,0,85947,,0.8 + M,0,85947,86629,269,393 +Sprite,Foreground,Centre,"SB\lyric\ja-15-1.png",320,240 + S,0,86629,,0.8 + M,0,86629,87720,269,393 + F,0,87720,88538,1,0 +Sprite,Foreground,Centre,"SB\lyric\ja-15-2.png",320,240 + S,0,87175,,0.8 + M,0,87175,87720,373,393 + F,0,87720,88538,1,0 +Sprite,Foreground,Centre,"SB\lyric\ja-16.png",320,240 + S,0,99720,,0.8 + F,0,99720,100811,0,1 + M,0,99720,104084,232,393,263,393 + F,0,100811,102993,1 + F,0,102993,104084,1,0 +Sprite,Foreground,Centre,"SB\lyric\ja-17.png",320,240 + S,0,104084,,0.8 + F,0,104084,105175,0,1 + M,0,104084,108447,232,393,263,393 + F,0,105175,107357,1 + F,0,107357,108175,1,0 +Sprite,Foreground,Centre,"SB\lyric\ja-18.png",320,240 + S,0,108447,,0.8 + F,0,108447,109538,0,1 + M,0,108447,112266,232,393,263,393 + F,0,109538,111175,1 + F,0,111175,112266,1,0 +Sprite,Foreground,Centre,"SB\lyric\ja-19.png",320,240 + S,0,112266,,0.8 + F,0,112266,113357,0,1 + M,0,112266,116084,232,393,263,393 + F,0,113357,114993,1 + F,0,114993,116084,1,0 +Sprite,Foreground,Centre,"SB\lyric\ja-20_1.png",320,240 + S,0,116084,,0.5 + M,0,116084,116220,478,319 +Sprite,Foreground,Centre,"SB\lyric\ja-20_1.png",320,240 + S,0,116220,,0.8 + M,0,116220,116357,160,257 +Sprite,Foreground,Centre,"SB\lyric\ja-20_1.png",320,240 + S,0,116357,,1.1 + M,0,116357,116493,393,161 +Sprite,Foreground,Centre,"SB\lyric\ja-20RED.png",320,240 + M,0,116493,,320,240 + S,0,116493,,1.3 + F,0,116493,116766,1 + F,0,116766,117175,1,0 +Sprite,Foreground,Centre,"SB\lyric\ja-24RED.png",320,240 + M,0,123720,,187,393 + S,0,123720,,0.8 + F,0,123720,124811,1 + F,0,124811,125902,1,0 +Sprite,Foreground,Centre,"SB\lyric\ja-25-1.png",320,240 + S,0,125766,,0.8 + M,0,125766,126175,156,312 +Sprite,Foreground,Centre,"SB\lyric\ja-25-2.png",320,240 + S,0,126175,,0.8 + M,0,126175,126993,156,312 + F,0,127538,127811,1,0 +Sprite,Foreground,Centre,"SB\lyric\ja-25-3.png",320,240 + S,0,126447,,1.1 + R,0,126447,,-0.189046 + M,0,126447,126993,306,288 + F,0,127538,,1 + F,0,127538,127811,1,0 +Sprite,Foreground,Centre,"SB\lyric\ja-25-4.png",320,240 + M,0,126993,,388,400 + S,0,126993,,1.3 + R,0,126993,,0.1785437 + F,0,127538,127811,1,0 +Sprite,Foreground,Centre,"SB\lyric\ja-26.png",320,240 + S,0,127402,,0.8 + F,0,127402,127538,0,1 + M,0,127402,128084,315,201 + R,0,127402,128084,0.1050253 + F,0,127538,128084,1 + M,0,128084,128493,315,201,315,236 + F,0,128084,128493,1,0 + R,0,128084,128493,0.1050253,0.3360815 +Sprite,Foreground,Centre,"SB\lyric\ja-27.png",320,240 + S,0,128493,,0.9 + F,0,128493,128629,0,1 + M,0,128493,130266,191,426 + R,0,128493,130266,0.06301453 + F,0,128629,130266,1 + M,0,130266,130675,191,426,191,458 + F,0,130266,130675,1,0 + R,0,130266,130675,0.06301453,0.2835687 +Sprite,Foreground,Centre,"SB\lyric\ja-28.png",320,240 + S,0,130675,,0.8 + R,0,130675,,-0.1155283 + F,0,130675,130811,0,1 + M,0,130675,131357,320,240 + F,0,130811,131357,1 + F,0,131357,131766,1,0 +Sprite,Foreground,Centre,"SB\lyric\ja-29.png",320,240 + S,0,131766,,0.8 + F,0,131766,131902,0,1 + M,0,131766,133538,230,154 + R,0,131766,133538,0.1260309 + F,0,131902,133538,1 + M,0,133538,134629,230,154,230,206 + F,0,133538,134629,1,0 + R,0,133538,134629,0.1260309,0.2835693 +Animation,Foreground,Centre,"SB\lyric\12\ja-12_0000.png",320,240,10,30,LoopForever + S,0,136129,,1.1 + M,0,136129,137220,306,214 +Animation,Foreground,Centre,"SB\lyric\13\ja-13_0000.png",320,240,10,30,LoopForever + S,0,137220,,0.8 + M,0,137220,139402,315,294 +Animation,Foreground,Centre,"SB\lyric\14\ja-14_0000.png",320,240,10,30,LoopForever + S,0,139402,,1.1 + M,0,139402,140493,320,222 +Animation,Foreground,Centre,"SB\lyric\30-1\ja-30-1_0000.png",320,240,10,30,LoopForever + S,0,134629,,0.8 + M,0,134629,136129,153,292 +Animation,Foreground,Centre,"SB\lyric\30-2\ja-30-2_0000.png",320,240,10,30,LoopForever + S,0,135175,,1.1 + M,0,135175,136129,333,216 +Animation,Foreground,Centre,"SB\lyric\30-3\ja-30-3_0000.png",320,240,10,30,LoopForever + S,0,135720,,1.3 + M,0,135720,136129,485,133 +Animation,Foreground,Centre,"SB\lyric\34-1\ja-34-1_0000.png",320,240,10,30,LoopForever + S,0,140493,,0.8 + M,0,140493,141175,269,393 +Sprite,Foreground,Centre,"SB\lyric\ja-34-1.png",320,240 + S,0,141175,,0.8 + M,0,141175,143357,269,393 + F,0,142266,143357,1,0 +Sprite,Foreground,Centre,"SB\lyric\ja-34-2.png",320,240 + S,0,141720,,0.8 + M,0,141720,143357,391,393 + F,0,142266,143357,1,0 +Sprite,Foreground,Centre,"SB\lyric\en-2.png",320,240 + S,0,58266,,0.6 + F,0,58266,59357,0,1 + M,0,58266,62629,404,425,373,425 + F,0,59357,61538,1 + F,0,61538,62357,1,0 +Sprite,Foreground,Centre,"SB\lyric\en-3.png",320,240 + S,0,62629,,0.6 + F,0,62629,63720,0,1 + M,0,62629,66447,404,425,373,425 + F,0,63720,65357,1 + F,0,65357,66447,1,0 +Sprite,Foreground,Centre,"SB\lyric\en-4.png",320,240 + S,0,66447,,0.6 + F,0,66447,67538,0,1 + M,0,66447,70266,404,425,373,425 + F,0,67538,69175,1 + F,0,69175,70266,1,0 +Sprite,Foreground,Centre,"SB\lyric\en-5.png",320,240 + F,0,70266,,1 + S,0,70266,,0.4 + M,0,70266,70402,436,192 +Sprite,Foreground,Centre,"SB\lyric\en-5.png",320,240 + S,0,70402,,0.6 + M,0,70402,70538,181,97 +Sprite,Foreground,Centre,"SB\lyric\en-5.png",320,240 + S,0,70538,,0.8 + M,0,70538,70675,391,392 +Sprite,Foreground,Centre,"SB\lyric\en-5RED.png",320,240 + M,0,70675,,320,294 + F,0,70675,70947,1 + F,0,70947,71357,1,0 +Sprite,Foreground,Centre,"SB\lyric\en-6-1.png",320,240 + S,0,71357,,0.7885935 + M,0,71357,71902,320,425 +Sprite,Foreground,Centre,"SB\lyric\en-6-2.png",320,240 + S,0,71902,,0.7357419 + M,0,71902,72447,320,425 +Sprite,Foreground,Centre,"SB\lyric\en-6-3.png",320,240 + S,0,72447,,0.8084131 + M,0,72447,72857,320,425 +Sprite,Foreground,Centre,"SB\lyric\en-7.png",320,240 + S,0,72857,,0.8018063 + M,0,72857,73947,320,425 +Sprite,Foreground,Centre,"SB\lyric\en-8.png",320,240 + S,0,73947,,0.6961035 + M,0,73947,76129,320,425 +Sprite,Foreground,Centre,"SB\lyric\en-9.png",320,240 + S,0,76129,,0.762168 + M,0,76129,77220,320,425 +Sprite,Foreground,Centre,"SB\lyric\en-10-1.png",320,240 + S,0,77220,,0.6564645 + M,0,77220,80084,262,425 +Sprite,Foreground,Centre,"SB\lyric\en-10-2.png",320,240 + S,0,78311,,0.5837936 + M,0,78311,80084,411,425 +Sprite,Foreground,Centre,"SB\lyric\en-16.png",320,240 + S,0,99720,,0.6 + F,0,99720,100811,0,1 + M,0,99720,104084,404,425,373,425 + F,0,100811,102993,1 + F,0,102993,104084,1,0 +Sprite,Foreground,Centre,"SB\lyric\en-17-1.png",320,240 + S,0,104084,,0.6 + F,0,104084,105175,0,1 + M,0,104084,108447,404,425,373,425 + F,0,105175,107357,1 + F,0,107357,108175,1,0 +Sprite,Foreground,Centre,"SB\lyric\en-17-2.png",320,240 + S,0,104084,,0.6 + F,0,104084,105175,0,1 + M,0,104084,108175,232,457,263,457 + F,0,105175,107357,1 + F,0,107357,108175,1,0 +Sprite,Foreground,Centre,"SB\lyric\en-18.png",320,240 + S,0,108447,,0.6 + F,0,108447,109538,0,1 + M,0,108447,112266,404,425,373,425 + F,0,109538,111175,1 + F,0,111175,112266,1,0 +Sprite,Foreground,Centre,"SB\lyric\en-19.png",320,240 + S,0,112266,,0.6 + F,0,112266,113357,0,1 + M,0,112266,116084,404,425,373,425 + F,0,113357,114993,1 + F,0,114993,116084,1,0 +Sprite,Foreground,Centre,"SB\lyric\en-20-1.png",320,240 + S,0,116084,,0.4 + M,0,116084,116220,205,167 +Sprite,Foreground,Centre,"SB\lyric\en-20-1.png",320,240 + S,0,116220,,0.6 + M,0,116220,116357,389,252 +Sprite,Foreground,Centre,"SB\lyric\en-20-1.png",320,240 + S,0,116357,,0.8 + M,0,116357,116493,220,395 +Sprite,Foreground,Centre,"SB\lyric\en-20RED.png",320,240 + M,0,116493,,320,293 + F,0,116493,116766,1 + F,0,116766,117175,1,0 +Sprite,Foreground,Centre,"SB\lyric\en-24RED.png",320,240 + M,0,123720,,446,425 + S,0,123720,,0.6 + F,0,123720,124811,1 + F,0,124811,125902,1,0 +Sprite,Foreground,Centre,"SB\lyric\en-25-1.png",320,240 + S,0,125766,,0.6 + M,0,125766,126175,408,240 +Sprite,Foreground,Centre,"SB\lyric\en-25-2.png",320,240 + S,0,126175,,0.6 + M,0,126175,126993,408,240 + F,0,127538,127811,1,0 +Sprite,Foreground,Centre,"SB\lyric\en-25-3.png",320,240 + M,0,126447,,525,277 + S,0,126447,,0.8 + R,0,126447,,0.189046 + F,0,127538,127811,1,0 +Sprite,Foreground,Centre,"SB\lyric\en-25-4.png",320,240 + M,0,126993,,219,400 + S,0,126993,,1 + R,0,126993,,-0.1260309 + F,0,126993,127402,1,0 +Sprite,Foreground,Centre,"SB\lyric\en-26.png",320,240 + S,0,127402,,0.7 + F,0,127402,127538,0,1 + M,0,127402,128084,416,325 + R,0,127402,128084,-0.1680408 + F,0,127538,128084,1 + M,0,128084,128493,416,325,416,391 + F,0,128084,128493,1,0 + R,0,128084,128493,-0.1680408,-0.3990967 +Sprite,Foreground,Centre,"SB\lyric\en-27.png",320,240 + S,0,128493,,0.6 + F,0,128493,128629,0,1 + M,0,128493,130266,443,226 + R,0,128493,130266,-0.08402039 + F,0,128629,130266,1 + M,0,130266,130675,443,226,443,252 + F,0,130266,130675,1,0 + R,0,130266,130675,-0.08402039,-0.2310565 +Sprite,Foreground,Centre,"SB\lyric\en-28.png",320,240 + S,0,130675,,0.7 + R,0,130675,,0.273067 + F,0,130675,130811,0,1 + M,0,130675,131357,140,303 + F,0,130811,131357,1 + F,0,131357,131766,1,0 +Sprite,Foreground,Centre,"SB\lyric\en-29.png",320,240 + S,0,131766,,0.7 + F,0,131766,131902,0,1 + M,0,131766,133538,438,257 + R,0,131766,133538,-0.1890472 + F,0,131902,133538,1 + M,0,133538,134629,438,257,438,314 + F,0,133538,134629,1,0 + R,0,133538,134629,-0.1890472,-0.4306067 +Animation,Foreground,Centre,"SB\lyric\11-1\en-11-1_0000.png",320,240,10,30,LoopForever + S,0,80357,,0.6 + M,0,80357,81584,336,384 +Animation,Foreground,Centre,"SB\lyric\11-2\en-11-2_0000.png",320,240,10,30,LoopForever + S,0,80902,,0.8 + M,0,80902,81993,142,245 +Animation,Foreground,Centre,"SB\lyric\11-3\en-11-3_0000.png",320,240,10,30,LoopForever + S,0,81447,,1 + M,0,81447,81993,413,164 +Animation,Foreground,Centre,"SB\lyric\12\en-12_0000.png",320,240,10,30,LoopForever + S,0,81993,,0.8 + M,0,81993,83357,255,360 +Animation,Foreground,Centre,"SB\lyric\13\en-13_0000.png",320,240,10,30,LoopForever + S,0,83357,,0.6 + M,0,83357,85538,222,140 +Animation,Foreground,Centre,"SB\lyric\14\en-14_0000.png",320,240,10,30,LoopForever + S,0,85538,,1 + M,0,85538,86357,437,300 +Animation,Foreground,Centre,"SB\lyric\15-1\en-15-1_0000.png",320,240,10,30,LoopForever + F,0,86357,,1 + S,0,86357,,0.6564642 + M,0,86357,86629,237,430 +Sprite,Foreground,Centre,"SB\lyric\en-15-1.png",320,240 + S,0,86629,,0.6630707 + M,0,86629,87720,234,430 + F,0,86629,87720,1 + F,0,87720,88538,1,0 +Sprite,Foreground,Centre,"SB\lyric\en-15-2.png",320,240 + S,0,87175,,0.7423482 + M,0,87175,87720,417,430 + F,0,87175,87720,1 + F,0,87720,88538,1,0 +Animation,Foreground,Centre,"SB\lyric\30-1\en-30-1_0000.png",320,240,10,30,LoopForever + F,0,134902,,1 + S,0,134902,,0.6 + M,0,134902,136129,419,374 +Animation,Foreground,Centre,"SB\lyric\30-2\en-30-2_0000.png",320,240,10,30,LoopForever + S,0,135447,,0.6 + M,0,135447,136811,156,164 +Animation,Foreground,Centre,"SB\lyric\30-3\en-30-3_0000.png",320,240,10,30,LoopForever + S,0,135993,,0.6 + M,0,135993,136811,128,404 +Animation,Foreground,Centre,"SB\lyric\12\en-12_0000.png",320,240,10,30,LoopForever + S,0,136811,,0.8 + M,0,136811,137902,439,391 +Animation,Foreground,Centre,"SB\lyric\13\en-13_0000.png",320,240,10,30,LoopForever + S,0,137902,,0.6 + M,0,137902,140084,450,142 +Animation,Foreground,Centre,"SB\lyric\14\en-14_0000.png",320,240,10,30,LoopForever + S,0,140084,,1 + M,0,140084,140902,191,294 +Animation,Foreground,Centre,"SB\lyric\34-1\en-34-1_0000.png",320,240,10,30,LoopForever + S,0,140902,,0.6 + M,0,140902,141175,286,425 +Sprite,Foreground,Centre,"SB\lyric\en-34-1-1.png",320,240 + S,0,141175,,0.6 + M,0,141175,142266,286,425 + F,0,142266,143357,1,0 +Sprite,Foreground,Centre,"SB\lyric\en-34-1-2.png",320,240 + S,0,141175,,0.6 + M,0,141175,142266,286,425 + F,0,142266,143357,1,0 +Sprite,Foreground,Centre,"SB\lyric\en-34-2.png",320,240 + S,0,141720,,0.6 + M,0,141720,142266,451,425 + F,0,142266,143357,1,0 +Sprite,Foreground,Centre,"SB\lyric\y1\intro-ja-1.png",320,240 + S,0,25538,,0.6 + M,0,25538,27720,50,240 + M,0,27720,27857,50,240,50,247 + F,0,27720,28538,1,0 + R,0,27720,28538,0,0.3567487 + M,0,27857,27993,50,247,50,263 + M,0,27993,28129,50,263,50,290 + M,0,28129,28266,50,290,50,321 + M,0,28266,28402,50,321,50,360 + M,0,28402,28538,50,360,50,408 +Sprite,Foreground,Centre,"SB\lyric\y1\intro-ja-2.png",320,240 + S,0,25538,,0.6 + M,0,25538,28947,104,240 + M,0,28947,29084,104,240,104,248 + F,0,28947,29766,1,0 + R,0,28947,29766,0,-0.4095996 + M,0,29084,29220,104,248,104,263 + M,0,29220,29357,104,263,104,287 + M,0,29357,29493,104,287,104,320 + M,0,29493,29629,104,320,104,360 + M,0,29629,29766,104,360,104,409 +Sprite,Foreground,Centre,"SB\lyric\y1\intro-ja-3.png",320,240 + S,0,25538,,0.6 + M,0,25538,28129,158,240 + M,0,28129,28266,158,240,158,249 + F,0,28129,28947,1,0 + R,0,28129,28947,0,-0.2114062 + M,0,28266,28402,158,249,158,263 + M,0,28402,28538,158,263,158,288 + M,0,28538,28675,158,288,158,321 + M,0,28675,28811,158,321,158,360 + M,0,28811,28947,158,360,158,409 +Sprite,Foreground,Centre,"SB\lyric\y1\intro-ja-4.png",320,240 + S,0,25538,,0.6 + M,0,25538,28811,212,240 + M,0,28811,28947,212,240,212,248 + F,0,28811,29629,1,0 + R,0,28811,29629,0,0.4624512 + M,0,28947,29084,212,248,212,263 + M,0,29084,29220,212,263,212,288 + M,0,29220,29357,212,288,212,320 + M,0,29357,29493,212,320,212,360 + M,0,29493,29629,212,360,212,407 +Sprite,Foreground,Centre,"SB\lyric\y1\intro-ja-5.png",320,240 + S,0,25538,,0.6 + M,0,25538,28538,266,240 + M,0,28538,28675,266,240,266,248 + F,0,28538,29357,1,0 + R,0,28538,29357,0,0.2906836 + M,0,28675,28811,266,248,266,264 + M,0,28811,28947,266,264,266,288 + M,0,28947,29084,266,288,266,321 + M,0,29084,29220,266,321,266,358 + M,0,29220,29357,266,358,266,407 +Sprite,Foreground,Centre,"SB\lyric\y1\intro-ja-6.png",320,240 + S,0,25538,,0.6 + M,0,25538,27993,320,240 + M,0,27993,28129,320,240,320,247 + F,0,27993,28811,1,0 + R,0,27993,28811,0,0.2906836 + M,0,28129,28266,320,247,320,263 + M,0,28266,28402,320,263,320,288 + M,0,28402,28538,320,288,320,321 + M,0,28538,28675,320,321,320,360 + M,0,28675,28811,320,360,320,408 +Sprite,Foreground,Centre,"SB\lyric\y1\intro-ja-7.png",320,240 + S,0,25538,,0.6 + M,0,25538,28266,374,240 + M,0,28266,28402,374,240,374,248 + F,0,28266,29084,1,0 + R,0,28266,29084,0,0.1717682 + M,0,28402,28538,374,248,374,264 + M,0,28538,28675,374,264,374,288 + M,0,28675,28811,374,288,374,320 + M,0,28811,28947,374,320,374,359 + M,0,28947,29084,374,359,374,409 +Sprite,Foreground,Centre,"SB\lyric\y1\intro-ja-8.png",320,240 + S,0,25538,,0.6 + M,0,25538,27857,428,240 + M,0,27857,27993,428,240,428,249 + F,0,27857,28675,1,0 + R,0,27857,28675,0,-0.237832 + M,0,27993,28129,428,249,428,263 + M,0,28129,28266,428,263,428,288 + M,0,28266,28402,428,288,428,320 + M,0,28402,28538,428,320,428,358 + M,0,28538,28675,428,358,428,408 +Sprite,Foreground,Centre,"SB\lyric\y1\intro-ja-9.png",320,240 + S,0,25538,,0.6 + M,0,25538,28675,482,240 + M,0,28675,28811,482,240,482,247 + F,0,28675,29493,1,0 + R,0,28675,29493,0,-0.5020905 + M,0,28811,28947,482,247,482,263 + M,0,28947,29084,482,263,482,287 + M,0,29084,29220,482,287,482,320 + M,0,29220,29357,482,320,482,360 + M,0,29357,29493,482,360,482,407 +Sprite,Foreground,Centre,"SB\lyric\y1\intro-ja-10-1.png",320,240 + S,0,25538,,0.6 + M,0,25538,28402,536,240 + M,0,28402,28538,536,240,536,247 + F,0,28402,29220,1,0 + R,0,28402,29220,0,-0.3038965 + M,0,28538,28675,536,247,536,263 + M,0,28675,28811,536,263,536,289 + M,0,28811,28947,536,289,536,321 + M,0,28947,29084,536,321,536,360 + M,0,29084,29220,536,360,536,407 +Sprite,Foreground,Centre,"SB\lyric\y1\intro-ja-11.png",320,240 + S,0,25538,,0.6 + M,0,25538,29084,590,240 + F,0,28947,29902,1,0 + R,0,28947,29902,0,0.6474323 + M,0,29084,29220,590,240,590,247 + M,0,29220,29357,590,247,590,263 + M,0,29357,29493,590,263,590,288 + M,0,29493,29629,590,288,590,320 + M,0,29629,29766,590,320,590,361 + M,0,29766,29902,590,361,590,408 +Sprite,Foreground,Centre,"SB\lyric\y2\introSECOND-ja-1.png",320,240 + S,0,29902,,0.6 + M,0,29902,32084,50,240 + M,0,32084,32221,50,240,50,247 + F,0,32084,32902,1,0 + R,0,32084,32902,0,0.4 + M,0,32221,32357,50,247,50,263 + M,0,32357,32493,50,263,50,290 + M,0,32493,32630,50,290,50,321 + M,0,32630,32766,50,321,50,360 + M,0,32766,32902,50,360,50,408 +Sprite,Foreground,Centre,"SB\lyric\y2\introSECOND-ja-2.png",320,240 + S,0,29902,,0.6 + M,0,29902,33311,104,240 + M,0,33311,33448,104,240,104,248 + F,0,33311,34130,1,0 + R,0,33311,34130,0,-0.4 + M,0,33448,33584,104,248,104,263 + M,0,33584,33721,104,263,104,287 + M,0,33721,33857,104,287,104,320 + M,0,33857,33993,104,320,104,360 + M,0,33993,34130,104,360,104,409 +Sprite,Foreground,Centre,"SB\lyric\y2\introSECOND-ja-3.png",320,240 + S,0,29902,,0.6 + M,0,29902,32493,158,240 + M,0,32493,32630,158,240,158,249 + F,0,32493,33311,1,0 + R,0,32493,33311,0,-0.4 + M,0,32630,32766,158,249,158,263 + M,0,32766,32902,158,263,158,288 + M,0,32902,33039,158,288,158,321 + M,0,33039,33175,158,321,158,360 + M,0,33175,33311,158,360,158,409 +Sprite,Foreground,Centre,"SB\lyric\y2\introSECOND-ja-4.png",320,240 + S,0,29902,,0.6 + M,0,29902,33175,212,240 + M,0,33175,33311,212,240,212,248 + F,0,33175,33993,1,0 + R,0,33175,33993,0,0.4 + M,0,33311,33448,212,248,212,263 + M,0,33448,33584,212,263,212,288 + M,0,33584,33721,212,288,212,320 + M,0,33721,33857,212,320,212,360 + M,0,33857,33993,212,360,212,407 +Sprite,Foreground,Centre,"SB\lyric\y2\introSECOND-ja-5.png",320,240 + S,0,29902,,0.6 + M,0,29902,32902,266,240 + M,0,32902,33039,266,240,266,248 + F,0,32902,33721,1,0 + R,0,32902,33721,0,0.4 + M,0,33039,33175,266,248,266,264 + M,0,33175,33311,266,264,266,288 + M,0,33311,33448,266,288,266,321 + M,0,33448,33584,266,321,266,358 + M,0,33584,33721,266,358,266,407 +Sprite,Foreground,Centre,"SB\lyric\y2\introSECOND-ja-6.png",320,240 + S,0,29902,,0.6 + M,0,29902,32357,320,240 + M,0,32357,32493,320,240,320,247 + F,0,32357,33175,1,0 + R,0,32357,33175,0,0.4 + M,0,32493,32630,320,247,320,263 + M,0,32630,32766,320,263,320,288 + M,0,32766,32902,320,288,320,321 + M,0,32902,33039,320,321,320,360 + M,0,33039,33175,320,360,320,408 +Sprite,Foreground,Centre,"SB\lyric\y2\introSECOND-ja-7.png",320,240 + S,0,29902,,0.6 + M,0,29902,32630,374,240 + M,0,32630,32766,374,240,374,248 + F,0,32630,33448,1,0 + R,0,32630,33448,0,0.4 + M,0,32766,32902,374,248,374,264 + M,0,32902,33039,374,264,374,288 + M,0,33039,33175,374,288,374,320 + M,0,33175,33311,374,320,374,359 + M,0,33311,33448,374,359,374,409 +Sprite,Foreground,Centre,"SB\lyric\y2\introSECOND-ja-8.png",320,240 + S,0,29902,,0.6 + M,0,29902,32221,428,240 + M,0,32221,32357,428,240,428,249 + F,0,32221,33039,1,0 + R,0,32221,33039,0,-0.4 + M,0,32357,32493,428,249,428,263 + M,0,32493,32630,428,263,428,288 + M,0,32630,32766,428,288,428,320 + M,0,32766,32902,428,320,428,358 + M,0,32902,33039,428,358,428,408 +Sprite,Foreground,Centre,"SB\lyric\y2\introSECOND-ja-9.png",320,240 + S,0,29902,,0.6 + M,0,29902,33039,482,240 + M,0,33039,33175,482,240,482,247 + F,0,33039,33857,1,0 + R,0,33039,33857,0,-0.4 + M,0,33175,33311,482,247,482,263 + M,0,33311,33448,482,263,482,287 + M,0,33448,33584,482,287,482,320 + M,0,33584,33721,482,320,482,360 + M,0,33721,33857,482,360,482,407 +Sprite,Foreground,Centre,"SB\lyric\y2\introSECOND-ja-10-1.png",320,240 + S,0,29902,,0.6 + M,0,29902,32766,536,240 + M,0,32766,32902,536,240,536,247 + F,0,32766,33584,1,0 + R,0,32766,33584,0,-0.4 + M,0,32902,33039,536,247,536,263 + M,0,33039,33175,536,263,536,289 + M,0,33175,33311,536,289,536,321 + M,0,33311,33448,536,321,536,360 + M,0,33448,33584,536,360,536,407 +Sprite,Foreground,Centre,"SB\lyric\y2\introSECOND-ja-11.png",320,240 + S,0,29902,,0.6 + M,0,29902,33448,590,240 + F,0,33311,34266,1,0 + R,0,33311,34266,0,0.4 + M,0,33448,33584,590,240,590,247 + M,0,33584,33721,590,247,590,263 + M,0,33721,33857,590,263,590,288 + M,0,33857,33993,590,288,590,320 + M,0,33993,34130,590,320,590,361 + M,0,34130,34266,590,361,590,408 +Sprite,Foreground,Centre,"SB\lyric\y2\introSECOND-ja-1.png",320,240 + S,0,143357,,0.6 + M,0,143357,145539,50,240 + M,0,145539,145676,50,240,50,247 + F,0,145539,146357,1,0 + R,0,145539,146357,0,0.4 + M,0,145676,145812,50,247,50,263 + M,0,145812,145948,50,263,50,290 + M,0,145948,146085,50,290,50,321 + M,0,146085,146221,50,321,50,360 + M,0,146221,146357,50,360,50,408 +Sprite,Foreground,Centre,"SB\lyric\y2\introSECOND-ja-2.png",320,240 + S,0,143357,,0.6 + M,0,143357,146766,104,240 + M,0,146766,146903,104,240,104,248 + F,0,146766,147585,1,0 + R,0,146766,147585,0,-0.4 + M,0,146903,147039,104,248,104,263 + M,0,147039,147176,104,263,104,287 + M,0,147176,147312,104,287,104,320 + M,0,147312,147448,104,320,104,360 + M,0,147448,147585,104,360,104,409 +Sprite,Foreground,Centre,"SB\lyric\y2\introSECOND-ja-3.png",320,240 + S,0,143357,,0.6 + M,0,143357,145948,158,240 + M,0,145948,146085,158,240,158,249 + F,0,145948,146766,1,0 + R,0,145948,146766,0,-0.4 + M,0,146085,146221,158,249,158,263 + M,0,146221,146357,158,263,158,288 + M,0,146357,146494,158,288,158,321 + M,0,146494,146630,158,321,158,360 + M,0,146630,146766,158,360,158,409 +Sprite,Foreground,Centre,"SB\lyric\y2\introSECOND-ja-4.png",320,240 + S,0,143357,,0.6 + M,0,143357,146630,212,240 + M,0,146630,146766,212,240,212,248 + F,0,146630,147448,1,0 + R,0,146630,147448,0,0.4 + M,0,146766,146903,212,248,212,263 + M,0,146903,147039,212,263,212,288 + M,0,147039,147176,212,288,212,320 + M,0,147176,147312,212,320,212,360 + M,0,147312,147448,212,360,212,407 +Sprite,Foreground,Centre,"SB\lyric\y2\introSECOND-ja-5.png",320,240 + S,0,143357,,0.6 + M,0,143357,146357,266,240 + M,0,146357,146494,266,240,266,248 + F,0,146357,147176,1,0 + R,0,146357,147176,0,0.4 + M,0,146494,146630,266,248,266,264 + M,0,146630,146766,266,264,266,288 + M,0,146766,146903,266,288,266,321 + M,0,146903,147039,266,321,266,358 + M,0,147039,147176,266,358,266,407 +Sprite,Foreground,Centre,"SB\lyric\y2\introSECOND-ja-6.png",320,240 + S,0,143357,,0.6 + M,0,143357,145812,320,240 + M,0,145812,145948,320,240,320,247 + F,0,145812,146630,1,0 + R,0,145812,146630,0,0.4 + M,0,145948,146085,320,247,320,263 + M,0,146085,146221,320,263,320,288 + M,0,146221,146357,320,288,320,321 + M,0,146357,146494,320,321,320,360 + M,0,146494,146630,320,360,320,408 +Sprite,Foreground,Centre,"SB\lyric\y2\introSECOND-ja-7.png",320,240 + S,0,143357,,0.6 + M,0,143357,146085,374,240 + M,0,146085,146221,374,240,374,248 + F,0,146085,146903,1,0 + R,0,146085,146903,0,0.4 + M,0,146221,146357,374,248,374,264 + M,0,146357,146494,374,264,374,288 + M,0,146494,146630,374,288,374,320 + M,0,146630,146766,374,320,374,359 + M,0,146766,146903,374,359,374,409 +Sprite,Foreground,Centre,"SB\lyric\y2\introSECOND-ja-8.png",320,240 + S,0,143357,,0.6 + M,0,143357,145676,428,240 + M,0,145676,145812,428,240,428,249 + F,0,145676,146494,1,0 + R,0,145676,146494,0,-0.4 + M,0,145812,145948,428,249,428,263 + M,0,145948,146085,428,263,428,288 + M,0,146085,146221,428,288,428,320 + M,0,146221,146357,428,320,428,358 + M,0,146357,146494,428,358,428,408 +Sprite,Foreground,Centre,"SB\lyric\y2\introSECOND-ja-9.png",320,240 + S,0,143357,,0.6 + M,0,143357,146494,482,240 + M,0,146494,146630,482,240,482,247 + F,0,146494,147312,1,0 + R,0,146494,147312,0,-0.4 + M,0,146630,146766,482,247,482,263 + M,0,146766,146903,482,263,482,287 + M,0,146903,147039,482,287,482,320 + M,0,147039,147176,482,320,482,360 + M,0,147176,147312,482,360,482,407 +Sprite,Foreground,Centre,"SB\lyric\y2\introSECOND-ja-10-1.png",320,240 + S,0,143357,,0.6 + M,0,143357,146221,536,240 + M,0,146221,146357,536,240,536,247 + F,0,146221,147039,1,0 + R,0,146221,147039,0,-0.4 + M,0,146357,146494,536,247,536,263 + M,0,146494,146630,536,263,536,289 + M,0,146630,146766,536,289,536,321 + M,0,146766,146903,536,321,536,360 + M,0,146903,147039,536,360,536,407 +Sprite,Foreground,Centre,"SB\lyric\y2\introSECOND-ja-11.png",320,240 + S,0,143357,,0.6 + M,0,143357,146903,590,240 + F,0,146766,147721,1,0 + R,0,146766,147721,0,0.4 + M,0,146903,147039,590,240,590,247 + M,0,147039,147176,590,247,590,263 + M,0,147176,147312,590,263,590,288 + M,0,147312,147448,590,288,590,320 + M,0,147448,147585,590,320,590,361 + M,0,147585,147721,590,361,590,408 +Sprite,Foreground,Centre,"SB\lyric\z\outroSECOND-ja-1.png",320,240 + S,0,147720,,0.6 + M,0,147720,150993,104,240 + M,0,150993,151130,104,240,104,248 + F,0,150993,151812,1,0 + R,0,150993,151812,0,-0.4 + M,0,151130,151266,104,248,104,263 + M,0,151266,151403,104,263,104,287 + M,0,151403,151539,104,287,104,320 + M,0,151539,151675,104,320,104,360 + M,0,151675,151812,104,360,104,409 +Sprite,Foreground,Centre,"SB\lyric\z\outroSECOND-ja-2.png",320,240 + S,0,147720,,0.6 + M,0,147720,150175,158,240 + M,0,150175,150312,158,240,158,249 + F,0,150175,150993,1,0 + R,0,150175,150993,0,-0.4 + M,0,150312,150448,158,249,158,263 + M,0,150448,150584,158,263,158,288 + M,0,150584,150721,158,288,158,321 + M,0,150721,150857,158,321,158,360 + M,0,150857,150993,158,360,158,409 +Sprite,Foreground,Centre,"SB\lyric\z\outroSECOND-ja-3.png",320,240 + S,0,147720,,0.6 + M,0,147720,150857,212,240 + M,0,150857,150993,212,240,212,248 + F,0,150857,151675,1,0 + R,0,150857,151675,0,0.4 + M,0,150993,151130,212,248,212,263 + M,0,151130,151266,212,263,212,288 + M,0,151266,151403,212,288,212,320 + M,0,151403,151539,212,320,212,360 + M,0,151539,151675,212,360,212,407 +Sprite,Foreground,Centre,"SB\lyric\z\outroSECOND-ja-4.png",320,240 + S,0,147720,,0.6 + M,0,147720,150584,266,240 + M,0,150584,150721,266,240,266,248 + F,0,150584,151403,1,0 + R,0,150584,151403,0,0.4 + M,0,150721,150857,266,248,266,264 + M,0,150857,150993,266,264,266,288 + M,0,150993,151130,266,288,266,321 + M,0,151130,151266,266,321,266,358 + M,0,151266,151403,266,358,266,407 +Sprite,Foreground,Centre,"SB\lyric\z\outroSECOND-ja-5.png",320,240 + S,0,147720,,0.6 + M,0,147720,150039,320,240 + M,0,150039,150175,320,240,320,247 + F,0,150039,150857,1,0 + R,0,150039,150857,0,0.4 + M,0,150175,150312,320,247,320,263 + M,0,150312,150448,320,263,320,288 + M,0,150448,150584,320,288,320,321 + M,0,150584,150721,320,321,320,360 + M,0,150721,150857,320,360,320,408 +Sprite,Foreground,Centre,"SB\lyric\z\outroSECOND-ja-6.png",320,240 + S,0,147720,,0.6 + M,0,147720,150312,374,240 + M,0,150312,150448,374,240,374,248 + F,0,150312,151130,1,0 + R,0,150312,151130,0,0.4 + M,0,150448,150584,374,248,374,264 + M,0,150584,150721,374,264,374,288 + M,0,150721,150857,374,288,374,320 + M,0,150857,150993,374,320,374,359 + M,0,150993,151130,374,359,374,409 +Sprite,Foreground,Centre,"SB\lyric\z\outroSECOND-ja-7.png",320,240 + S,0,147720,,0.6 + M,0,147720,149903,428,240 + M,0,149903,150039,428,240,428,249 + F,0,149903,150721,1,0 + R,0,149903,150721,0,-0.4 + M,0,150039,150175,428,249,428,263 + M,0,150175,150312,428,263,428,288 + M,0,150312,150448,428,288,428,320 + M,0,150448,150584,428,320,428,358 + M,0,150584,150721,428,358,428,408 +Sprite,Foreground,Centre,"SB\lyric\z\outroSECOND-ja-8.png",320,240 + S,0,147720,,0.6 + M,0,147720,150721,482,240 + M,0,150721,150857,482,240,482,247 + F,0,150721,151539,1,0 + R,0,150721,151539,0,-0.4 + M,0,150857,150993,482,247,482,263 + M,0,150993,151130,482,263,482,287 + M,0,151130,151266,482,287,482,320 + M,0,151266,151403,482,320,482,360 + M,0,151403,151539,482,360,482,407 +Sprite,Foreground,Centre,"SB\lyric\z\outroSECOND-ja-9.png",320,240 + S,0,147720,,0.6 + M,0,147720,150448,536,240 + M,0,150448,150584,536,240,536,247 + F,0,150448,151266,1,0 + R,0,150448,151266,0,-0.4 + M,0,150584,150721,536,247,536,263 + M,0,150721,150857,536,263,536,289 + M,0,150857,150993,536,289,536,321 + M,0,150993,151130,536,321,536,360 + M,0,151130,151266,536,360,536,407 +Sprite,Foreground,Centre,"SB\lyric\y1\intro-en1.png",320,240 + S,0,25538,,0.8 + M,0,25538,27720,320,280 + F,0,27720,27993,1,0 +Sprite,Foreground,Centre,"SB\lyric\y2\intro-en2.png",320,240 + M,0,29902,,320,280 + S,0,29902,,0.8 + F,0,32084,32357,1,0 +Sprite,Foreground,Centre,"SB\lyric\z\outro-en1.png",320,240 + M,0,143357,,320,280 + S,0,143357,,0.8 + F,0,145538,145811,1,0 +Sprite,Foreground,Centre,"SB\lyric\z\outro-en2.png",320,240 + M,0,147720,,320,280 + S,0,147720,,0.8 + F,0,149902,150175,1,0 +Sprite,Foreground,Centre,"SB\lyric\ja-34-1-1.png",320,240 + S,0,141174,,0.8 + M,0,141174,143356,343,393 + F,0,142265,143356,1,0 +//Storyboard Sound Samples +//Background Colour Transformations +3,100,163,162,255 + +[TimingPoints] +2629,272.727272727273,4,2,1,30,1,0 +20083,291.26213592233,4,2,1,30,1,0 +21248,288.461538461538,4,2,1,30,1,0 +22329,-181.818181818182,4,2,1,30,0,0 +22473,-181.818181818182,4,2,1,5,0,0 +23356,272.727272727273,4,2,1,5,1,0 +23492,-100,4,2,1,10,0,0 +23628,-100,4,2,1,15,0,0 +23765,-100,4,2,1,20,0,0 +23901,-100,4,2,1,25,0,0 +24037,-100,4,2,1,30,0,0 +24174,-100,4,2,1,35,0,0 +24310,-100,4,2,1,40,0,0 +24446,-117.647058823529,4,1,1,50,0,0 +25333,-100,4,1,1,5,0,0 +25537,-100,4,1,1,50,0,0 +25810,-117.647058823529,4,2,1,50,0,0 +26492,-117.647058823529,4,1,1,50,0,0 +26765,-117.647058823529,4,2,1,50,0,0 +27719,-117.647058823529,4,1,1,50,0,0 +27992,-117.647058823529,4,2,1,50,0,0 +28674,-117.647058823529,4,1,1,50,0,0 +28878,-117.647058823529,4,2,1,5,0,0 +28946,-117.647058823529,4,2,1,50,0,0 +29901,-100,4,1,1,50,0,0 +30174,-117.647058823529,4,2,1,50,0,0 +30856,-117.647058823529,4,1,1,50,0,0 +31128,-117.647058823529,4,2,1,50,0,0 +32083,-117.647058823529,4,1,1,50,0,0 +32356,-117.647058823529,4,2,1,50,0,0 +33242,-117.647058823529,4,1,1,5,0,0 +33310,-117.647058823529,4,1,1,30,0,0 +34265,-100,4,1,1,60,0,0 +42583,-117.647058823529,4,1,1,40,0,0 +42992,-100,4,1,1,60,0,0 +51719,-133.333333333333,4,2,1,60,0,0 +53492,-117.647058823529,4,2,1,80,0,0 +53628,-200,4,2,1,60,0,0 +56015,-200,4,2,1,5,0,0 +56083,-200,4,2,1,60,0,0 +60992,-200,4,2,1,60,0,0 +61060,-200,4,1,1,20,0,0 +61128,-200,4,1,1,60,0,0 +61196,-200,4,1,1,20,0,0 +61265,-200,4,1,1,60,0,0 +61401,-200,4,1,1,20,0,0 +61537,-200,4,1,1,60,0,0 +61605,-200,4,1,1,20,0,0 +61946,-200,4,1,1,60,0,0 +62015,-200,4,1,1,20,0,0 +62355,-200,4,1,1,60,0,0 +62628,-200,4,2,1,60,0,0 +70265,-100,4,1,1,60,0,0 +71356,-90.9090909090909,4,1,1,60,0,1 +78992,-90.9090909090909,4,1,1,40,0,0 +80083,-90.9090909090909,4,1,1,60,0,1 +86628,-117.647058823529,4,1,1,60,0,0 +87719,-117.647058823529,4,1,1,60,0,0 +88810,-100,4,1,1,60,0,0 +97537,-133.333333333333,4,2,1,60,0,0 +99310,-100,4,2,1,80,0,0 +99446,-200,4,2,1,60,0,0 +101833,-200,4,2,1,5,0,0 +101901,-200,4,2,1,60,0,0 +106810,-200,4,2,1,60,0,0 +106878,-200,4,2,1,20,0,0 +106878,-200,4,2,1,60,0,0 +106946,-200,4,1,1,60,0,0 +107015,-200,4,1,1,20,0,0 +107083,-200,4,1,1,60,0,0 +107219,-200,4,1,1,20,0,0 +107355,-200,4,1,1,60,0,0 +107424,-200,4,1,1,20,0,0 +107765,-200,4,1,1,60,0,0 +107833,-200,4,1,1,20,0,0 +108174,-200,4,1,1,60,0,0 +108446,-200,4,2,1,60,0,0 +116083,-200,4,1,1,60,0,0 +117174,-133.333333333333,4,1,1,60,0,1 +123719,-200,4,1,1,20,0,0 +124674,-200,4,1,1,60,0,0 +126174,-90.9090909090909,4,1,1,60,0,1 +127810,-200,4,1,1,60,0,0 +128356,-100,4,1,1,60,0,1 +129992,-200,4,2,1,60,0,0 +130265,-100,4,1,1,60,0,1 +133537,-100,4,1,1,60,0,0 +134628,-100,4,1,1,60,0,1 +141174,-100,4,1,1,60,0,0 +142265,-117.647058823529,4,1,1,60,0,1 +143287,-117.647058823529,4,1,1,5,0,1 +143355,-117.647058823529,4,1,1,5,0,0 +143356,-133.333333333333,4,2,1,60,0,0 +147651,-133.333333333333,4,2,1,5,0,0 +147719,-133.333333333333,4,2,1,60,0,0 +152083,-100,4,1,1,60,0,0 +160810,-83.3333333333333,4,1,1,60,0,1 +162992,-76.9230769230769,4,1,1,60,0,1 +165174,-71.4285714285714,4,1,1,60,0,1 +167356,-117.647058823529,4,1,1,60,0,0 +168446,-117.647058823529,4,1,1,60,0,1 +169537,-117.647058823529,4,2,1,60,0,0 +171310,-117.647058823529,4,2,1,80,0,0 +171446,-117.647058823529,4,2,1,60,0,0 + + +[Colours] +Combo1 : 255,255,255 +Combo2 : 72,255,72 +Combo3 : 255,128,0 +Combo4 : 255,32,32 +Combo5 : 0,128,255 +Combo6 : 255,85,255 +Combo7 : 0,183,0 +Combo8 : 176,112,77 + +[HitObjects] +154,247,22401,6,0,P|56:170|169:171,1,313.500009567261 +169,170,23356,53,0,1:0:0:0: +192,92,23492,1,0,1:0:0:0: +252,40,23629,1,0,1:0:0:0: +332,28,23765,1,0,1:0:0:0: +404,60,23901,1,0,1:0:0:0: +452,124,24038,1,0,1:0:0:0: +456,204,24174,1,0,1:0:0:0: +424,276,24310,1,0,1:0:0:0: +356,316,24447,54,0,P|316:312|272:312,1,80.750001540184,0|0,0:0|2:0,0:0:0:0: +275,311,24651,1,0,2:0:0:0: +275,311,24719,1,2,2:0:0:0: +168,344,24856,1,4,3:0:0:0: +92,264,24992,1,0,0:0:0:0: +96,249,25060,1,0,2:0:0:0: +99,235,25129,1,0,2:0:0:0: +103,221,25197,1,0,2:0:0:0: +106,207,25265,54,0,P|72:144|92:104,1,121.125002310276 +108,208,25538,54,0,P|153:206|208:204,1,95 +260,88,25810,2,0,L|176:84,1,80.750001540184 +304,220,26083,1,2,0:0:0:0: +400,212,26219,1,0,0:0:0:0: +344,132,26356,1,0,0:0:0:0: +356,300,26492,54,0,P|300:360|212:348,1,161.500003080368,0|0,3:0|0:0,0:0:0:0: +200,216,26901,1,0,0:0:0:0: +200,216,27038,1,0,0:0:0:0: +108,328,27174,1,2,0:0:0:0: +108,328,27310,1,0,0:0:0:0: +28,208,27447,1,2,0:0:0:0: +112,244,27583,1,4,3:0:0:0: +112,244,27651,1,0,0:0:0:0: +112,244,27719,54,0,B|104:176|88:116|88:116|104:124|120:128,1,161.500003080368 +208,164,28129,2,0,P|248:159|284:136,1,80.750001540184,0|2,0:0|0:0,0:0:0:0: +212,72,28401,1,0,0:0:0:0: +280,240,28538,1,0,0:0:0:0: +388,140,28674,54,0,P|392:200|372:264,1,121.125002310276,0|0,3:0|0:0,0:0:0:0: +375,257,28947,2,0,P|336:252|288:252,1,80.750001540184,0|2,0:0|0:0,0:0:0:0: +456,308,29219,1,0,0:0:0:0: +376,372,29356,1,2,0:0:0:0: +376,372,29492,1,0,0:0:0:0: +376,372,29629,1,2,0:0:0:0: +376,372,29765,1,4,3:0:0:0: +376,372,29901,54,0,P|331:371|280:368,1,95 +224,248,30174,2,0,P|264:247|312:244,1,80.750001540184 +164,352,30447,1,2,0:0:0:0: +68,360,30583,1,0,0:0:0:0: +108,272,30719,1,0,0:0:0:0: +40,156,30856,54,0,P|44:80|120:36,1,161.500003080368,0|0,3:0|0:0,0:0:0:0: +136,132,31265,1,0,0:0:0:0: +136,132,31401,1,0,0:0:0:0: +256,48,31538,1,2,0:0:0:0: +256,48,31674,1,0,0:0:0:0: +376,132,31810,1,2,0:0:0:0: +312,224,31947,1,4,3:0:0:0: +312,224,32015,1,0,0:0:0:0: +312,224,32083,54,0,B|272:288|312:352|312:352|280:344,1,161.500003080368 +196,324,32492,1,0,0:0:0:0: +312,352,32629,2,0,P|348:340|396:340,1,80.750001540184,2|0,0:0|0:0,0:0:0:0: +484,368,32901,1,0,0:0:0:0: +448,229,33038,54,0,B|430:168|430:168|448:149|456:113,1,121.125002310276,2|0,0:0|0:0,0:0:0:0: +455,116,33310,2,0,L|375:132,1,80.750001540184 +208,44,33583,2,0,L|290:58,1,80.750001540184,2|0,2:0|0:0,0:0:0:0: +295,65,34265,54,0,L|199:47,1,95,4|0,0:0|0:0,0:0:0:0: +76,88,34538,1,8,0:0:0:0: +116,216,34674,1,2,2:0:0:0: +116,216,34742,1,2,2:0:0:0: +116,216,34810,2,0,P|156:244|208:240,1,95,2|0,2:0|0:0,0:0:0:0: +360,152,35083,2,0,P|311:147|268:177,1,95,8|0,0:0|0:0,0:0:0:0: +336,244,35356,54,0,L|384:60,1,190,2|8,2:0|0:0,0:0:0:0: +436,228,35765,1,2,2:0:0:0: +287,76,35901,1,2,2:0:0:0: +462,128,36038,1,2,2:0:0:0: +260,176,36174,1,8,0:0:0:0: +384,60,36310,1,2,2:0:0:0: +360,152,36447,54,0,B|312:224|348:312|348:312|320:300,1,190,4|8,0:0|0:0,0:0:0:0: +204,252,36856,1,2,2:0:0:0: +204,252,36924,1,2,2:0:0:0: +204,252,36992,2,0,P|160:236|104:244,1,95 +232,356,37265,1,8,0:0:0:0: +176,152,37401,1,0,0:0:0:0: +48,332,37538,54,0,L|144:324,1,95,8|2,0:0|2:0,0:0:0:0: +300,244,37810,2,0,L|204:252,1,95,8|2,0:0|2:0,0:0:0:0: +40,320,38083,2,0,L|136:312,1,95,0|2,0:0|2:0,0:0:0:0: +312,252,38356,2,0,P|340:184|308:124,1,142.5,8|0,0:0|0:0,0:0:0:0: +296,120,38629,54,0,P|340:132|390:129,1,95,4|0,0:0|0:0,0:0:0:0: +264,44,38901,2,0,P|224:72|212:116,1,95,8|0,0:0|0:0,0:0:0:0: +252,192,39174,1,2,2:0:0:0: +96,120,39310,1,0,0:0:0:0: +144,284,39447,1,8,0:0:0:0: +256,304,39583,1,2,2:0:0:0: +256,304,39651,1,2,2:0:0:0: +256,304,39719,54,0,B|352:296|420:292|420:292|404:260,1,190,0|8,0:0|0:0,0:0:0:0: +360,196,40129,1,0,0:0:0:0: +320,376,40265,2,0,L|424:371,1,95,2|2,2:0|2:0,0:0:0:0: +456,192,40538,2,0,L|352:197,1,95,8|0,0:0|0:0,0:0:0:0: +416,272,40810,54,0,P|360:192|400:116,1,190,4|8,0:0|0:0,0:0:0:0: +500,84,41219,1,0,0:0:0:0: +380,28,41356,2,0,P|332:20|284:40,1,95,2|0,2:0|0:0,0:0:0:0: +248,164,41629,1,8,0:0:0:0: +120,184,41765,1,2,2:0:0:0: +164,60,41901,54,0,L|68:56,1,95,8|2,0:0|2:0,0:0:0:0: +25,180,42174,2,0,L|121:184,1,95,8|2,0:0|2:0,0:0:0:0: +164,308,42447,1,8,0:0:0:0: +419,236,42583,1,0,0:0:0:0: +398,233,42651,1,0,0:0:0:0: +377,231,42719,1,8,0:0:0:0: +356,230,42788,1,0,0:0:0:0: +335,231,42856,1,8,0:0:0:0: +314,233,42924,1,8,0:0:0:0: +294,237,42992,54,0,P|257:209|249:161,1,95,4|0,0:0|0:0,0:0:0:0: +296,40,43265,1,8,0:0:0:0: +328,168,43401,1,2,2:0:0:0: +328,168,43469,1,2,2:0:0:0: +328,168,43538,2,0,P|376:176|424:152,1,95,2|0,2:0|0:0,0:0:0:0: +480,64,43810,1,8,0:0:0:0: +492,180,43947,1,0,0:0:0:0: +344,320,44083,54,0,P|256:328|197:233,1,190,2|8,2:0|0:0,0:0:0:0: +294,236,44492,1,0,0:0:0:0: +232,160,44629,53,2,2:0:0:0: +244,28,44765,1,2,2:0:0:0: +112,16,44901,1,8,0:0:0:0: +96,148,45038,1,2,2:0:0:0: +96,148,45106,1,0,0:0:0:0: +96,148,45174,54,0,B|72:216|112:268|112:268|88:264,1,142.5,4|0,0:0|0:0,0:0:0:0: +84,264,45447,2,0,P|36:268|0:304,1,95,8|0,0:0|0:0,0:0:0:0: +72,344,45719,2,0,L|272:360,1,190,2|8,2:0|0:0,0:0:0:0: +261,359,46129,1,0,0:0:0:0: +444,296,46265,54,0,P|472:260|472:200,1,95,8|2,0:0|2:0,0:0:0:0: +408,48,46538,2,0,P|397:92|423:146,1,95,8|2,0:0|2:0,0:0:0:0: +384,224,46810,1,2,2:0:0:0: +384,224,46947,2,0,L|288:216,1,95,8|8,0:0|0:0,0:0:0:0: +320,80,47219,1,2,2:0:0:0: +320,80,47288,1,2,2:0:0:0: +320,80,47356,54,0,B|248:68|184:100|184:100|161:85|120:80,1,190,4|8,0:0|0:0,0:0:0:0: +16,144,47765,1,0,0:0:0:0: +4,284,47901,2,0,P|32:323|84:344,1,95,2|0,2:0|0:0,0:0:0:0: +160,368,48174,1,8,0:0:0:0: +268,348,48310,1,0,0:0:0:0: +160,368,48447,54,0,P|206:365|256:364,1,95,2|0,2:0|0:0,0:0:0:0: +440,308,48719,2,0,P|393:305|344:304,1,95,8|0,0:0|0:0,0:0:0:0: +209,229,48992,1,2,2:0:0:0: +232,224,49060,1,0,2:0:0:0: +253,213,49129,1,2,2:0:0:0: +270,196,49197,1,0,2:0:0:0: +282,175,49265,1,8,0:0:0:0: +287,151,49333,1,0,2:0:0:0: +286,127,49401,1,2,2:0:0:0: +279,104,49469,1,2,2:0:0:0: +266,83,49538,54,0,P|312:97|364:86,1,95,4|0,0:0|0:0,0:0:0:0: +195,142,49810,2,0,P|146:137|99:161,1,95,8|0,0:0|0:0,0:0:0:0: +84,288,50083,2,0,P|188:304|264:256,1,190,2|8,2:0|0:0,0:0:0:0: +160,224,50492,1,0,0:0:0:0: +195,142,50629,53,8,0:0:0:0: +172,60,50765,1,2,2:0:0:0: +272,120,50901,53,8,0:0:0:0: +293,37,51038,1,2,2:0:0:0: +348,132,51174,53,8,0:0:0:0: +407,70,51310,1,2,2:0:0:0: +414,174,51447,53,8,0:0:0:0: +416,179,51515,1,8,0:0:0:0: +419,184,51583,1,8,0:0:0:0: +421,189,51651,1,8,0:0:0:0: +424,195,51719,38,0,P|432:231|432:267,1,71.2500027179719,0|0,1:0|0:0,0:0:0:0: +412,356,51992,2,0,P|378:353|336:352,1,71.2500027179719,0|0,1:0|0:0,0:0:0:0: +180,296,52265,1,0,1:0:0:0: +180,296,52401,1,0,1:0:0:0: +180,296,52538,2,0,P|217:294|252:293,1,71.2500027179719,0|0,1:0|0:0,0:0:0:0: +316,216,52810,101,4,3:0:0:0: +280,124,52947,1,0,0:0:0:0: +280,124,53015,1,0,0:0:0:0: +280,124,53083,1,2,0:0:0:0: +212,196,53219,5,4,3:0:0:0: +176,104,53356,1,0,0:0:0:0: +176,104,53424,1,0,0:0:0:0: +176,104,53492,1,2,0:0:0:0: +116,176,53629,1,0,1:0:0:0: +168,308,53765,5,2,0:0:0:0: +168,308,53901,2,0,P|232:300|312:300,1,142.5,4|0,3:0|0:0,0:0:0:0: +368,348,54447,2,0,P|384:312|372:256,1,95,0|0,1:0|1:0,0:0:0:0: +320,204,54856,1,0,0:0:0:0: +448,240,54992,54,0,L|464:184,1,47.5,4|4,3:0|3:0,3:0:0:0: +392,152,55265,2,0,L|388:100,1,47.5,0|4,1:0|3:0,0:0:0:0: +312,80,55538,2,0,L|284:32,1,47.5,0|4,0:0|3:0,0:0:0:0: +228,116,55810,2,0,P|188:112|136:112,1,71.25,0|0,1:0|0:0,0:0:0:0: +156,111,56083,54,0,P|152:152|100:192,1,95,4|0,3:0|0:0,0:0:0:0: +36,184,56492,1,0,0:0:0:0: +72,252,56629,1,0,1:0:0:0: +80,120,56765,1,4,3:0:0:0: +52,48,56901,53,0,1:0:0:0: +114,188,57038,2,0,P|184:184|256:184,1,142.5,0|2,0:0|0:0,0:0:0:0: +352,284,57583,1,0,0:0:0:0: +352,284,57719,1,2,0:0:0:0: +352,284,57856,1,0,0:0:0:0: +352,284,57992,2,0,L|336:344,1,47.5,2|2,0:0|0:0,0:0:0:0: +352,284,58265,54,0,P|288:277|208:276,1,142.5,4|4,3:0|3:0,0:0:0:0: +209,275,58810,2,0,P|180:240|180:184,1,95,0|0,1:0|1:0,0:0:0:0: +204,120,59219,1,0,1:0:0:0: +252,212,59356,54,0,L|304:196,1,47.5,4|0,3:0|0:0,0:0:0:0: +456,76,59629,2,0,L|404:92,1,47.5,0|4,1:0|3:0,0:0:0:0: +260,220,59901,2,0,L|312:204,1,47.5,0|4,0:0|3:0,0:0:0:0: +464,84,60174,2,0,L|412:100,1,47.5,0|0,1:0|0:0,0:0:0:0: +364,152,60447,54,0,P|320:128|268:136,1,95,4|2,3:0|0:0,0:0:0:0: +304,208,60856,1,0,0:0:0:0: +432,289,60992,1,0,1:0:0:0: +415,287,61060,1,0,0:0:0:0: +398,286,61129,1,4,3:0:0:0: +381,285,61197,1,0,0:0:0:0: +364,286,61265,1,0,1:0:0:0: +263,356,61401,1,0,0:0:0:0: +263,356,61469,1,0,0:0:0:0: +263,356,61538,53,0,0:0:0:0: +246,352,61606,1,0,0:0:0:0: +229,349,61674,1,0,0:0:0:0: +212,348,61742,1,0,0:0:0:0: +195,348,61810,1,0,0:0:0:0: +60,362,61947,1,0,0:0:0:0: +48,343,62015,1,0,0:0:0:0: +39,323,62083,1,0,0:0:0:0: +34,302,62151,1,0,0:0:0:0: +34,280,62219,1,0,0:0:0:0: +38,259,62288,1,0,0:0:0:0: +46,239,62356,1,0,0:0:0:0: +46,239,62629,54,0,B|97:232|157:220|157:220|183:242,1,142.5,4|4,3:0|3:0,0:0:0:0: +92,156,63174,2,0,L|189:138,1,95,0|0,1:0|1:0,0:0:0:0: +256,100,63583,1,0,1:0:0:0: +256,176,63719,53,4,3:0:0:0: +256,176,63856,1,4,3:0:0:0: +256,24,63992,1,0,1:0:0:0: +326,138,64129,2,0,L|420:156,1,95,4|4,3:0|3:0,0:0:0:0: +476,208,64538,2,0,P|492:256|476:304,1,95,0|0,1:0|1:0,0:0:0:0: +480,297,65083,1,0,0:0:0:0: +328,224,65219,1,0,0:0:0:0: +340,304,65356,53,0,1:0:0:0: +208,324,65492,1,4,3:0:0:0: +340,304,65629,2,0,P|288:296|260:264,1,95,0|4,1:0|3:0,1:0:0:0: +260,188,66038,1,0,0:0:0:0: +192,224,66174,2,0,L|136:216,1,47.5,0|4,1:0|3:0,0:0:0:0: +24,196,66447,53,2,0:0:0:0: +24,196,66583,1,4,3:0:0:0: +24,196,66719,1,0,1:0:0:0: +24,196,66856,2,0,P|0:152|8:104,1,95,4|0,3:0|0:0,0:0:0:0: +76,140,67265,2,0,P|122:137|172:132,1,95,2|0,0:0|1:0,0:0:0:0: +332,172,67674,1,4,3:0:0:0: +332,172,67810,2,0,L|284:176|236:180,1,95,0|0,1:0|0:0,0:0:0:0: +128,240,68219,1,0,0:0:0:0: +232,276,68356,1,0,1:0:0:0: +120,332,68492,1,4,3:0:0:0: +224,372,68629,54,0,P|268:376|320:356,1,95,4|0,3:0|1:0,0:0:0:0: +360,300,69038,1,4,3:0:0:0: +432,276,69174,2,0,P|452:232|440:180,1,95,2|0,0:0|1:0,0:0:0:0: +344,44,69583,2,0,P|340:92|372:140,1,95,4|0,3:0|0:0,0:0:0:0: +448,88,69992,1,0,1:0:0:0: +448,88,70265,21,0,0:0:0:0: +419,356,70401,1,0,0:0:0:0: +151,324,70538,1,0,0:0:0:0: +180,56,70674,1,0,0:0:0:0: +180,56,71219,37,0,0:0:0:0: +256,300,71356,2,0,L|224:192,1,104.500003189087,4|0,0:0|0:0,0:0:0:0: +202,130,71629,1,8,0:0:0:0: +252,24,71765,53,0,0:0:0:0: +151,324,71901,2,0,L|192:208,1,104.500003189087 +220,124,72174,1,8,0:0:0:0: +352,56,72310,53,0,0:0:0:0: +20,216,72447,2,0,L|124:172,1,104.500003189087 +236,116,72719,1,8,0:0:0:0: +350,191,72856,53,0,0:0:0:0: +468,120,72992,1,0,0:0:0:0: +472,256,73129,1,0,0:0:0:0: +388,120,73265,1,8,0:0:0:0: +511,187,73401,1,0,0:0:0:0: +392,256,73538,54,0,P|284:228|288:144,1,209.000006378174,4|8,0:0|0:0,0:0:0:0: +168,264,73947,1,0,0:0:0:0: +350,191,74083,2,0,P|297:189|236:188,1,104.500003189087 +468,120,74356,2,0,P|415:118|354:117,1,104.500003189087,8|0,0:0|0:0,0:0:0:0: +172,168,74629,54,0,P|184:236|264:260,1,156.750004783631 +172,168,74901,2,0,P|119:164|60:164,1,104.500003189087,8|0,0:0|0:0,0:0:0:0: +20,236,75174,53,0,0:0:0:0: +80,56,75310,1,0,0:0:0:0: +248,132,75447,2,0,P|298:125|352:124,1,104.500003189087,8|8,0:0|0:0,0:0:0:0: +351,123,75651,1,8,0:0:0:0: +351,123,75719,54,0,B|348:186|340:240|340:240|352:251|364:276,1,156.750004783631,12|0,0:0|0:0,0:0:0:0: +372,288,75993,2,0,L|384:180,1,104.500003189087,8|0,0:0|0:0,0:0:0:0: +280,316,76265,2,0,P|232:340|168:328,1,104.500003189087 +220,260,76538,1,8,0:0:0:0: +344,368,76674,53,0,0:0:0:0: +396,176,76810,2,0,L|384:284,1,104.500003189087 +456,348,77083,1,8,0:0:0:0: +308,200,77219,53,0,0:0:0:0: +472,252,77356,1,0,0:0:0:0: +452,116,77492,1,0,0:0:0:0: +352,24,77629,1,8,0:0:0:0: +216,28,77765,1,0,0:0:0:0: +112,116,77901,54,0,B|76:216|144:288|144:288|148:256,1,209.000006378174,4|8,0:0|0:0,0:0:0:0: +196,80,78310,1,0,0:0:0:0: +372,200,78447,2,0,B|332:164|284:148|284:148|224:156|180:184,1,209.000006378174,0|0,0:0|0:0,0:0:0:0: +36,268,78856,1,0,0:0:0:0: +92,368,78992,53,8,0:0:0:0: +110,362,79060,1,0,0:0:0:0: +128,357,79129,1,0,0:0:0:0: +146,353,79197,1,0,0:0:0:0: +165,350,79265,1,0,0:0:0:0: +183,348,79333,1,0,0:0:0:0: +201,346,79401,1,8,0:0:0:0: +219,345,79469,1,0,0:0:0:0: +237,345,79538,1,0,0:0:0:0: +208,236,79674,1,0,0:0:0:0: +370,255,79810,1,8,0:0:0:0: +386,262,79879,1,0,0:0:0:0: +402,268,79947,1,0,0:0:0:0: +417,278,80015,1,0,0:0:0:0: +431,287,80083,6,0,P|408:243|424:184,1,104.500003189087,4|0,0:0|0:0,0:0:0:0: +486,234,80356,1,8,0:0:0:0: +374,350,80492,1,0,0:0:0:0: +286,215,80629,54,0,P|288:159|324:117,1,104.500003189087 +368,192,80901,1,8,0:0:0:0: +182,243,81038,1,0,0:0:0:0: +152,76,81174,54,0,P|168:28|221:-2,1,104.500003189087 +262,103,81447,1,8,0:0:0:0: +105,201,81583,1,0,0:0:0:0: +56,116,81719,53,0,0:0:0:0: +20,248,81856,1,0,0:0:0:0: +152,280,81992,1,8,0:0:0:0: +192,152,82129,1,0,0:0:0:0: +20,248,82265,54,0,P|8:320|64:380,1,156.750004783631 +60,378,82538,2,0,P|114:374|168:372,1,104.500003189087,8|0,0:0|0:0,0:0:0:0: +260,336,82810,2,0,P|280:284|268:232,1,104.500003189087 +260,336,83083,1,8,0:0:0:0: +192,152,83219,1,0,0:0:0:0: +105,201,83356,54,0,P|54:203|-7:209,1,104.500003189087 +56,52,83629,2,0,P|107:54|168:60,1,104.500003189087,8|0,0:0|0:0,0:0:0:0: +312,216,83901,2,0,P|368:184|376:96,1,156.750004783631 +360,88,84174,2,0,P|416:92|472:76,1,104.500003189087,8|0,0:0|0:0,0:0:0:0: +380,284,84447,54,0,P|312:216|200:216,1,209.000006378174,4|8,0:0|0:0,0:0:0:0: +256,340,84856,1,0,0:0:0:0: +164,364,84992,2,0,P|100:356|64:328,1,104.500003189087 +16,208,85265,1,8,0:0:0:0: +140,260,85401,53,0,0:0:0:0: +92,132,85538,2,0,P|120:88|172:64,1,104.500003189087 +192,168,85810,1,8,0:0:0:0: +192,168,85947,1,0,0:0:0:0: +280,64,86083,53,0,0:0:0:0: +404,120,86219,1,0,0:0:0:0: +392,256,86356,1,8,0:0:0:0: +260,284,86492,1,0,0:0:0:0: +304,176,86629,53,0,0:0:0:0: +288,172,87174,53,0,0:0:0:0: +272,168,87719,69,8,0:0:0:0: +257,174,87788,1,0,0:0:0:0: +241,178,87856,2,0,P|202:170|160:168,1,80.750001540184 +136,284,88129,1,8,0:0:0:0: +151,290,88197,1,0,0:0:0:0: +167,294,88265,2,0,P|206:286|248:284,1,80.750001540184 +57,253,88538,1,8,0:0:0:0: +57,253,88810,6,0,P|80:300|72:348,1,95,4|0,0:0|0:0,0:0:0:0: +0,312,89083,1,8,0:0:0:0: +148,344,89219,1,2,2:0:0:0: +96,176,89356,2,0,P|48:168|0:192,1,95,2|0,2:0|0:0,0:0:0:0: +160,232,89629,1,8,0:0:0:0: +148,344,89765,1,0,0:0:0:0: +176,144,89901,54,0,P|264:136|323:231,1,190,2|8,2:0|0:0,0:0:0:0: +336,292,90310,1,0,0:0:0:0: +252,260,90447,53,2,2:0:0:0: +408,236,90583,1,2,2:0:0:0: +244,272,90719,1,8,0:0:0:0: +420,224,90856,1,2,2:0:0:0: +420,224,90924,1,0,0:0:0:0: +420,224,90992,2,0,B|452:152|400:88,1,142.5,4|0,0:0|0:0,0:0:0:0: +401,90,91265,2,0,P|359:95|296:100,1,95,8|0,0:0|0:0,0:0:0:0: +504,64,91538,2,0,L|304:20,1,190,2|8,2:0|0:0,0:0:0:0: +220,80,91947,1,0,0:0:0:0: +60,48,92083,54,0,P|32:84|32:144,1,95,8|2,0:0|2:0,0:0:0:0: +112,292,92356,2,0,P|123:248|97:194,1,95,8|2,0:0|2:0,0:0:0:0: +32,256,92629,1,8,0:0:0:0: +112,116,92765,1,2,2:0:0:0: +208,236,92901,1,8,0:0:0:0: +192,332,93038,1,2,2:0:0:0: +192,332,93106,1,2,2:0:0:0: +192,332,93174,54,0,B|264:344|328:312|328:312|351:327|392:332,1,190,12|8,0:0|0:0,0:0:0:0: +508,268,93583,1,0,0:0:0:0: +384,184,93719,2,0,P|348:152|336:104,1,95,2|0,2:0|0:0,0:0:0:0: +352,16,93992,1,8,0:0:0:0: +244,36,94129,1,0,0:0:0:0: +352,16,94265,54,0,P|306:19|256:20,1,95,2|0,2:0|0:0,0:0:0:0: +228,184,94538,2,0,P|275:187|324:188,1,95,8|0,0:0|0:0,0:0:0:0: +292,104,94810,53,2,2:0:0:0: +144,116,94947,1,2,2:0:0:0: +217,80,95083,53,8,0:0:0:0: +120,192,95219,1,2,2:0:0:0: +156,264,95356,54,0,P|110:250|58:261,1,95,4|0,0:0|0:0,0:0:0:0: +124,352,95629,2,0,P|173:357|220:333,1,95,8|0,0:0|0:0,0:0:0:0: +412,228,95901,2,0,P|308:212|232:260,1,190,2|8,2:0|0:0,0:0:0:0: +340,308,96310,1,0,0:0:0:0: +389,146,96447,37,8,0:0:0:0: +412,228,96583,1,2,2:0:0:0: +300,124,96719,101,8,0:0:0:0: +323,206,96856,1,2,2:0:0:0: +212,104,96992,37,8,0:0:0:0: +235,186,97129,1,2,2:0:0:0: +148,304,97265,53,8,0:0:0:0: +148,304,97333,1,8,0:0:0:0: +148,304,97401,1,8,0:0:0:0: +148,304,97469,1,8,0:0:0:0: +148,304,97538,86,0,P|112:296|72:288,1,71.2500027179719,0|0,1:0|0:0,0:0:0:0: +28,184,97810,2,0,P|65:177|107:173,1,71.2500027179719,0|0,1:0|0:0,1:0:0:0: +232,152,98083,1,0,1:0:0:0: +232,152,98219,1,0,1:0:0:0: +232,152,98356,2,0,P|222:185|217:223,1,71.2500027179719,0|0,1:0|0:0,0:0:0:0: +248,352,98629,101,4,3:0:0:0: +328,324,98765,1,0,0:0:0:0: +328,324,98833,1,0,0:0:0:0: +328,324,98901,1,2,0:0:0:0: +476,288,99038,5,4,3:0:0:0: +392,268,99174,1,0,0:0:0:0: +392,268,99242,1,0,0:0:0:0: +392,268,99310,1,2,0:0:0:0: +448,212,99447,1,0,1:0:0:0: +264,272,99583,5,2,0:0:0:0: +264,272,99719,2,0,P|256:208|312:160,1,142.5,4|0,3:0|0:0,0:0:0:0: +392,156,100265,2,0,P|381:118|335:82,1,95,0|0,1:0|1:0,0:0:0:0: +264,96,100674,1,0,0:0:0:0: +192,72,100810,54,0,L|136:56,1,47.5,4|0,3:0|0:0,0:0:0:0: +32,96,101083,2,0,L|88:112,1,47.5,0|4,1:0|3:0,0:0:0:0: +168,192,101356,2,0,L|112:176,1,47.5,0|4,0:0|3:0,0:0:0:0: +60,228,101629,2,0,P|56:268|56:320,1,71.25,0|0,1:0|0:0,0:0:0:0: +55,299,101901,54,0,P|100:280|144:288,1,95,4|2,3:0|0:0,0:0:0:0: +108,356,102310,1,0,0:0:0:0: +184,356,102447,1,0,1:0:0:0: +188,224,102583,1,4,3:0:0:0: +224,288,102719,53,0,1:0:0:0: +117,193,102856,2,0,P|173:149|245:173,1,142.5,4|8,3:0|1:0,0:0:0:0: +308,304,103401,1,4,3:0:0:0: +308,304,103538,1,0,1:0:0:0: +308,304,103674,1,4,3:0:0:0: +308,304,103810,2,0,L|324:244,1,47.5,0|0,1:0|0:0,0:0:0:0: +308,304,104083,54,0,P|340:352|424:336,1,142.5,4|4,3:0|3:0,0:0:0:0: +456,280,104629,2,0,P|452:231|448:180,1,95,0|0,1:0|1:0,0:0:0:0: +440,108,105038,1,2,0:0:0:0: +340,44,105174,54,0,L|288:60,1,47.5,4|0,3:0|0:0,0:0:0:0: +172,32,105447,2,0,L|188:84,1,47.5,0|4,1:0|3:0,0:0:0:0: +164,192,105719,2,0,L|216:176,1,47.5,0|4,0:0|3:0,0:0:0:0: +324,208,105992,2,0,L|308:156,1,47.5,0|0,1:0|0:0,0:0:0:0: +252,112,106265,54,0,P|208:88|156:96,1,95,4|2,3:0|0:0,0:0:0:0: +192,164,106674,1,0,0:0:0:0: +48,240,106810,1,0,1:0:0:0: +65,242,106879,1,0,0:0:0:0: +82,243,106947,1,4,3:0:0:0: +99,244,107015,1,0,0:0:0:0: +116,243,107083,1,0,1:0:0:0: +180,324,107219,1,0,0:0:0:0: +180,324,107288,1,0,0:0:0:0: +180,324,107356,53,0,0:0:0:0: +197,320,107424,1,0,0:0:0:0: +214,317,107492,1,0,0:0:0:0: +231,316,107560,1,0,0:0:0:0: +248,316,107629,1,0,0:0:0:0: +364,367,107765,1,0,0:0:0:0: +372,347,107833,1,0,0:0:0:0: +376,326,107901,1,0,0:0:0:0: +376,304,107969,1,0,0:0:0:0: +371,283,108038,1,0,0:0:0:0: +362,263,108106,1,0,0:0:0:0: +350,244,108174,1,0,0:0:0:0: +350,244,108447,54,0,B|361:178|372:120|372:120|344:132,1,142.5,4|4,3:0|3:0,0:0:0:0: +268,256,108992,2,0,L|286:157,1,95,0|0,1:0|1:0,0:0:0:0: +256,92,109401,1,4,3:0:0:0: +208,152,109538,53,4,3:0:0:0: +208,152,109674,1,0,0:0:0:0: +88,76,109810,1,0,1:0:0:0: +84,216,109947,2,0,P|36:212|-4:180,1,95,4|4,3:0|3:0,0:0:0:0: +64,144,110356,1,0,1:0:0:0: +24,320,110629,54,0,P|72:321|124:324,1,95,4|2,3:0|0:0,0:0:0:0: +196,348,111038,1,0,0:0:0:0: +184,272,111174,53,0,1:0:0:0: +316,252,111310,1,4,3:0:0:0: +184,272,111447,2,0,P|236:280|264:312,1,95,0|4,1:0|3:0,0:0:0:0: +292,380,111856,1,0,0:0:0:0: +344,324,111992,2,0,L|400:332,1,47.5,0|4,1:0|3:0,0:0:0:0: +488,248,112265,53,2,0:0:0:0: +488,248,112401,1,4,3:0:0:0: +488,248,112538,1,0,1:0:0:0: +488,248,112674,2,0,P|512:204|504:156,1,95,4|0,3:0|0:0,0:0:0:0: +456,104,113083,2,0,P|410:107|360:112,1,95,2|0,0:0|1:0,0:0:0:0: +308,232,113492,1,4,3:0:0:0: +308,232,113629,2,0,P|268:204|212:212,1,95,0|4,1:0|3:0,0:0:0:0: +248,280,114038,1,0,0:0:0:0: +128,348,114174,1,0,1:0:0:0: +148,220,114310,1,4,3:0:0:0: +76,184,114447,54,0,P|108:150|163:141,1,95,4|0,3:0|1:0,0:0:0:0: +228,164,114856,1,0,0:0:0:0: +288,116,114992,2,0,P|336:104|384:112,1,95,4|0,3:0|1:0,0:0:0:0: +468,244,115401,2,0,P|468:196|440:148,1,95,4|0,3:0|0:0,0:0:0:0: +376,188,115810,1,0,1:0:0:0: +376,188,116083,85,0,0:0:0:0: +248,324,116219,1,0,0:0:0:0: +164,156,116356,1,0,0:0:0:0: +288,16,116492,1,0,0:0:0:0: +288,16,117038,53,0,0:0:0:0: +196,56,117174,2,0,P|124:48|48:48,1,142.500005435944,4|0,0:0|0:0,0:0:0:0: +16,140,117583,1,0,0:0:0:0: +72,232,117719,2,0,P|116:256|180:248,1,106.875004076958,8|0,0:0|0:0,0:0:0:0: +184,248,117992,2,0,P|221:249|256:264,1,71.2500027179719,0|0,0:0|0:0,0:0:0:0: +300,340,118265,6,0,P|372:356|440:320,1,142.500005435944 +448,240,118674,1,0,0:0:0:0: +408,160,118810,101,8,0:0:0:0: +464,92,118947,1,0,0:0:0:0: +324,144,119083,5,0,0:0:0:0: +408,160,119219,1,0,0:0:0:0: +256,192,119356,102,0,P|188:196|112:200,1,142.500005435944 +56,120,119765,1,0,0:0:0:0: +72,288,119901,2,0,P|126:282|184:300,1,106.875004076958,8|0,0:0|0:0,0:0:0:0: +192,304,120174,2,0,P|230:306|264:296,1,71.2500027179719 +352,240,120447,6,0,P|416:208|488:228,1,142.500005435944 +428,308,120856,1,0,0:0:0:0: +460,128,120992,101,8,0:0:0:0: +424,36,121129,1,0,0:0:0:0: +360,112,121265,1,0,0:0:0:0: +296,72,121401,1,0,0:0:0:0: +296,72,121469,1,0,0:0:0:0: +296,72,121538,6,0,P|248:96|180:88,1,106.875004076958,4|0,0:0|0:0,0:0:0:0: +194,19,121810,2,0,P|228:5|268:6,1,71.2500027179719 +136,148,122083,102,0,P|106:192|43:219,1,106.875004076958,8|0,0:0|0:0,0:0:0:0: +23,153,122356,2,0,P|45:123|76:103,1,71.2500027179719 +184,256,122629,6,0,P|236:254|284:278,1,106.875004076958 +253,347,122901,2,0,P|218:343|184:329,1,71.2500027179719 +240,164,123174,101,8,0:0:0:0: +204,88,123310,1,0,0:0:0:0: +324,180,123447,5,0,0:0:0:0: +240,164,123583,1,0,0:0:0:0: +292,100,123719,6,0,P|392:132|328:264,1,285,4|0,3:0|0:0,0:0:0:0: +324,180,124674,101,0,0:0:0:0: +324,180,124810,1,0,0:0:0:0: +248,264,124947,1,0,0:0:0:0: +216,152,125083,1,0,0:0:0:0: +136,232,125219,1,0,0:0:0:0: +104,124,125356,1,0,0:0:0:0: +24,200,125492,1,0,0:0:0:0: +72,352,125629,37,0,0:0:0:0: +72,352,125765,1,0,0:0:0:0: +72,352,125901,2,0,P|108:346|152:344,1,71.25,2|0,2:0|0:0,0:0:0:0: +156,324,126174,6,0,P|108:340|48:332,1,104.500003189087 +248,264,126447,2,0,P|300:258|348:274,1,104.500003189087,0|0,0:0|0:0,0:0:0:0: +432,368,126719,1,8,0:0:0:0: +464,248,126856,53,0,0:0:0:0: +292,340,126992,2,0,L|308:184,1,156.750004783631,0|0,0:0|0:0,0:0:0:0: +312,164,127265,2,0,P|300:108|264:76,1,104.500003189087,8|0,0:0|0:0,0:0:0:0: +220,156,127538,1,0,0:0:0:0: +368,76,127674,1,0,0:0:0:0: +200,152,127810,1,8,0:0:0:0: +388,80,127947,53,8,0:0:0:0: +400,96,128015,1,8,0:0:0:0: +412,112,128083,1,8,0:0:0:0: +412,112,128356,54,0,P|348:144|288:96,1,142.5,12|0,0:0|0:0,0:0:0:0: +284,92,128629,2,0,P|238:93|188:88,1,95 +24,176,128901,2,0,P|73:173|120:172,1,95,8|0,0:0|0:0,0:0:0:0: +200,244,129174,53,0,0:0:0:0: +152,384,129310,1,0,0:0:0:0: +304,356,129447,1,8,0:0:0:0: +220,324,129583,1,0,0:0:0:0: +268,188,129719,1,8,0:0:0:0: +288,184,129788,1,0,0:0:0:0: +308,184,129856,1,0,0:0:0:0: +325,188,129924,1,0,0:0:0:0: +342,195,129992,1,2,0:0:0:0: +342,195,130265,54,0,P|320:152|324:100,1,95,4|0,0:0|0:0,0:0:0:0: +428,268,130538,1,8,0:0:0:0: +312,304,130674,1,0,0:0:0:0: +408,92,130810,54,0,P|368:32|284:28,1,142.5,0|0,0:0|0:0,0:0:0:0: +280,32,131083,2,0,P|233:27|180:32,1,95,8|0,0:0|0:0,0:0:0:0: +164,42,131288,1,0,0:0:0:0: +141,54,131356,54,0,B|150:142|150:142|132:163|124:192,1,142.5 +120,204,131629,2,0,P|168:180|208:180,1,95,8|0,0:0|0:0,0:0:0:0: +308,236,131901,53,0,0:0:0:0: +136,320,132038,1,0,0:0:0:0: +228,152,132174,1,8,0:0:0:0: +256,344,132310,1,2,2:0:0:0: +256,344,132379,1,2,2:0:0:0: +256,344,132447,54,0,B|305:335|352:332|352:332|372:280|424:260,1,190,4|8,0:0|0:0,0:0:0:0: +300,256,132856,2,0,P|255:261|204:268,1,95 +120,204,133129,1,0,0:0:0:0: +212,164,133265,53,8,0:0:0:0: +164,20,133401,1,0,0:0:0:0: +312,48,133538,54,0,L|344:88,1,47.5,8|0,0:0|0:0,0:0:0:0: +504,120,133674,2,0,L|453:127,1,47.5 +344,252,133810,2,0,L|362:204,1,47.5 +428,32,133947,2,0,L|409:79,1,47.5,8|0,0:0|0:0,0:0:0:0: +461,234,134083,2,0,L|429:194,1,47.5 +267,161,134219,2,0,L|317:153,1,47.5 +388,140,134356,1,8,0:0:0:0: +394,142,134424,1,0,0:0:0:0: +399,145,134492,1,0,0:0:0:0: +405,148,134560,1,0,0:0:0:0: +411,152,134629,102,0,P|439:208|407:280,1,142.5,4|0,0:0|0:0,0:0:0:0: +372,340,134901,2,0,P|324:341|276:344,1,95,8|0,0:0|0:0,0:0:0:0: +56,308,135174,53,0,0:0:0:0: +200,208,135310,1,8,0:0:0:0: +204,384,135447,1,0,0:0:0:0: +276,244,135583,53,0,0:0:0:0: +128,348,135719,1,0,0:0:0:0: +128,168,135856,2,0,P|168:128|212:124,1,95,0|8,0:0|0:0,0:0:0:0: +20,228,136129,53,0,0:0:0:0: +200,208,136265,1,0,0:0:0:0: +8,128,136401,1,8,0:0:0:0: +186,108,136538,1,0,0:0:0:0: +116,274,136674,1,0,0:0:0:0: +92,64,136810,54,0,B|99:117|112:160|112:160|104:180|96:208,1,142.5,4|0,0:0|0:0,0:0:0:0: +116,274,137083,2,0,P|166:268|212:268,1,95,8|0,0:0|0:0,0:0:0:0: +372,352,137356,1,0,0:0:0:0: +210,267,137492,1,0,0:0:0:0: +372,352,137629,2,0,P|420:340|452:304,1,95,8|0,0:0|0:0,0:0:0:0: +424,216,137901,53,0,0:0:0:0: +448,56,138038,1,0,0:0:0:0: +352,160,138174,5,8,0:0:0:0: +280,16,138310,1,0,0:0:0:0: +264,152,138447,5,0,0:0:0:0: +120,76,138583,1,0,0:0:0:0: +180,188,138719,5,8,0:0:0:0: +24,224,138856,1,0,0:0:0:0: +136,268,138992,70,0,P|184:268|232:272,1,95,4|0,0:0|0:0,0:0:0:0: +332,284,139265,1,8,0:0:0:0: +272,364,139401,53,0,0:0:0:0: +292,188,139538,2,0,P|332:124|408:120,1,142.5 +376,200,139810,2,0,P|425:195|480:188,1,95,8|0,0:0|0:0,0:0:0:0: +368,372,140083,54,0,P|320:368|268:364,1,95 +168,352,140356,1,8,0:0:0:0: +232,276,140492,53,0,0:0:0:0: +220,152,140629,1,0,0:0:0:0: +104,108,140765,1,0,0:0:0:0: +8,188,140901,1,8,0:0:0:0: +4,212,140969,1,0,0:0:0:0: +8,236,141038,1,0,0:0:0:0: +20,256,141106,1,0,0:0:0:0: +38,272,141174,53,0,0:0:0:0: +77,284,141719,53,0,0:0:0:0: +116,292,142265,54,0,B|104:196|8:232|-44:140|4:44|60:20|136:16|172:76|172:76|152:96|152:96|163:120|163:120|144:141|144:141|155:165|155:165|136:188|136:188|156:204|172:236|172:236,1,605.62501155138 +168,224,143356,6,0,P|228:188|304:196,1,142.500005435944,0|2,1:0|0:0,0:0:0:0: +298,193,143697,1,0,0:0:0:0: +298,193,143765,1,0,0:0:0:0: +352,264,143901,1,0,0:0:0:0: +424,212,144038,2,0,P|472:152|464:88,2,142.500005435944,2|2|0,0:0|0:0|0:0,0:0:0:0: +388,132,144719,54,0,L|364:48,1,71.2500027179719,0|0,0:0|0:0,0:0:0:0: +296,112,144992,2,0,P|264:96|224:96,2,71.2500027179719,2|0|0,0:0|0:0|0:0,0:0:0:0: +240,176,145401,53,0,0:0:0:0: +344,308,145538,2,0,P|288:348|216:344,1,142.500005435944,2|0,0:0|0:0,0:0:0:0: +260,260,145947,1,2,0:0:0:0: +172,240,146083,53,0,0:0:0:0: +172,240,146219,1,0,0:0:0:0: +172,240,146356,1,0,0:0:0:0: +172,240,146492,2,0,B|150:244|128:252|128:252|86:238|32:260,1,142.500005435944,2|0,0:0|0:0,0:0:0:0: +124,168,146901,2,0,B|146:164|168:156|168:156|210:170|264:148,1,142.500005435944,2|2,0:0|0:0,0:0:0:0: +276,140,147242,1,0,0:0:0:0: +288,132,147310,1,0,0:0:0:0: +448,180,147447,2,0,B|403:182|360:176|360:176|372:192,1,106.875004076958,2|0,0:0|0:0,0:0:0:0: +380,204,147719,54,0,P|420:264|384:336,1,142.500005435944,4|2,0:0|0:0,0:0:0:0: +397,325,148129,1,0,0:0:0:0: +332,268,148265,1,0,0:0:0:0: +276,332,148401,2,0,P|203:327|128:328,2,142.500005435944,2|2|0,0:0|0:0|0:0,0:0:0:0: +48,264,149083,1,0,0:0:0:0: +48,264,149219,2,0,P|28:196|60:136,2,142.500005435944,2|2|0,0:0|0:0|0:0,0:0:0:0: +112,208,149901,53,2,0:0:0:0: +296,256,150174,1,2,0:0:0:0: +296,256,150242,1,0,0:0:0:0: +296,256,150310,2,0,P|336:252|368:232,1,71.2500027179719,0|0,0:0|0:0,0:0:0:0: +388,72,150583,2,0,P|348:76|316:96,1,71.2500027179719,2|0,0:0|0:0,0:0:0:0: +308,172,150856,2,0,L|460:208,1,142.500005435944,2|0,0:0|0:0,0:0:0:0: +446,204,151265,37,2,0:0:0:0: +446,204,151401,1,0,0:0:0:0: +446,204,151538,5,2,0:0:0:0: +446,204,152083,86,0,P|472:248|464:304,1,95,4|0,0:0|0:0,0:0:0:0: +400,384,152356,1,8,0:0:0:0: +392,264,152492,1,2,2:0:0:0: +392,264,152560,1,2,2:0:0:0: +392,264,152629,2,0,P|348:240|300:240,1,95,2|0,2:0|0:0,0:0:0:0: +328,320,152901,1,8,0:0:0:0: +280,160,153038,1,0,0:0:0:0: +328,320,153174,54,0,L|140:344,1,190,2|8,2:0|0:0,0:0:0:0: +64,296,153583,1,0,0:0:0:0: +176,236,153719,2,2,L|224:244,2,47.5,2|2|2,2:0|2:0|2:0,2:0:0:0: +4,184,153992,1,8,0:0:0:0: +116,124,154129,1,2,2:0:0:0: +116,124,154197,1,2,2:0:0:0: +116,124,154265,54,0,P|136:68|128:32,1,95,4|0,0:0|0:0,0:0:0:0: +16,96,154538,2,0,P|-4:152|4:188,1,95,8|0,0:0|0:0,0:0:0:0: +76,288,154810,2,0,P|124:284|176:284,1,95,2|0,2:0|0:0,0:0:0:0: +292,340,155083,1,8,0:0:0:0: +276,208,155219,1,0,0:0:0:0: +380,312,155356,54,0,P|430:304|488:300,1,95,8|2,0:0|2:0,0:0:0:0: +384,232,155629,2,0,P|434:224|492:220,1,95,8|2,0:0|2:0,0:0:0:0: +312,128,155901,2,0,P|262:120|204:116,1,95,8|2,0:0|2:0,0:0:0:0: +308,48,156174,2,0,P|258:40|200:36,1,95,0|2,0:0|2:0,0:0:0:0: +213,36,156379,1,2,2:0:0:0: +213,36,156447,54,0,P|308:35|408:32,1,190,4|8,0:0|0:0,0:0:0:0: +480,88,156856,1,0,0:0:0:0: +384,232,156992,2,0,P|288:236|236:164,1,190,2|8,2:0|0:0,0:0:0:0: +328,152,157401,1,0,0:0:0:0: +264,324,157538,2,0,P|168:324|64:332,1,190,2|8,2:0|0:0,0:0:0:0: +112,248,157947,1,2,2:0:0:0: +112,248,158015,1,2,2:0:0:0: +112,248,158083,53,2,2:0:0:0: +28,144,158219,1,2,2:0:0:0: +100,32,158356,1,8,0:0:0:0: +228,72,158492,1,2,2:0:0:0: +232,204,158629,54,0,P|264:244|320:256,1,95,4|0,0:0|0:0,0:0:0:0: +484,208,158901,2,0,P|436:189|381:206,1,95,8|0,0:0|0:0,0:0:0:0: +160,328,159174,2,0,P|164:256|216:208,1,142.5,2|2,2:0|2:0,0:0:0:0: +232,204,159447,2,0,P|284:208|320:236,1,95,8|0,0:0|0:0,0:0:0:0: +251,295,159719,54,0,L|215:103,1,190,8|8,0:0|0:0,0:0:0:0: +296,64,160129,1,2,2:0:0:0: +128,152,160265,1,2,2:0:0:0: +105,144,160333,1,2,2:0:0:0: +84,131,160401,1,2,2:0:0:0: +67,114,160469,1,0,2:0:0:0: +54,94,160538,1,8,0:0:0:0: +52,117,160606,1,0,2:0:0:0: +48,140,160674,1,2,2:0:0:0: +37,161,160742,1,2,2:0:0:0: +23,179,160810,6,0,P|5:234|16:292,1,113.999996520996,4|8,0:0|0:0,0:0:0:0: +92,240,161083,2,0,P|103:296|142:341,1,113.999996520996,0|8,0:0|0:0,0:0:0:0: +184,264,161356,2,0,P|222:307|278:326,1,113.999996520996,0|8,0:0|0:0,0:0:0:0: +276,240,161629,2,0,P|331:258|388:247,1,113.999996520996,0|8,0:0|0:0,0:0:0:0: +500,184,161901,54,0,P|518:129|507:71,1,113.999996520996,0|8,0:0|0:0,0:0:0:0: +403,107,162174,2,0,P|391:50|352:5,1,113.999996520996,0|8,0:0|0:0,0:0:0:0: +275,87,162447,2,0,P|236:43|180:24,1,113.999996520996,8|0,0:0|0:0,0:0:0:0: +151,127,162719,2,0,P|96:109|38:120,1,113.999996520996,8|0,0:0|0:0,0:0:0:0: +84,292,162992,53,4,0:0:0:0: +188,228,163129,1,8,0:0:0:0: +200,352,163265,2,0,P|260:344|332:344,1,123.499994346619,0|8,0:0|0:0,0:0:0:0: +512,200,163538,53,0,0:0:0:0: +408,136,163674,1,8,0:0:0:0: +396,260,163810,2,0,P|336:252|264:252,1,123.499994346619,0|8,0:0|0:0,0:0:0:0: +240,80,164083,54,0,P|192:32|128:32,1,123.499994346619,0|8,0:0|0:0,0:0:0:0: +64,176,164356,2,0,P|112:224|176:224,1,123.499994346619,0|8,0:0|0:0,0:0:0:0: +244,72,164629,54,0,P|196:24|132:24,1,123.499994346619,0|8,0:0|0:0,0:0:0:0: +56,184,164901,2,8,P|104:232|168:232,1,123.499994346619,8|8,0:0|0:0,0:0:0:0: +328,128,165174,54,0,P|392:120|448:76,1,132.999995941162,4|8,0:0|0:0,0:0:0:0: +360,212,165447,2,0,B|394:206|416:200|416:200|456:216|500:208,1,132.999995941162,0|8,0:0|0:0,0:0:0:0: +328,300,165719,2,0,P|392:320|452:352,1,132.999995941162,0|8,0:0|0:0,0:0:0:0: +184,300,165992,2,0,P|120:320|60:352,1,132.999995941162,0|8,0:0|0:0,0:0:0:0: +152,212,166265,2,0,B|117:206|96:200|96:200|56:216|12:208,1,132.999995941162,0|8,0:0|0:0,0:0:0:0: +184,128,166538,2,0,P|120:120|64:76,1,132.999995941162,0|8,0:0|0:0,0:0:0:0: +256,80,166810,53,8,0:0:0:0: +256,100,166879,1,0,2:0:0:0: +256,120,166947,1,2,2:0:0:0: +256,140,167015,1,2,2:0:0:0: +256,160,167083,1,2,2:0:0:0: +256,256,167219,1,2,2:0:0:0: +256,256,167288,1,0,2:0:0:0: +256,256,167356,53,4,3:0:0:0: +256,276,167901,53,0,0:0:0:0: +256,296,168447,53,0,0:0:0:0: +256,84,168583,1,8,0:0:0:0: +181,265,168719,1,0,0:0:0:0: +330,115,168856,1,8,0:0:0:0: +150,190,168992,1,0,0:0:0:0: +361,190,169129,1,8,0:0:0:0: +181,115,169265,1,0,0:0:0:0: +330,265,169401,1,8,0:0:0:0: +256,192,169538,38,0,L|344:184,1,80.750001540184,4|0,3:0|0:0,0:0:0:0: +256,192,169810,2,0,L|168:200,1,80.750001540184,4|0,3:0|0:0,0:0:0:0: +256,36,170083,1,4,3:0:0:0: +256,36,170219,1,2,0:0:0:0: +256,36,170356,2,0,L|256:116,1,80.750001540184 +208,260,170629,69,2,0:0:0:0: +191,249,170697,1,0,0:0:0:0: +176,240,170765,1,0,0:0:0:0: +256,192,170901,1,0,0:0:0:0: +304,260,171038,37,2,0:0:0:0: +321,249,171106,1,0,0:0:0:0: +336,240,171174,1,0,0:0:0:0: +256,192,171310,1,0,0:0:0:0: +256,332,171447,101,4,3:0:0:0: diff --git a/osu.Game.Tests/Visual/TestCaseEditorCompose.cs b/osu.Game.Tests/Visual/TestCaseEditorCompose.cs new file mode 100644 index 0000000000..d52f27f4ab --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseEditorCompose.cs @@ -0,0 +1,46 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Allocation; +using osu.Game.Beatmaps; +using osu.Game.Screens.Edit.Screens.Compose; + +namespace osu.Game.Tests.Visual +{ + public class TestCaseEditorCompose : OsuTestCase + { + private readonly Random random; + private readonly Compose compose; + + public TestCaseEditorCompose() + { + random = new Random(1337); + + Add(compose = new Compose()); + AddStep("Next beatmap", nextBeatmap); + } + + private OsuGameBase osuGame; + private BeatmapManager beatmaps; + + [BackgroundDependencyLoader] + private void load(OsuGameBase osuGame, BeatmapManager beatmaps) + { + this.osuGame = osuGame; + this.beatmaps = beatmaps; + + compose.Beatmap.BindTo(osuGame.Beatmap); + } + + private void nextBeatmap() + { + var sets = beatmaps.GetAllUsableBeatmapSets(); + if (sets.Count == 0) + return; + + var b = sets[random.Next(0, sets.Count)].Beatmaps[0]; + osuGame.Beatmap.Value = beatmaps.GetWorkingBeatmap(b); + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseEditorComposeRadioButtons.cs b/osu.Game.Tests/Visual/TestCaseEditorComposeRadioButtons.cs new file mode 100644 index 0000000000..f8669cde4b --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseEditorComposeRadioButtons.cs @@ -0,0 +1,41 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Game.Screens.Edit.Screens.Compose.RadioButtons; + +namespace osu.Game.Tests.Visual +{ + public class TestCaseEditorComposeRadioButtons : OsuTestCase + { + public override IReadOnlyList RequiredTypes => new[] { typeof(DrawableRadioButton) }; + + public TestCaseEditorComposeRadioButtons() + { + RadioButtonCollection collection; + Add(collection = new RadioButtonCollection + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 150, + Items = new[] + { + new RadioButton("Item 1", () => { }), + new RadioButton("Item 2", () => { }), + new RadioButton("Item 3", () => { }), + new RadioButton("Item 4", () => { }), + new RadioButton("Item 5", () => { }) + } + }); + + for (int i = 0; i < collection.Items.Count; i++) + { + int l = i; + AddStep($"Select item {l + 1}", () => collection.Items[l].Select()); + AddStep($"Deselect item {l + 1}", () => collection.Items[l].Deselect()); + } + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseStoryboard.cs b/osu.Game.Tests/Visual/TestCaseStoryboard.cs index 1dad106cbe..0a158f5662 100644 --- a/osu.Game.Tests/Visual/TestCaseStoryboard.cs +++ b/osu.Game.Tests/Visual/TestCaseStoryboard.cs @@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual var decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true }; storyboardContainer.Clock = decoupledClock; - storyboard = working.Beatmap.Storyboard.CreateDrawable(beatmapBacking); + storyboard = working.Storyboard.CreateDrawable(beatmapBacking); storyboard.Passing = false; storyboardContainer.Add(storyboard); diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 312a564f71..96685066b1 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -1,5 +1,6 @@  - + + Debug AnyCPU @@ -19,7 +20,6 @@ 4 false false - 6 true @@ -45,9 +45,6 @@ - - osu.licenseheader - @@ -86,10 +83,11 @@ + - + @@ -105,6 +103,8 @@ + + @@ -147,6 +147,7 @@ + \ No newline at end of file diff --git a/osu.Game.props b/osu.Game.props new file mode 100644 index 0000000000..60a5e97944 --- /dev/null +++ b/osu.Game.props @@ -0,0 +1,13 @@ + + + + + + 7 + + + + osu.licenseheader + + + \ No newline at end of file diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 35b6cc2b02..c8390310d4 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -8,7 +8,6 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps.ControlPoints; using osu.Game.IO.Serialization; -using osu.Game.Storyboards; namespace osu.Game.Beatmaps { @@ -41,11 +40,6 @@ namespace osu.Game.Beatmaps /// public double TotalBreakTime => Breaks.Sum(b => b.Duration); - /// - /// The Beatmap's Storyboard. - /// - public Storyboard Storyboard = new Storyboard(); - /// /// Constructs a new beatmap. /// @@ -57,7 +51,6 @@ namespace osu.Game.Beatmaps Breaks = original?.Breaks ?? Breaks; ComboColors = original?.ComboColors ?? ComboColors; HitObjects = original?.HitObjects ?? HitObjects; - Storyboard = original?.Storyboard ?? Storyboard; if (original == null && Metadata == null) { diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index 962c790fb2..e087eebbfe 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -80,6 +80,7 @@ namespace osu.Game.Beatmaps /// /// Performs the conversion of a hit object. + /// This method is generally executed sequentially for all objects in a beatmap. /// /// The hit object to convert. /// The un-converted Beatmap. diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 0641cabcd8..edbda1a685 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -25,6 +25,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets; +using osu.Game.Storyboards; namespace osu.Game.Beatmaps { @@ -494,7 +495,7 @@ namespace osu.Game.Beatmaps BeatmapMetadata metadata; using (var stream = new StreamReader(reader.GetStream(mapName))) - metadata = BeatmapDecoder.GetDecoder(stream).Decode(stream).Metadata; + metadata = Decoder.GetDecoder(stream).DecodeBeatmap(stream).Metadata; // check if a set already exists with the same online id. beatmapSet = beatmaps.BeatmapSets.FirstOrDefault(b => b.OnlineBeatmapSetID == metadata.OnlineBeatmapSetID) ?? new BeatmapSetInfo @@ -517,8 +518,8 @@ namespace osu.Game.Beatmaps raw.CopyTo(ms); ms.Position = 0; - var decoder = BeatmapDecoder.GetDecoder(sr); - Beatmap beatmap = decoder.Decode(sr); + var decoder = Decoder.GetDecoder(sr); + Beatmap beatmap = decoder.DecodeBeatmap(sr); beatmap.BeatmapInfo.Path = name; beatmap.BeatmapInfo.Hash = ms.ComputeSHA2Hash(); @@ -568,23 +569,11 @@ namespace osu.Game.Beatmaps { try { - Beatmap beatmap; - - BeatmapDecoder decoder; using (var stream = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo.Path)))) { - decoder = BeatmapDecoder.GetDecoder(stream); - beatmap = decoder.Decode(stream); + Decoder decoder = Decoder.GetDecoder(stream); + return decoder.DecodeBeatmap(stream); } - - if (beatmap == null || BeatmapSetInfo.StoryboardFile == null) - return beatmap; - - using (var stream = new StreamReader(store.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile)))) - decoder.Decode(stream, beatmap); - - - return beatmap; } catch { @@ -623,6 +612,28 @@ namespace osu.Game.Beatmaps } protected override Waveform GetWaveform() => new Waveform(store.GetStream(getPathForFile(Metadata.AudioFile))); + + protected override Storyboard GetStoryboard() + { + if (BeatmapInfo?.Path == null && BeatmapSetInfo?.StoryboardFile == null) + return new Storyboard(); + + try + { + Decoder decoder; + using (var stream = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo?.Path)))) + decoder = Decoder.GetDecoder(stream); + + // try for .osb first and fall back to .osu + string storyboardFile = BeatmapSetInfo.StoryboardFile ?? BeatmapInfo.Path; + using (var stream = new StreamReader(store.GetStream(getPathForFile(storyboardFile)))) + return decoder.GetStoryboardDecoder().DecodeStoryboard(stream); + } + catch + { + return new Storyboard(); + } + } } /// diff --git a/osu.Game/Beatmaps/DifficultyCalculator.cs b/osu.Game/Beatmaps/DifficultyCalculator.cs index f58f433cb2..687e1b2177 100644 --- a/osu.Game/Beatmaps/DifficultyCalculator.cs +++ b/osu.Game/Beatmaps/DifficultyCalculator.cs @@ -14,7 +14,7 @@ namespace osu.Game.Beatmaps { protected double TimeRate = 1; - public abstract double Calculate(Dictionary categoryDifficulty = null); + public abstract double Calculate(Dictionary categoryDifficulty = null); } public abstract class DifficultyCalculator : DifficultyCalculator where T : HitObject diff --git a/osu.Game/Beatmaps/Formats/BeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/BeatmapDecoder.cs deleted file mode 100644 index 7e1a87085c..0000000000 --- a/osu.Game/Beatmaps/Formats/BeatmapDecoder.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using System.IO; - -namespace osu.Game.Beatmaps.Formats -{ - public abstract class BeatmapDecoder - { - private static readonly Dictionary decoders = new Dictionary(); - - static BeatmapDecoder() - { - OsuLegacyDecoder.Register(); - } - - public static BeatmapDecoder GetDecoder(StreamReader stream) - { - if (stream == null) - throw new ArgumentNullException(nameof(stream)); - - string line; - do { line = stream.ReadLine()?.Trim(); } - while (line != null && line.Length == 0); - - if (line == null || !decoders.ContainsKey(line)) - throw new IOException(@"Unknown file format"); - return (BeatmapDecoder)Activator.CreateInstance(decoders[line], line); - } - - protected static void AddDecoder(string magic) where T : BeatmapDecoder - { - decoders[magic] = typeof(T); - } - - public virtual Beatmap Decode(StreamReader stream) - { - return ParseFile(stream); - } - - public virtual void Decode(StreamReader stream, Beatmap beatmap) - { - ParseFile(stream, beatmap); - } - - protected virtual Beatmap ParseFile(StreamReader stream) - { - var beatmap = new Beatmap - { - BeatmapInfo = new BeatmapInfo - { - Metadata = new BeatmapMetadata(), - BaseDifficulty = new BeatmapDifficulty(), - }, - }; - - ParseFile(stream, beatmap); - return beatmap; - } - - protected abstract void ParseFile(StreamReader stream, Beatmap beatmap); - } -} diff --git a/osu.Game/Beatmaps/Formats/Decoder.cs b/osu.Game/Beatmaps/Formats/Decoder.cs new file mode 100644 index 0000000000..e157150651 --- /dev/null +++ b/osu.Game/Beatmaps/Formats/Decoder.cs @@ -0,0 +1,80 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.IO; +using osu.Game.Storyboards; + +namespace osu.Game.Beatmaps.Formats +{ + public abstract class Decoder + { + private static readonly Dictionary decoders = new Dictionary(); + + static Decoder() + { + LegacyDecoder.Register(); + } + + /// + /// Retrieves a to parse a . + /// + /// A stream pointing to the . + public static Decoder GetDecoder(StreamReader stream) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + string line; + do + { line = stream.ReadLine()?.Trim(); } + while (line != null && line.Length == 0); + + if (line == null || !decoders.ContainsKey(line)) + throw new IOException(@"Unknown file format"); + return (Decoder)Activator.CreateInstance(decoders[line], line); + } + + /// + /// Adds the to the list of and decoder. + /// + /// Type to decode a with. + /// A string representation of the version. + protected static void AddDecoder(string version) where T : Decoder + { + decoders[version] = typeof(T); + } + + /// + /// Retrieves a to parse a + /// + public abstract Decoder GetStoryboardDecoder(); + + public virtual Beatmap DecodeBeatmap(StreamReader stream) + { + var beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + Metadata = new BeatmapMetadata(), + BaseDifficulty = new BeatmapDifficulty(), + }, + }; + + ParseBeatmap(stream, beatmap); + return beatmap; + } + + protected abstract void ParseBeatmap(StreamReader stream, Beatmap beatmap); + + public virtual Storyboard DecodeStoryboard(StreamReader stream) + { + var storyboard = new Storyboard(); + ParseStoryboard(stream, storyboard); + return storyboard; + } + + protected abstract void ParseStoryboard(StreamReader stream, Storyboard storyboard); + } +} diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs new file mode 100644 index 0000000000..b7004dd3eb --- /dev/null +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -0,0 +1,421 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Globalization; +using System.IO; +using OpenTK.Graphics; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Objects.Legacy; +using osu.Game.Beatmaps.ControlPoints; +using System.Collections.Generic; + +namespace osu.Game.Beatmaps.Formats +{ + public class LegacyBeatmapDecoder : LegacyDecoder + { + private Beatmap beatmap; + + private bool hasCustomColours; + private ConvertHitObjectParser parser; + + private LegacySampleBank defaultSampleBank; + private int defaultSampleVolume = 100; + + public LegacyBeatmapDecoder() + { + } + + public LegacyBeatmapDecoder(string header) + { + BeatmapVersion = int.Parse(header.Substring(17)); + } + + protected override void ParseBeatmap(StreamReader stream, Beatmap beatmap) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + if (beatmap == null) + throw new ArgumentNullException(nameof(beatmap)); + + this.beatmap = beatmap; + this.beatmap.BeatmapInfo.BeatmapVersion = BeatmapVersion; + + ParseContent(stream); + + foreach (var hitObject in this.beatmap.HitObjects) + hitObject.ApplyDefaults(this.beatmap.ControlPointInfo, this.beatmap.BeatmapInfo.BaseDifficulty); + } + + protected override bool ShouldSkipLine(string line) + { + if (base.ShouldSkipLine(line) || line.StartsWith(" ") || line.StartsWith("_")) + return true; + return false; + } + + protected override void ProcessSection(Section section, string line) + { + switch (section) + { + case Section.General: + handleGeneral(line); + break; + case Section.Editor: + handleEditor(line); + break; + case Section.Metadata: + handleMetadata(line); + break; + case Section.Difficulty: + handleDifficulty(line); + break; + case Section.Events: + handleEvents(line); + break; + case Section.TimingPoints: + handleTimingPoints(line); + break; + case Section.Colours: + handleColours(line); + break; + case Section.HitObjects: + handleHitObjects(line); + break; + case Section.Variables: + handleVariables(line); + break; + } + } + + private void handleGeneral(string line) + { + var pair = splitKeyVal(line, ':'); + + var metadata = beatmap.BeatmapInfo.Metadata; + switch (pair.Key) + { + case @"AudioFilename": + metadata.AudioFile = pair.Value; + break; + case @"AudioLeadIn": + beatmap.BeatmapInfo.AudioLeadIn = int.Parse(pair.Value); + break; + case @"PreviewTime": + metadata.PreviewTime = int.Parse(pair.Value); + break; + case @"Countdown": + beatmap.BeatmapInfo.Countdown = int.Parse(pair.Value) == 1; + break; + case @"SampleSet": + defaultSampleBank = (LegacySampleBank)Enum.Parse(typeof(LegacySampleBank), pair.Value); + break; + case @"SampleVolume": + defaultSampleVolume = int.Parse(pair.Value); + break; + case @"StackLeniency": + beatmap.BeatmapInfo.StackLeniency = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + break; + case @"Mode": + beatmap.BeatmapInfo.RulesetID = int.Parse(pair.Value); + + switch (beatmap.BeatmapInfo.RulesetID) + { + case 0: + parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(); + break; + case 1: + parser = new Rulesets.Objects.Legacy.Taiko.ConvertHitObjectParser(); + break; + case 2: + parser = new Rulesets.Objects.Legacy.Catch.ConvertHitObjectParser(); + break; + case 3: + parser = new Rulesets.Objects.Legacy.Mania.ConvertHitObjectParser(); + break; + } + break; + case @"LetterboxInBreaks": + beatmap.BeatmapInfo.LetterboxInBreaks = int.Parse(pair.Value) == 1; + break; + case @"SpecialStyle": + beatmap.BeatmapInfo.SpecialStyle = int.Parse(pair.Value) == 1; + break; + case @"WidescreenStoryboard": + beatmap.BeatmapInfo.WidescreenStoryboard = int.Parse(pair.Value) == 1; + break; + } + } + + private void handleEditor(string line) + { + var pair = splitKeyVal(line, ':'); + + switch (pair.Key) + { + case @"Bookmarks": + beatmap.BeatmapInfo.StoredBookmarks = pair.Value; + break; + case @"DistanceSpacing": + beatmap.BeatmapInfo.DistanceSpacing = double.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + break; + case @"BeatDivisor": + beatmap.BeatmapInfo.BeatDivisor = int.Parse(pair.Value); + break; + case @"GridSize": + beatmap.BeatmapInfo.GridSize = int.Parse(pair.Value); + break; + case @"TimelineZoom": + beatmap.BeatmapInfo.TimelineZoom = double.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + break; + } + } + + private void handleMetadata(string line) + { + var pair = splitKeyVal(line, ':'); + + var metadata = beatmap.BeatmapInfo.Metadata; + switch (pair.Key) + { + case @"Title": + metadata.Title = pair.Value; + break; + case @"TitleUnicode": + metadata.TitleUnicode = pair.Value; + break; + case @"Artist": + metadata.Artist = pair.Value; + break; + case @"ArtistUnicode": + metadata.ArtistUnicode = pair.Value; + break; + case @"Creator": + metadata.AuthorString = pair.Value; + break; + case @"Version": + beatmap.BeatmapInfo.Version = pair.Value; + break; + case @"Source": + beatmap.BeatmapInfo.Metadata.Source = pair.Value; + break; + case @"Tags": + beatmap.BeatmapInfo.Metadata.Tags = pair.Value; + break; + case @"BeatmapID": + beatmap.BeatmapInfo.OnlineBeatmapID = int.Parse(pair.Value); + break; + case @"BeatmapSetID": + beatmap.BeatmapInfo.OnlineBeatmapSetID = int.Parse(pair.Value); + metadata.OnlineBeatmapSetID = int.Parse(pair.Value); + break; + } + } + + private void handleDifficulty(string line) + { + var pair = splitKeyVal(line, ':'); + + var difficulty = beatmap.BeatmapInfo.BaseDifficulty; + switch (pair.Key) + { + case @"HPDrainRate": + difficulty.DrainRate = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + break; + case @"CircleSize": + difficulty.CircleSize = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + break; + case @"OverallDifficulty": + difficulty.OverallDifficulty = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + break; + case @"ApproachRate": + difficulty.ApproachRate = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + break; + case @"SliderMultiplier": + difficulty.SliderMultiplier = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + break; + case @"SliderTickRate": + difficulty.SliderTickRate = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + break; + } + } + + private void handleEvents(string line) + { + DecodeVariables(ref line); + + string[] split = line.Split(','); + + EventType type; + if (!Enum.TryParse(split[0], out type)) + throw new InvalidDataException($@"Unknown event type {split[0]}"); + + switch (type) + { + case EventType.Background: + string filename = split[2].Trim('"'); + beatmap.BeatmapInfo.Metadata.BackgroundFile = filename; + break; + case EventType.Break: + var breakEvent = new BreakPeriod + { + StartTime = double.Parse(split[1], NumberFormatInfo.InvariantInfo), + EndTime = double.Parse(split[2], NumberFormatInfo.InvariantInfo) + }; + + if (!breakEvent.HasEffect) + return; + + beatmap.Breaks.Add(breakEvent); + break; + } + } + + private void handleTimingPoints(string line) + { + string[] split = line.Split(','); + + double time = double.Parse(split[0].Trim(), NumberFormatInfo.InvariantInfo); + double beatLength = double.Parse(split[1].Trim(), NumberFormatInfo.InvariantInfo); + double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1; + + TimeSignatures timeSignature = TimeSignatures.SimpleQuadruple; + if (split.Length >= 3) + timeSignature = split[2][0] == '0' ? TimeSignatures.SimpleQuadruple : (TimeSignatures)int.Parse(split[2]); + + LegacySampleBank sampleSet = defaultSampleBank; + if (split.Length >= 4) + sampleSet = (LegacySampleBank)int.Parse(split[3]); + + //SampleBank sampleBank = SampleBank.Default; + //if (split.Length >= 5) + // sampleBank = (SampleBank)int.Parse(split[4]); + + int sampleVolume = defaultSampleVolume; + if (split.Length >= 6) + sampleVolume = int.Parse(split[5]); + + bool timingChange = true; + if (split.Length >= 7) + timingChange = split[6][0] == '1'; + + bool kiaiMode = false; + bool omitFirstBarSignature = false; + if (split.Length >= 8) + { + int effectFlags = int.Parse(split[7]); + kiaiMode = (effectFlags & 1) > 0; + omitFirstBarSignature = (effectFlags & 8) > 0; + } + + string stringSampleSet = sampleSet.ToString().ToLower(); + if (stringSampleSet == @"none") + stringSampleSet = @"normal"; + + DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(time); + SoundControlPoint soundPoint = beatmap.ControlPointInfo.SoundPointAt(time); + EffectControlPoint effectPoint = beatmap.ControlPointInfo.EffectPointAt(time); + + if (timingChange) + { + beatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint + { + Time = time, + BeatLength = beatLength, + TimeSignature = timeSignature + }); + } + + if (speedMultiplier != difficultyPoint.SpeedMultiplier) + { + beatmap.ControlPointInfo.DifficultyPoints.RemoveAll(x => x.Time == time); + beatmap.ControlPointInfo.DifficultyPoints.Add(new DifficultyControlPoint + { + Time = time, + SpeedMultiplier = speedMultiplier + }); + } + + if (stringSampleSet != soundPoint.SampleBank || sampleVolume != soundPoint.SampleVolume) + { + beatmap.ControlPointInfo.SoundPoints.Add(new SoundControlPoint + { + Time = time, + SampleBank = stringSampleSet, + SampleVolume = sampleVolume + }); + } + + if (kiaiMode != effectPoint.KiaiMode || omitFirstBarSignature != effectPoint.OmitFirstBarLine) + { + beatmap.ControlPointInfo.EffectPoints.Add(new EffectControlPoint + { + Time = time, + KiaiMode = kiaiMode, + OmitFirstBarLine = omitFirstBarSignature + }); + } + } + + private void handleColours(string line) + { + var pair = splitKeyVal(line, ':'); + + string[] split = pair.Value.Split(','); + + if (split.Length != 3) + throw new InvalidOperationException($@"Color specified in incorrect format (should be R,G,B): {pair.Value}"); + + byte r, g, b; + if (!byte.TryParse(split[0], out r) || !byte.TryParse(split[1], out g) || !byte.TryParse(split[2], out b)) + throw new InvalidOperationException(@"Color must be specified with 8-bit integer components"); + + if (!hasCustomColours) + { + beatmap.ComboColors.Clear(); + hasCustomColours = true; + } + + // Note: the combo index specified in the beatmap is discarded + if (pair.Key.StartsWith(@"Combo")) + { + beatmap.ComboColors.Add(new Color4 + { + R = r / 255f, + G = g / 255f, + B = b / 255f, + A = 1f, + }); + } + } + + private void handleHitObjects(string line) + { + // If the ruleset wasn't specified, assume the osu!standard ruleset. + if (parser == null) + parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(); + + var obj = parser.Parse(line); + + if (obj != null) + beatmap.HitObjects.Add(obj); + } + + private void handleVariables(string line) + { + var pair = splitKeyVal(line, '='); + Variables[pair.Key] = pair.Value; + } + + private KeyValuePair splitKeyVal(string line, char separator) + { + var split = line.Trim().Split(new[] { separator }, 2); + + return new KeyValuePair + ( + split[0].Trim(), + split.Length > 1 ? split[1].Trim() : string.Empty + ); + } + } +} diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs new file mode 100644 index 0000000000..96747a870d --- /dev/null +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -0,0 +1,163 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.IO; +using osu.Game.Beatmaps.Legacy; +using osu.Game.Storyboards; + +namespace osu.Game.Beatmaps.Formats +{ + public abstract class LegacyDecoder : Decoder + { + public static void Register() + { + AddDecoder(@"osu file format v14"); + AddDecoder(@"osu file format v13"); + AddDecoder(@"osu file format v12"); + AddDecoder(@"osu file format v11"); + AddDecoder(@"osu file format v10"); + AddDecoder(@"osu file format v9"); + AddDecoder(@"osu file format v8"); + AddDecoder(@"osu file format v7"); + AddDecoder(@"osu file format v6"); + AddDecoder(@"osu file format v5"); + AddDecoder(@"osu file format v4"); + AddDecoder(@"osu file format v3"); + // TODO: differences between versions + } + + protected int BeatmapVersion; + protected readonly Dictionary Variables = new Dictionary(); + + public override Decoder GetStoryboardDecoder() => new LegacyStoryboardDecoder(BeatmapVersion); + + public override Beatmap DecodeBeatmap(StreamReader stream) => new LegacyBeatmap(base.DecodeBeatmap(stream)); + + protected override void ParseBeatmap(StreamReader stream, Beatmap beatmap) + { + throw new NotImplementedException(); + } + + protected override void ParseStoryboard(StreamReader stream, Storyboard storyboard) + { + throw new NotImplementedException(); + } + + protected void ParseContent(StreamReader stream) + { + Section section = Section.None; + + string line; + while ((line = stream.ReadLine()) != null) + { + if (ShouldSkipLine(line)) + continue; + + // It's already set in ParseBeatmap... why do it again? + //if (line.StartsWith(@"osu file format v")) + //{ + // Beatmap.BeatmapInfo.BeatmapVersion = int.Parse(line.Substring(17)); + // continue; + //} + + if (line.StartsWith(@"[") && line.EndsWith(@"]")) + { + if (!Enum.TryParse(line.Substring(1, line.Length - 2), out section)) + throw new InvalidDataException($@"Unknown osu section {line}"); + continue; + } + + ProcessSection(section, line); + } + } + + protected virtual bool ShouldSkipLine(string line) + { + if (string.IsNullOrWhiteSpace(line) || line.StartsWith("//")) + return true; + return false; + } + + protected abstract void ProcessSection(Section section, string line); + + /// + /// Decodes any beatmap variables present in a line into their real values. + /// + /// The line which may contains variables. + protected void DecodeVariables(ref string line) + { + while (line.IndexOf('$') >= 0) + { + string origLine = line; + string[] split = line.Split(','); + for (int i = 0; i < split.Length; i++) + { + var item = split[i]; + if (item.StartsWith("$") && Variables.ContainsKey(item)) + split[i] = Variables[item]; + } + + line = string.Join(",", split); + if (line == origLine) + break; + } + } + + protected enum Section + { + None, + General, + Editor, + Metadata, + Difficulty, + Events, + TimingPoints, + Colours, + HitObjects, + Variables, + } + + internal enum LegacySampleBank + { + None = 0, + Normal = 1, + Soft = 2, + Drum = 3 + } + + internal enum EventType + { + Background = 0, + Video = 1, + Break = 2, + Colour = 3, + Sprite = 4, + Sample = 5, + Animation = 6 + } + + internal enum LegacyOrigins + { + TopLeft, + Centre, + CentreLeft, + TopRight, + BottomCentre, + TopCentre, + Custom, + CentreRight, + BottomLeft, + BottomRight + }; + + internal enum StoryLayer + { + Background = 0, + Fail = 1, + Pass = 2, + Foreground = 3 + } + } +} diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs new file mode 100644 index 0000000000..8da6a0cefb --- /dev/null +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -0,0 +1,271 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Globalization; +using System.IO; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Graphics; +using osu.Framework.IO.File; +using osu.Game.Storyboards; + +namespace osu.Game.Beatmaps.Formats +{ + public class LegacyStoryboardDecoder : LegacyDecoder + { + private Storyboard storyboard; + + private StoryboardSprite storyboardSprite; + private CommandTimelineGroup timelineGroup; + + public LegacyStoryboardDecoder() + { + } + + public LegacyStoryboardDecoder(int beatmapVersion) + { + BeatmapVersion = beatmapVersion; + } + + protected override void ParseStoryboard(StreamReader stream, Storyboard storyboard) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + if (storyboard == null) + throw new ArgumentNullException(nameof(storyboard)); + + this.storyboard = storyboard; + + ParseContent(stream); + } + + protected override void ProcessSection(Section section, string line) + { + switch (section) + { + case Section.Events: + handleEvents(line); + break; + } + } + + private void handleEvents(string line) + { + var depth = 0; + while (line.StartsWith(" ") || line.StartsWith("_")) + { + ++depth; + line = line.Substring(1); + } + + DecodeVariables(ref line); + + string[] split = line.Split(','); + + if (depth == 0) + { + storyboardSprite = null; + + EventType type; + if (!Enum.TryParse(split[0], out type)) + throw new InvalidDataException($@"Unknown event type {split[0]}"); + + switch (type) + { + case EventType.Sprite: + { + var layer = parseLayer(split[1]); + var origin = parseOrigin(split[2]); + var path = cleanFilename(split[3]); + var x = float.Parse(split[4], NumberFormatInfo.InvariantInfo); + var y = float.Parse(split[5], NumberFormatInfo.InvariantInfo); + storyboardSprite = new StoryboardSprite(path, origin, new Vector2(x, y)); + storyboard.GetLayer(layer).Add(storyboardSprite); + } + break; + case EventType.Animation: + { + var layer = parseLayer(split[1]); + var origin = parseOrigin(split[2]); + var path = cleanFilename(split[3]); + var x = float.Parse(split[4], NumberFormatInfo.InvariantInfo); + var y = float.Parse(split[5], NumberFormatInfo.InvariantInfo); + var frameCount = int.Parse(split[6]); + var frameDelay = double.Parse(split[7], NumberFormatInfo.InvariantInfo); + var loopType = split.Length > 8 ? (AnimationLoopType)Enum.Parse(typeof(AnimationLoopType), split[8]) : AnimationLoopType.LoopForever; + storyboardSprite = new StoryboardAnimation(path, origin, new Vector2(x, y), frameCount, frameDelay, loopType); + storyboard.GetLayer(layer).Add(storyboardSprite); + } + break; + case EventType.Sample: + { + var time = double.Parse(split[1], CultureInfo.InvariantCulture); + var layer = parseLayer(split[2]); + var path = cleanFilename(split[3]); + var volume = split.Length > 4 ? float.Parse(split[4], CultureInfo.InvariantCulture) : 100; + storyboard.GetLayer(layer).Add(new StoryboardSample(path, time, volume)); + } + break; + } + } + else + { + if (depth < 2) + timelineGroup = storyboardSprite?.TimelineGroup; + + var commandType = split[0]; + switch (commandType) + { + case "T": + { + var triggerName = split[1]; + var startTime = split.Length > 2 ? double.Parse(split[2], CultureInfo.InvariantCulture) : double.MinValue; + var endTime = split.Length > 3 ? double.Parse(split[3], CultureInfo.InvariantCulture) : double.MaxValue; + var groupNumber = split.Length > 4 ? int.Parse(split[4]) : 0; + timelineGroup = storyboardSprite?.AddTrigger(triggerName, startTime, endTime, groupNumber); + } + break; + case "L": + { + var startTime = double.Parse(split[1], CultureInfo.InvariantCulture); + var loopCount = int.Parse(split[2]); + timelineGroup = storyboardSprite?.AddLoop(startTime, loopCount); + } + break; + default: + { + if (string.IsNullOrEmpty(split[3])) + split[3] = split[2]; + + var easing = (Easing)int.Parse(split[1]); + var startTime = double.Parse(split[2], CultureInfo.InvariantCulture); + var endTime = double.Parse(split[3], CultureInfo.InvariantCulture); + + switch (commandType) + { + case "F": + { + var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); + var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + timelineGroup?.Alpha.Add(easing, startTime, endTime, startValue, endValue); + } + break; + case "S": + { + var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); + var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + timelineGroup?.Scale.Add(easing, startTime, endTime, new Vector2(startValue), new Vector2(endValue)); + } + break; + case "V": + { + var startX = float.Parse(split[4], CultureInfo.InvariantCulture); + var startY = float.Parse(split[5], CultureInfo.InvariantCulture); + var endX = split.Length > 6 ? float.Parse(split[6], CultureInfo.InvariantCulture) : startX; + var endY = split.Length > 7 ? float.Parse(split[7], CultureInfo.InvariantCulture) : startY; + timelineGroup?.Scale.Add(easing, startTime, endTime, new Vector2(startX, startY), new Vector2(endX, endY)); + } + break; + case "R": + { + var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); + var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + timelineGroup?.Rotation.Add(easing, startTime, endTime, MathHelper.RadiansToDegrees(startValue), MathHelper.RadiansToDegrees(endValue)); + } + break; + case "M": + { + var startX = float.Parse(split[4], CultureInfo.InvariantCulture); + var startY = float.Parse(split[5], CultureInfo.InvariantCulture); + var endX = split.Length > 6 ? float.Parse(split[6], CultureInfo.InvariantCulture) : startX; + var endY = split.Length > 7 ? float.Parse(split[7], CultureInfo.InvariantCulture) : startY; + timelineGroup?.X.Add(easing, startTime, endTime, startX, endX); + timelineGroup?.Y.Add(easing, startTime, endTime, startY, endY); + } + break; + case "MX": + { + var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); + var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + timelineGroup?.X.Add(easing, startTime, endTime, startValue, endValue); + } + break; + case "MY": + { + var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); + var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + timelineGroup?.Y.Add(easing, startTime, endTime, startValue, endValue); + } + break; + case "C": + { + var startRed = float.Parse(split[4], CultureInfo.InvariantCulture); + var startGreen = float.Parse(split[5], CultureInfo.InvariantCulture); + var startBlue = float.Parse(split[6], CultureInfo.InvariantCulture); + var endRed = split.Length > 7 ? float.Parse(split[7], CultureInfo.InvariantCulture) : startRed; + var endGreen = split.Length > 8 ? float.Parse(split[8], CultureInfo.InvariantCulture) : startGreen; + var endBlue = split.Length > 9 ? float.Parse(split[9], CultureInfo.InvariantCulture) : startBlue; + timelineGroup?.Colour.Add(easing, startTime, endTime, + new Color4(startRed / 255f, startGreen / 255f, startBlue / 255f, 1), + new Color4(endRed / 255f, endGreen / 255f, endBlue / 255f, 1)); + } + break; + case "P": + { + var type = split[4]; + switch (type) + { + case "A": + timelineGroup?.BlendingMode.Add(easing, startTime, endTime, BlendingMode.Additive, startTime == endTime ? BlendingMode.Additive : BlendingMode.Inherit); + break; + case "H": + timelineGroup?.FlipH.Add(easing, startTime, endTime, true, startTime == endTime); + break; + case "V": + timelineGroup?.FlipV.Add(easing, startTime, endTime, true, startTime == endTime); + break; + } + } + break; + default: + throw new InvalidDataException($@"Unknown command type: {commandType}"); + } + } + break; + } + } + } + + private string parseLayer(string value) => Enum.Parse(typeof(StoryLayer), value).ToString(); + + private Anchor parseOrigin(string value) + { + var origin = (LegacyOrigins)Enum.Parse(typeof(LegacyOrigins), value); + switch (origin) + { + case LegacyOrigins.TopLeft: + return Anchor.TopLeft; + case LegacyOrigins.TopCentre: + return Anchor.TopCentre; + case LegacyOrigins.TopRight: + return Anchor.TopRight; + case LegacyOrigins.CentreLeft: + return Anchor.CentreLeft; + case LegacyOrigins.Centre: + return Anchor.Centre; + case LegacyOrigins.CentreRight: + return Anchor.CentreRight; + case LegacyOrigins.BottomLeft: + return Anchor.BottomLeft; + case LegacyOrigins.BottomCentre: + return Anchor.BottomCentre; + case LegacyOrigins.BottomRight: + return Anchor.BottomRight; + } + throw new InvalidDataException($@"Unknown origin: {value}"); + } + + private string cleanFilename(string path) => FileSafety.PathStandardise(path.Trim('\"')); + } +} diff --git a/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs b/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs deleted file mode 100644 index 11631e9447..0000000000 --- a/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs +++ /dev/null @@ -1,781 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using OpenTK.Graphics; -using osu.Game.Beatmaps.Timing; -using osu.Game.Beatmaps.Legacy; -using osu.Game.Rulesets.Objects.Legacy; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Storyboards; -using OpenTK; -using osu.Framework.Graphics; -using osu.Framework.IO.File; - -namespace osu.Game.Beatmaps.Formats -{ - public class OsuLegacyDecoder : BeatmapDecoder - { - public static void Register() - { - AddDecoder(@"osu file format v14"); - AddDecoder(@"osu file format v13"); - AddDecoder(@"osu file format v12"); - AddDecoder(@"osu file format v11"); - AddDecoder(@"osu file format v10"); - AddDecoder(@"osu file format v9"); - AddDecoder(@"osu file format v8"); - AddDecoder(@"osu file format v7"); - AddDecoder(@"osu file format v6"); - AddDecoder(@"osu file format v5"); - AddDecoder(@"osu file format v4"); - AddDecoder(@"osu file format v3"); - // TODO: differences between versions - } - - private ConvertHitObjectParser parser; - - private readonly Dictionary variables = new Dictionary(); - - private LegacySampleBank defaultSampleBank; - private int defaultSampleVolume = 100; - - private readonly int beatmapVersion; - - public OsuLegacyDecoder() - { - } - - public OsuLegacyDecoder(string header) - { - beatmapVersion = int.Parse(header.Substring(17)); - } - - private enum Section - { - None, - General, - Editor, - Metadata, - Difficulty, - Events, - TimingPoints, - Colours, - HitObjects, - Variables, - } - - private void handleGeneral(Beatmap beatmap, string line) - { - if (beatmap == null) - throw new ArgumentNullException(nameof(beatmap)); - if (line == null) - throw new ArgumentNullException(nameof(line)); - - var pair = splitKeyVal(line, ':'); - - var metadata = beatmap.BeatmapInfo.Metadata; - switch (pair.Key) - { - case @"AudioFilename": - metadata.AudioFile = pair.Value; - break; - case @"AudioLeadIn": - beatmap.BeatmapInfo.AudioLeadIn = int.Parse(pair.Value); - break; - case @"PreviewTime": - metadata.PreviewTime = int.Parse(pair.Value); - break; - case @"Countdown": - beatmap.BeatmapInfo.Countdown = int.Parse(pair.Value) == 1; - break; - case @"SampleSet": - defaultSampleBank = (LegacySampleBank)Enum.Parse(typeof(LegacySampleBank), pair.Value); - break; - case @"SampleVolume": - defaultSampleVolume = int.Parse(pair.Value); - break; - case @"StackLeniency": - beatmap.BeatmapInfo.StackLeniency = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); - break; - case @"Mode": - beatmap.BeatmapInfo.RulesetID = int.Parse(pair.Value); - - switch (beatmap.BeatmapInfo.RulesetID) - { - case 0: - parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(); - break; - case 1: - parser = new Rulesets.Objects.Legacy.Taiko.ConvertHitObjectParser(); - break; - case 2: - parser = new Rulesets.Objects.Legacy.Catch.ConvertHitObjectParser(); - break; - case 3: - parser = new Rulesets.Objects.Legacy.Mania.ConvertHitObjectParser(); - break; - } - break; - case @"LetterboxInBreaks": - beatmap.BeatmapInfo.LetterboxInBreaks = int.Parse(pair.Value) == 1; - break; - case @"SpecialStyle": - beatmap.BeatmapInfo.SpecialStyle = int.Parse(pair.Value) == 1; - break; - case @"WidescreenStoryboard": - beatmap.BeatmapInfo.WidescreenStoryboard = int.Parse(pair.Value) == 1; - break; - } - } - - private void handleEditor(Beatmap beatmap, string line) - { - if (beatmap == null) - throw new ArgumentNullException(nameof(beatmap)); - if (line == null) - throw new ArgumentNullException(nameof(line)); - - var pair = splitKeyVal(line, ':'); - - switch (pair.Key) - { - case @"Bookmarks": - beatmap.BeatmapInfo.StoredBookmarks = pair.Value; - break; - case @"DistanceSpacing": - beatmap.BeatmapInfo.DistanceSpacing = double.Parse(pair.Value, NumberFormatInfo.InvariantInfo); - break; - case @"BeatDivisor": - beatmap.BeatmapInfo.BeatDivisor = int.Parse(pair.Value); - break; - case @"GridSize": - beatmap.BeatmapInfo.GridSize = int.Parse(pair.Value); - break; - case @"TimelineZoom": - beatmap.BeatmapInfo.TimelineZoom = double.Parse(pair.Value, NumberFormatInfo.InvariantInfo); - break; - } - } - - private void handleMetadata(Beatmap beatmap, string line) - { - if (beatmap == null) - throw new ArgumentNullException(nameof(beatmap)); - if (line == null) - throw new ArgumentNullException(nameof(line)); - - var pair = splitKeyVal(line, ':'); - - var metadata = beatmap.BeatmapInfo.Metadata; - switch (pair.Key) - { - case @"Title": - metadata.Title = pair.Value; - break; - case @"TitleUnicode": - metadata.TitleUnicode = pair.Value; - break; - case @"Artist": - metadata.Artist = pair.Value; - break; - case @"ArtistUnicode": - metadata.ArtistUnicode = pair.Value; - break; - case @"Creator": - metadata.AuthorString = pair.Value; - break; - case @"Version": - beatmap.BeatmapInfo.Version = pair.Value; - break; - case @"Source": - beatmap.BeatmapInfo.Metadata.Source = pair.Value; - break; - case @"Tags": - beatmap.BeatmapInfo.Metadata.Tags = pair.Value; - break; - case @"BeatmapID": - beatmap.BeatmapInfo.OnlineBeatmapID = int.Parse(pair.Value); - break; - case @"BeatmapSetID": - beatmap.BeatmapInfo.OnlineBeatmapSetID = int.Parse(pair.Value); - metadata.OnlineBeatmapSetID = int.Parse(pair.Value); - break; - } - } - - private void handleDifficulty(Beatmap beatmap, string line) - { - if (beatmap == null) - throw new ArgumentNullException(nameof(beatmap)); - if (line == null) - throw new ArgumentNullException(nameof(line)); - - var pair = splitKeyVal(line, ':'); - - var difficulty = beatmap.BeatmapInfo.BaseDifficulty; - switch (pair.Key) - { - case @"HPDrainRate": - difficulty.DrainRate = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); - break; - case @"CircleSize": - difficulty.CircleSize = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); - break; - case @"OverallDifficulty": - difficulty.OverallDifficulty = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); - break; - case @"ApproachRate": - difficulty.ApproachRate = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); - break; - case @"SliderMultiplier": - difficulty.SliderMultiplier = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); - break; - case @"SliderTickRate": - difficulty.SliderTickRate = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); - break; - } - } - - /// - /// Decodes any beatmap variables present in a line into their real values. - /// - /// The line which may contains variables. - private void decodeVariables(ref string line) - { - if (line == null) - throw new ArgumentNullException(nameof(line)); - - while (line.IndexOf('$') >= 0) - { - string origLine = line; - string[] split = line.Split(','); - for (int i = 0; i < split.Length; i++) - { - var item = split[i]; - if (item.StartsWith("$") && variables.ContainsKey(item)) - split[i] = variables[item]; - } - - line = string.Join(",", split); - if (line == origLine) break; - } - } - - private void handleEvents(Beatmap beatmap, string line, ref StoryboardSprite storyboardSprite, ref CommandTimelineGroup timelineGroup) - { - if (line == null) - throw new ArgumentNullException(nameof(line)); - if (beatmap == null) - throw new ArgumentNullException(nameof(beatmap)); - - var depth = 0; - while (line.StartsWith(" ") || line.StartsWith("_")) - { - ++depth; - line = line.Substring(1); - } - - decodeVariables(ref line); - - string[] split = line.Split(','); - - if (depth == 0) - { - storyboardSprite = null; - - EventType type; - if (!Enum.TryParse(split[0], out type)) - throw new InvalidDataException($@"Unknown event type {split[0]}"); - - switch (type) - { - case EventType.Video: - case EventType.Background: - string filename = split[2].Trim('"'); - - if (type == EventType.Background) - beatmap.BeatmapInfo.Metadata.BackgroundFile = filename; - - break; - case EventType.Break: - var breakEvent = new BreakPeriod - { - StartTime = double.Parse(split[1], NumberFormatInfo.InvariantInfo), - EndTime = double.Parse(split[2], NumberFormatInfo.InvariantInfo) - }; - - if (!breakEvent.HasEffect) - return; - - beatmap.Breaks.Add(breakEvent); - break; - case EventType.Sprite: - { - var layer = parseLayer(split[1]); - var origin = parseOrigin(split[2]); - var path = cleanFilename(split[3]); - var x = float.Parse(split[4], NumberFormatInfo.InvariantInfo); - var y = float.Parse(split[5], NumberFormatInfo.InvariantInfo); - storyboardSprite = new StoryboardSprite(path, origin, new Vector2(x, y)); - beatmap.Storyboard.GetLayer(layer).Add(storyboardSprite); - } - break; - case EventType.Animation: - { - var layer = parseLayer(split[1]); - var origin = parseOrigin(split[2]); - var path = cleanFilename(split[3]); - var x = float.Parse(split[4], NumberFormatInfo.InvariantInfo); - var y = float.Parse(split[5], NumberFormatInfo.InvariantInfo); - var frameCount = int.Parse(split[6]); - var frameDelay = double.Parse(split[7], NumberFormatInfo.InvariantInfo); - var loopType = split.Length > 8 ? (AnimationLoopType)Enum.Parse(typeof(AnimationLoopType), split[8]) : AnimationLoopType.LoopForever; - storyboardSprite = new StoryboardAnimation(path, origin, new Vector2(x, y), frameCount, frameDelay, loopType); - beatmap.Storyboard.GetLayer(layer).Add(storyboardSprite); - } - break; - case EventType.Sample: - { - var time = double.Parse(split[1], CultureInfo.InvariantCulture); - var layer = parseLayer(split[2]); - var path = cleanFilename(split[3]); - var volume = split.Length > 4 ? float.Parse(split[4], CultureInfo.InvariantCulture) : 100; - beatmap.Storyboard.GetLayer(layer).Add(new StoryboardSample(path, time, volume)); - } - break; - } - } - else - { - if (depth < 2) - timelineGroup = storyboardSprite?.TimelineGroup; - - var commandType = split[0]; - switch (commandType) - { - case "T": - { - var triggerName = split[1]; - var startTime = split.Length > 2 ? double.Parse(split[2], CultureInfo.InvariantCulture) : double.MinValue; - var endTime = split.Length > 3 ? double.Parse(split[3], CultureInfo.InvariantCulture) : double.MaxValue; - var groupNumber = split.Length > 4 ? int.Parse(split[4]) : 0; - timelineGroup = storyboardSprite?.AddTrigger(triggerName, startTime, endTime, groupNumber); - } - break; - case "L": - { - var startTime = double.Parse(split[1], CultureInfo.InvariantCulture); - var loopCount = int.Parse(split[2]); - timelineGroup = storyboardSprite?.AddLoop(startTime, loopCount); - } - break; - default: - { - if (string.IsNullOrEmpty(split[3])) - split[3] = split[2]; - - var easing = (Easing)int.Parse(split[1]); - var startTime = double.Parse(split[2], CultureInfo.InvariantCulture); - var endTime = double.Parse(split[3], CultureInfo.InvariantCulture); - - switch (commandType) - { - case "F": - { - var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); - var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; - timelineGroup?.Alpha.Add(easing, startTime, endTime, startValue, endValue); - } - break; - case "S": - { - var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); - var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; - timelineGroup?.Scale.Add(easing, startTime, endTime, new Vector2(startValue), new Vector2(endValue)); - } - break; - case "V": - { - var startX = float.Parse(split[4], CultureInfo.InvariantCulture); - var startY = float.Parse(split[5], CultureInfo.InvariantCulture); - var endX = split.Length > 6 ? float.Parse(split[6], CultureInfo.InvariantCulture) : startX; - var endY = split.Length > 7 ? float.Parse(split[7], CultureInfo.InvariantCulture) : startY; - timelineGroup?.Scale.Add(easing, startTime, endTime, new Vector2(startX, startY), new Vector2(endX, endY)); - } - break; - case "R": - { - var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); - var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; - timelineGroup?.Rotation.Add(easing, startTime, endTime, MathHelper.RadiansToDegrees(startValue), MathHelper.RadiansToDegrees(endValue)); - } - break; - case "M": - { - var startX = float.Parse(split[4], CultureInfo.InvariantCulture); - var startY = float.Parse(split[5], CultureInfo.InvariantCulture); - var endX = split.Length > 6 ? float.Parse(split[6], CultureInfo.InvariantCulture) : startX; - var endY = split.Length > 7 ? float.Parse(split[7], CultureInfo.InvariantCulture) : startY; - timelineGroup?.X.Add(easing, startTime, endTime, startX, endX); - timelineGroup?.Y.Add(easing, startTime, endTime, startY, endY); - } - break; - case "MX": - { - var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); - var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; - timelineGroup?.X.Add(easing, startTime, endTime, startValue, endValue); - } - break; - case "MY": - { - var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); - var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; - timelineGroup?.Y.Add(easing, startTime, endTime, startValue, endValue); - } - break; - case "C": - { - var startRed = float.Parse(split[4], CultureInfo.InvariantCulture); - var startGreen = float.Parse(split[5], CultureInfo.InvariantCulture); - var startBlue = float.Parse(split[6], CultureInfo.InvariantCulture); - var endRed = split.Length > 7 ? float.Parse(split[7], CultureInfo.InvariantCulture) : startRed; - var endGreen = split.Length > 8 ? float.Parse(split[8], CultureInfo.InvariantCulture) : startGreen; - var endBlue = split.Length > 9 ? float.Parse(split[9], CultureInfo.InvariantCulture) : startBlue; - timelineGroup?.Colour.Add(easing, startTime, endTime, - new Color4(startRed / 255f, startGreen / 255f, startBlue / 255f, 1), - new Color4(endRed / 255f, endGreen / 255f, endBlue / 255f, 1)); - } - break; - case "P": - { - var type = split[4]; - switch (type) - { - case "A": timelineGroup?.BlendingMode.Add(easing, startTime, endTime, BlendingMode.Additive, startTime == endTime ? BlendingMode.Additive : BlendingMode.Inherit); break; - case "H": timelineGroup?.FlipH.Add(easing, startTime, endTime, true, startTime == endTime); break; - case "V": timelineGroup?.FlipV.Add(easing, startTime, endTime, true, startTime == endTime); break; - } - } - break; - default: - throw new InvalidDataException($@"Unknown command type: {commandType}"); - } - } - break; - } - } - } - - private static string cleanFilename(string path) - => FileSafety.PathStandardise(path.Trim('\"')); - - private static Anchor parseOrigin(string value) - { - var origin = (LegacyOrigins)Enum.Parse(typeof(LegacyOrigins), value); - switch (origin) - { - case LegacyOrigins.TopLeft: return Anchor.TopLeft; - case LegacyOrigins.TopCentre: return Anchor.TopCentre; - case LegacyOrigins.TopRight: return Anchor.TopRight; - case LegacyOrigins.CentreLeft: return Anchor.CentreLeft; - case LegacyOrigins.Centre: return Anchor.Centre; - case LegacyOrigins.CentreRight: return Anchor.CentreRight; - case LegacyOrigins.BottomLeft: return Anchor.BottomLeft; - case LegacyOrigins.BottomCentre: return Anchor.BottomCentre; - case LegacyOrigins.BottomRight: return Anchor.BottomRight; - } - throw new InvalidDataException($@"Unknown origin: {value}"); - } - - private static string parseLayer(string value) - => Enum.Parse(typeof(StoryLayer), value).ToString(); - - private void handleTimingPoints(Beatmap beatmap, string line) - { - if (beatmap == null) - throw new ArgumentNullException(nameof(beatmap)); - if (line == null) - throw new ArgumentNullException(nameof(line)); - - string[] split = line.Split(','); - - double time = double.Parse(split[0].Trim(), NumberFormatInfo.InvariantInfo); - double beatLength = double.Parse(split[1].Trim(), NumberFormatInfo.InvariantInfo); - double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1; - - TimeSignatures timeSignature = TimeSignatures.SimpleQuadruple; - if (split.Length >= 3) - timeSignature = split[2][0] == '0' ? TimeSignatures.SimpleQuadruple : (TimeSignatures)int.Parse(split[2]); - - LegacySampleBank sampleSet = defaultSampleBank; - if (split.Length >= 4) - sampleSet = (LegacySampleBank)int.Parse(split[3]); - - //SampleBank sampleBank = SampleBank.Default; - //if (split.Length >= 5) - // sampleBank = (SampleBank)int.Parse(split[4]); - - int sampleVolume = defaultSampleVolume; - if (split.Length >= 6) - sampleVolume = int.Parse(split[5]); - - bool timingChange = true; - if (split.Length >= 7) - timingChange = split[6][0] == '1'; - - bool kiaiMode = false; - bool omitFirstBarSignature = false; - if (split.Length >= 8) - { - int effectFlags = int.Parse(split[7]); - kiaiMode = (effectFlags & 1) > 0; - omitFirstBarSignature = (effectFlags & 8) > 0; - } - - string stringSampleSet = sampleSet.ToString().ToLower(); - if (stringSampleSet == @"none") - stringSampleSet = @"normal"; - - DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(time); - SoundControlPoint soundPoint = beatmap.ControlPointInfo.SoundPointAt(time); - EffectControlPoint effectPoint = beatmap.ControlPointInfo.EffectPointAt(time); - - if (timingChange) - { - beatmap.ControlPointInfo.TimingPoints.Add(new TimingControlPoint - { - Time = time, - BeatLength = beatLength, - TimeSignature = timeSignature - }); - } - - if (speedMultiplier != difficultyPoint.SpeedMultiplier) - { - beatmap.ControlPointInfo.DifficultyPoints.RemoveAll(x => x.Time == time); - beatmap.ControlPointInfo.DifficultyPoints.Add(new DifficultyControlPoint - { - Time = time, - SpeedMultiplier = speedMultiplier - }); - } - - if (stringSampleSet != soundPoint.SampleBank || sampleVolume != soundPoint.SampleVolume) - { - beatmap.ControlPointInfo.SoundPoints.Add(new SoundControlPoint - { - Time = time, - SampleBank = stringSampleSet, - SampleVolume = sampleVolume - }); - } - - if (kiaiMode != effectPoint.KiaiMode || omitFirstBarSignature != effectPoint.OmitFirstBarLine) - { - beatmap.ControlPointInfo.EffectPoints.Add(new EffectControlPoint - { - Time = time, - KiaiMode = kiaiMode, - OmitFirstBarLine = omitFirstBarSignature - }); - } - } - - private void handleColours(Beatmap beatmap, string line, ref bool hasCustomColours) - { - if (beatmap == null) - throw new ArgumentNullException(nameof(beatmap)); - if (line == null) - throw new ArgumentNullException(nameof(line)); - - var pair = splitKeyVal(line, ':'); - - string[] split = pair.Value.Split(','); - - if (split.Length != 3) - throw new InvalidOperationException($@"Color specified in incorrect format (should be R,G,B): {pair.Value}"); - - byte r, g, b; - if (!byte.TryParse(split[0], out r) || !byte.TryParse(split[1], out g) || !byte.TryParse(split[2], out b)) - throw new InvalidOperationException(@"Color must be specified with 8-bit integer components"); - - if (!hasCustomColours) - { - beatmap.ComboColors.Clear(); - hasCustomColours = true; - } - - // Note: the combo index specified in the beatmap is discarded - if (pair.Key.StartsWith(@"Combo")) - { - beatmap.ComboColors.Add(new Color4 - { - R = r / 255f, - G = g / 255f, - B = b / 255f, - A = 1f, - }); - } - } - - private void handleVariables(string line) - { - if (line == null) - throw new ArgumentNullException(nameof(line)); - - var pair = splitKeyVal(line, '='); - variables[pair.Key] = pair.Value; - } - - protected override Beatmap ParseFile(StreamReader stream) - { - return new LegacyBeatmap(base.ParseFile(stream)); - } - - public override Beatmap Decode(StreamReader stream) - { - return new LegacyBeatmap(base.Decode(stream)); - } - - protected override void ParseFile(StreamReader stream, Beatmap beatmap) - { - if (beatmap == null) - throw new ArgumentNullException(nameof(beatmap)); - if (stream == null) - throw new ArgumentNullException(nameof(stream)); - - beatmap.BeatmapInfo.BeatmapVersion = beatmapVersion; - - Section section = Section.None; - bool hasCustomColours = false; - StoryboardSprite storyboardSprite = null; - CommandTimelineGroup timelineGroup = null; - - string line; - while ((line = stream.ReadLine()) != null) - { - if (string.IsNullOrWhiteSpace(line)) - continue; - - if (line.StartsWith("//")) - continue; - - if (line.StartsWith(@"osu file format v")) - { - beatmap.BeatmapInfo.BeatmapVersion = int.Parse(line.Substring(17)); - continue; - } - - if (line.StartsWith(@"[") && line.EndsWith(@"]")) - { - if (!Enum.TryParse(line.Substring(1, line.Length - 2), out section)) - throw new InvalidDataException($@"Unknown osu section {line}"); - continue; - } - - switch (section) - { - case Section.General: - handleGeneral(beatmap, line); - break; - case Section.Editor: - handleEditor(beatmap, line); - break; - case Section.Metadata: - handleMetadata(beatmap, line); - break; - case Section.Difficulty: - handleDifficulty(beatmap, line); - break; - case Section.Events: - handleEvents(beatmap, line, ref storyboardSprite, ref timelineGroup); - break; - case Section.TimingPoints: - handleTimingPoints(beatmap, line); - break; - case Section.Colours: - handleColours(beatmap, line, ref hasCustomColours); - break; - case Section.HitObjects: - - // If the ruleset wasn't specified, assume the osu!standard ruleset. - if (parser == null) - parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(); - - var obj = parser.Parse(line); - - if (obj != null) - beatmap.HitObjects.Add(obj); - - break; - case Section.Variables: - handleVariables(line); - break; - } - } - - foreach (var hitObject in beatmap.HitObjects) - hitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty); - } - - private KeyValuePair splitKeyVal(string line, char separator) - { - if (line == null) - throw new ArgumentNullException(nameof(line)); - - var split = line.Trim().Split(new[] { separator }, 2); - - return new KeyValuePair - ( - split[0].Trim(), - split.Length > 1 ? split[1].Trim() : string.Empty - ); - } - - internal enum LegacySampleBank - { - None = 0, - Normal = 1, - Soft = 2, - Drum = 3 - } - - internal enum EventType - { - Background = 0, - Video = 1, - Break = 2, - Colour = 3, - Sprite = 4, - Sample = 5, - Animation = 6 - } - - internal enum LegacyOrigins - { - TopLeft, - Centre, - CentreLeft, - TopRight, - BottomCentre, - TopCentre, - Custom, - CentreRight, - BottomLeft, - BottomRight - }; - - internal enum StoryLayer - { - Background = 0, - Fail = 1, - Pass = 2, - Foreground = 3 - } - } -} diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 2a8178882e..736cc2a0b0 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using osu.Game.Storyboards; namespace osu.Game.Beatmaps { @@ -31,17 +32,19 @@ namespace osu.Game.Beatmaps Mods.ValueChanged += mods => applyRateAdjustments(); beatmap = new AsyncLazy(populateBeatmap); - background = new AsyncLazy(populateBackground); + background = new AsyncLazy(populateBackground, b => b == null || !b.IsDisposed); track = new AsyncLazy(populateTrack); waveform = new AsyncLazy(populateWaveform); + storyboard = new AsyncLazy(populateStoryboard); } protected abstract Beatmap GetBeatmap(); protected abstract Texture GetBackground(); protected abstract Track GetTrack(); protected virtual Waveform GetWaveform() => new Waveform(); + protected virtual Storyboard GetStoryboard() => new Storyboard(); - public bool BeatmapLoaded => beatmap.IsValueCreated; + public bool BeatmapLoaded => beatmap.IsResultAvailable; public Beatmap Beatmap => beatmap.Value.Result; public async Task GetBeatmapAsync() => await beatmap.Value; @@ -57,14 +60,14 @@ namespace osu.Game.Beatmaps return b; } - public bool BackgroundLoaded => background.IsValueCreated; + public bool BackgroundLoaded => background.IsResultAvailable; public Texture Background => background.Value.Result; public async Task GetBackgroundAsync() => await background.Value; private AsyncLazy background; private Texture populateBackground() => GetBackground(); - public bool TrackLoaded => track.IsValueCreated; + public bool TrackLoaded => track.IsResultAvailable; public Track Track => track.Value.Result; public async Task GetTrackAsync() => await track.Value; private AsyncLazy track; @@ -77,19 +80,26 @@ namespace osu.Game.Beatmaps return t; } - public bool WaveformLoaded => waveform.IsValueCreated; + public bool WaveformLoaded => waveform.IsResultAvailable; public Waveform Waveform => waveform.Value.Result; public async Task GetWaveformAsync() => await waveform.Value; private readonly AsyncLazy waveform; private Waveform populateWaveform() => GetWaveform(); + public bool StoryboardLoaded => storyboard.IsResultAvailable; + public Storyboard Storyboard => storyboard.Value.Result; + public async Task GetStoryboardAsync() => await storyboard.Value; + private readonly AsyncLazy storyboard; + + private Storyboard populateStoryboard() => GetStoryboard(); + public void TransferTo(WorkingBeatmap other) { - if (track.IsValueCreated && Track != null && BeatmapInfo.AudioEquals(other.BeatmapInfo)) + if (track.IsResultAvailable && Track != null && BeatmapInfo.AudioEquals(other.BeatmapInfo)) other.track = track; - if (background.IsValueCreated && Background != null && BeatmapInfo.BackgroundEquals(other.BeatmapInfo)) + if (background.IsResultAvailable && Background != null && BeatmapInfo.BackgroundEquals(other.BeatmapInfo)) other.background = background; } @@ -97,16 +107,18 @@ namespace osu.Game.Beatmaps { if (BackgroundLoaded) Background?.Dispose(); if (WaveformLoaded) Waveform?.Dispose(); + if (StoryboardLoaded) Storyboard?.Dispose(); } - public void DisposeTrack() - { - if (TrackLoaded) Track?.Dispose(); - } + /// + /// Eagerly dispose of the audio track associated with this (if any). + /// Accessing track again will load a fresh instance. + /// + public void RecycleTrack() => track.Recycle(); private void applyRateAdjustments(Track t = null) { - if (t == null && track.IsValueCreated) t = Track; + if (t == null && track.IsResultAvailable) t = Track; if (t == null) return; t.ResetSpeedAdjustments(); @@ -114,12 +126,65 @@ namespace osu.Game.Beatmaps mod.ApplyToClock(t); } - public class AsyncLazy : Lazy> + public class AsyncLazy { - public AsyncLazy(Func valueFactory) - : base(() => Task.Run(valueFactory)) + private Lazy> lazy; + private readonly Func valueFactory; + private readonly Func stillValidFunction; + + private readonly object initLock = new object(); + + public AsyncLazy(Func valueFactory, Func stillValidFunction = null) { + this.valueFactory = valueFactory; + this.stillValidFunction = stillValidFunction; + + recreate(); } + + public void Recycle() + { + if (!IsResultAvailable) return; + + (lazy.Value.Result as IDisposable)?.Dispose(); + recreate(); + } + + public bool IsResultAvailable + { + get + { + recreateIfInvalid(); + return lazy.Value.IsCompleted; + } + } + + public Task Value + { + get + { + recreateIfInvalid(); + return lazy.Value; + } + } + + private void recreateIfInvalid() + { + lock (initLock) + { + if (!lazy.IsValueCreated || !lazy.Value.IsCompleted) + // we have not yet been initialised or haven't run the task. + return; + + if (stillValidFunction?.Invoke(lazy.Value.Result) ?? true) + // we are still in a valid state. + return; + + recreate(); + } + } + + private void recreate() => lazy = new Lazy>(() => Task.Run(valueFactory)); } } } diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index c087a5afb7..1a7d29e907 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -22,7 +22,7 @@ namespace osu.Game.Configuration Set(OsuSetting.SelectionRandomType, SelectionRandomType.RandomPermutation); - Set(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2, 1, 0.01); + Set(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2, 1); // Online settings Set(OsuSetting.Username, string.Empty); diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingInputManager.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingInputManager.cs index bae14fc1dc..784e6462f2 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingInputManager.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingInputManager.cs @@ -14,7 +14,7 @@ namespace osu.Game.Input.Bindings /// A KeyBindingInputManager with a database backing for custom overrides. /// /// The type of the custom action. - public abstract class DatabasedKeyBindingInputManager : KeyBindingInputManager + public class DatabasedKeyBindingInputManager : KeyBindingContainer where T : struct { private readonly RulesetInfo ruleset; @@ -31,7 +31,7 @@ namespace osu.Game.Input.Bindings /// A reference to identify the current . Used to lookup mappings. Null for global mappings. /// An optional variant for the specified . Used when a ruleset has more than one possible keyboard layouts. /// Specify how to deal with multiple matches of s and s. - protected DatabasedKeyBindingInputManager(RulesetInfo ruleset = null, int? variant = null, SimultaneousBindingMode simultaneousMode = SimultaneousBindingMode.None) + public DatabasedKeyBindingInputManager(RulesetInfo ruleset = null, int? variant = null, SimultaneousBindingMode simultaneousMode = SimultaneousBindingMode.None) : base(simultaneousMode) { this.ruleset = ruleset; diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs index 95791391f0..9e2988417a 100644 --- a/osu.Game/Input/KeyBindingStore.cs +++ b/osu.Game/Input/KeyBindingStore.cs @@ -27,7 +27,7 @@ namespace osu.Game.Input } } - public void Register(KeyBindingInputManager manager) => insertDefaults(manager.DefaultKeyBindings); + public void Register(KeyBindingContainer manager) => insertDefaults(manager.DefaultKeyBindings); private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) { diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 8eaa20f781..0ddff5e5aa 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -154,7 +154,7 @@ namespace osu.Game Debug.Assert(lastBeatmap != null); Debug.Assert(lastBeatmap.Track != null); - lastBeatmap.DisposeTrack(); + lastBeatmap.RecycleTrack(); } Audio.Track.AddItem(b.Track); diff --git a/osu.Game/Overlays/BeatmapSet/DownloadButton.cs b/osu.Game/Overlays/BeatmapSet/DownloadButton.cs index 18a0cfd968..47787d2ced 100644 --- a/osu.Game/Overlays/BeatmapSet/DownloadButton.cs +++ b/osu.Game/Overlays/BeatmapSet/DownloadButton.cs @@ -14,10 +14,10 @@ namespace osu.Game.Overlays.BeatmapSet public DownloadButton(string title, string subtitle) { Width = 120; - RelativeSizeAxes = Axes.Y; - Child = new Container + Add(new Container { + Depth = -1, RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Horizontal = 10 }, Children = new Drawable[] @@ -53,7 +53,7 @@ namespace osu.Game.Overlays.BeatmapSet Margin = new MarginPadding { Right = 5 }, }, }, - }; + }); } } } diff --git a/osu.Game/Overlays/BeatmapSet/FavouriteButton.cs b/osu.Game/Overlays/BeatmapSet/FavouriteButton.cs index 9fd4ac177c..1b22853656 100644 --- a/osu.Game/Overlays/BeatmapSet/FavouriteButton.cs +++ b/osu.Game/Overlays/BeatmapSet/FavouriteButton.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -15,13 +16,12 @@ namespace osu.Game.Overlays.BeatmapSet { public readonly Bindable Favourited = new Bindable(); - public FavouriteButton() + [BackgroundDependencyLoader] + private void load() { - RelativeSizeAxes = Axes.Y; - Container pink; SpriteIcon icon; - Children = new Drawable[] + AddRange(new Drawable[] { pink = new Container { @@ -51,7 +51,7 @@ namespace osu.Game.Overlays.BeatmapSet Size = new Vector2(18), Shadow = false, }, - }; + }); Favourited.ValueChanged += value => { diff --git a/osu.Game/Overlays/BeatmapSet/HeaderButton.cs b/osu.Game/Overlays/BeatmapSet/HeaderButton.cs index 3075020fe6..ac5683de00 100644 --- a/osu.Game/Overlays/BeatmapSet/HeaderButton.cs +++ b/osu.Game/Overlays/BeatmapSet/HeaderButton.cs @@ -2,44 +2,27 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; -using osu.Game.Graphics.Backgrounds; -using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Framework.Allocation; namespace osu.Game.Overlays.BeatmapSet { - public class HeaderButton : OsuClickableContainer + public class HeaderButton : TriangleButton { - private readonly Container content; - - protected override Container Content => content; - public HeaderButton() { - CornerRadius = 3; - Masking = true; + Height = 0; + RelativeSizeAxes = Axes.Y; + } - InternalChildren = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = OsuColour.FromHex(@"094c5f"), - }, - new Triangles - { - RelativeSizeAxes = Axes.Both, - ColourLight = OsuColour.FromHex(@"0f7c9b"), - ColourDark = OsuColour.FromHex(@"094c5f"), - TriangleScale = 1.5f, - }, - content = new Container - { - RelativeSizeAxes = Axes.Both, - }, - }; + [BackgroundDependencyLoader] + private void load() + { + BackgroundColour = OsuColour.FromHex(@"094c5f"); + Triangles.ColourLight = OsuColour.FromHex(@"0f7c9b"); + Triangles.ColourDark = OsuColour.FromHex(@"094c5f"); + Triangles.TriangleScale = 1.5f; } } } diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs index b49ac269a9..0b7a30797d 100644 --- a/osu.Game/Overlays/DirectOverlay.cs +++ b/osu.Game/Overlays/DirectOverlay.cs @@ -220,7 +220,11 @@ namespace osu.Game.Overlays switch (displayStyle) { case PanelDisplayStyle.Grid: - return new DirectGridPanel(b); + return new DirectGridPanel(b) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }; default: return new DirectListPanel(b); } diff --git a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs index 8ebd4ac545..4a7e4f4e6e 100644 --- a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs +++ b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs @@ -12,7 +12,7 @@ namespace osu.Game.Overlays.KeyBinding public override FontAwesome Icon => FontAwesome.fa_osu_hot; public override string Header => "Global"; - public GlobalKeyBindingsSection(KeyBindingInputManager manager) + public GlobalKeyBindingsSection(KeyBindingContainer manager) { Add(new DefaultBindingsSubsection(manager)); } @@ -21,7 +21,7 @@ namespace osu.Game.Overlays.KeyBinding { protected override string Header => string.Empty; - public DefaultBindingsSubsection(KeyBindingInputManager manager) + public DefaultBindingsSubsection(KeyBindingContainer manager) : base(null) { Defaults = manager.DefaultKeyBindings; diff --git a/osu.Game/Overlays/Music/PlaylistList.cs b/osu.Game/Overlays/Music/PlaylistList.cs index af01cdc451..245b2d36ce 100644 --- a/osu.Game/Overlays/Music/PlaylistList.cs +++ b/osu.Game/Overlays/Music/PlaylistList.cs @@ -35,7 +35,11 @@ namespace osu.Game.Overlays.Music set { base.Padding = value; } } - public IEnumerable BeatmapSets { set { items.Sets = value; } } + public IEnumerable BeatmapSets + { + get { return items.Sets; } + set { items.Sets = value; } + } public BeatmapSetInfo FirstVisibleSet => items.FirstVisibleSet; public BeatmapSetInfo NextSet => items.NextSet; @@ -48,7 +52,7 @@ namespace osu.Game.Overlays.Music } public void AddBeatmapSet(BeatmapSetInfo beatmapSet) => items.AddBeatmapSet(beatmapSet); - public bool RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => items.RemoveBeatmapSet(beatmapSet); + public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => items.RemoveBeatmapSet(beatmapSet); public void Filter(string searchTerm) => items.SearchTerm = searchTerm; @@ -81,6 +85,7 @@ namespace osu.Game.Overlays.Music public IEnumerable Sets { + get { return items.Select(x => x.BeatmapSetInfo).ToList(); } set { items.Clear(); @@ -103,12 +108,11 @@ namespace osu.Game.Overlays.Music }); } - public bool RemoveBeatmapSet(BeatmapSetInfo beatmapSet) + public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) { var itemToRemove = items.FirstOrDefault(i => i.BeatmapSetInfo.ID == beatmapSet.ID); - if (itemToRemove == null) - return false; - return items.Remove(itemToRemove); + if (itemToRemove != null) + items.Remove(itemToRemove); } public BeatmapSetInfo SelectedSet @@ -230,6 +234,7 @@ namespace osu.Game.Overlays.Music private class ItemSearchContainer : FillFlowContainer, IHasFilterableChildren { public IEnumerable FilterTerms => new string[] { }; + public bool MatchingFilter { set diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index d05ad85726..23bec53014 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -13,6 +13,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using OpenTK; using OpenTK.Graphics; +using System.Threading; namespace osu.Game.Overlays.Music { @@ -29,7 +30,7 @@ namespace osu.Game.Overlays.Music private readonly Bindable beatmapBacking = new Bindable(); - public IEnumerable BeatmapSets; + public IEnumerable BeatmapSets => list.BeatmapSets; [BackgroundDependencyLoader] private void load(OsuGameBase game, BeatmapManager beatmaps, OsuColour colours) @@ -74,11 +75,10 @@ namespace osu.Game.Overlays.Music }, }; - beatmaps.BeatmapSetAdded += s => Schedule(() => list.AddBeatmapSet(s)); - beatmaps.BeatmapSetRemoved += s => Schedule(() => list.RemoveBeatmapSet(s)); - - list.BeatmapSets = BeatmapSets = beatmaps.GetAllUsableBeatmapSets(); + beatmaps.BeatmapSetAdded += list.AddBeatmapSet; + beatmaps.BeatmapSetRemoved += list.RemoveBeatmapSet; + list.BeatmapSets = beatmaps.GetAllUsableBeatmapSets(); beatmapBacking.BindTo(game.Beatmap); @@ -121,7 +121,7 @@ namespace osu.Game.Overlays.Music return; } - playSpecified(set.Beatmaps[0]); + playSpecified(set.Beatmaps.First()); } public void PlayPrevious() @@ -130,7 +130,7 @@ namespace osu.Game.Overlays.Music if (playable != null) { - playSpecified(playable.Beatmaps[0]); + playSpecified(playable.Beatmaps.First()); list.SelectedSet = playable; } } @@ -141,7 +141,7 @@ namespace osu.Game.Overlays.Music if (playable != null) { - playSpecified(playable.Beatmaps[0]); + playSpecified(playable.Beatmaps.First()); list.SelectedSet = playable; } } @@ -149,7 +149,15 @@ namespace osu.Game.Overlays.Music private void playSpecified(BeatmapInfo info) { beatmapBacking.Value = beatmaps.GetWorkingBeatmap(info, beatmapBacking); - beatmapBacking.Value.Track.Start(); + + var track = beatmapBacking.Value.Track; + + track.Restart(); + + // this is temporary until we have blocking (async.Wait()) audio component methods. + // then we can call RestartAsync().Wait() or the blocking version above. + while (!track.IsRunning) + Thread.Sleep(1); } } diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 4f57ea1bcd..b30ee8f6fc 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -251,7 +251,7 @@ namespace osu.Game.Overlays playButton.Icon = track.IsRunning ? FontAwesome.fa_pause_circle_o : FontAwesome.fa_play_circle_o; - if (track.HasCompleted && !track.Looping && !beatmapBacking.Disabled) + if (track.HasCompleted && !beatmapBacking.Disabled && playlist.BeatmapSets.Any()) next(); } else diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index 5ebac37cc8..392bc6f1bd 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -97,6 +97,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input doubleValue.ValueChanged += newValue => base.Bindable.Value = newValue; } } + + public SensitivitySetting() + { + KeyboardStep = 0.01f; + } } private class SensitivitySlider : OsuSliderBar @@ -105,8 +110,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input public SensitivitySlider() { - KeyboardStep = 0.01f; - Current.ValueChanged += newValue => { if (!isDragging && Sensitivity != null) @@ -133,4 +136,4 @@ namespace osu.Game.Overlays.Settings.Sections.Input public override string TooltipText => Current.Disabled ? "Enable raw input to adjust sensitivity" : Current.Value.ToString(@"0.##x"); } } -} \ No newline at end of file +} diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs new file mode 100644 index 0000000000..3184b84e98 --- /dev/null +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -0,0 +1,109 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Logging; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.Edit.Screens.Compose.RadioButtons; + +namespace osu.Game.Rulesets.Edit +{ + public abstract class HitObjectComposer : CompositeDrawable + { + private readonly Ruleset ruleset; + + protected ICompositionTool CurrentTool { get; private set; } + + protected HitObjectComposer(Ruleset ruleset) + { + this.ruleset = ruleset; + + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(OsuGameBase osuGame) + { + RulesetContainer rulesetContainer; + try + { + rulesetContainer = CreateRulesetContainer(ruleset, osuGame.Beatmap.Value); + } + catch (Exception e) + { + Logger.Log($"Could not load this beatmap sucessfully ({e})!", LoggingTarget.Runtime, LogLevel.Error); + return; + } + + RadioButtonCollection toolboxCollection; + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new FillFlowContainer + { + Name = "Sidebar", + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = 10 }, + Children = new Drawable[] + { + new ToolboxGroup { Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X } } + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderColour = Color4.White, + BorderThickness = 2, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true, + }, + rulesetContainer + } + } + }, + }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 200), + } + }; + + rulesetContainer.Clock = new InterpolatingFramedClock((IAdjustableClock)osuGame.Beatmap.Value.Track ?? new StopwatchClock()); + + toolboxCollection.Items = + new[] { new RadioButton("Select", () => setCompositionTool(null)) } + .Concat( + CompositionTools.Select(t => new RadioButton(t.Name, () => setCompositionTool(t))) + ) + .ToList(); + + toolboxCollection.Items[0].Select(); + } + + private void setCompositionTool(ICompositionTool tool) => CurrentTool = tool; + + protected virtual RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => ruleset.CreateRulesetContainerWith(beatmap, true); + + protected abstract IReadOnlyList CompositionTools { get; } + } +} diff --git a/osu.Game/Rulesets/Edit/ToolboxGroup.cs b/osu.Game/Rulesets/Edit/ToolboxGroup.cs new file mode 100644 index 0000000000..70e4d3a0c5 --- /dev/null +++ b/osu.Game/Rulesets/Edit/ToolboxGroup.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Game.Screens.Play.ReplaySettings; + +namespace osu.Game.Rulesets.Edit +{ + public class ToolboxGroup : ReplayGroup + { + protected override string Title => "toolbox"; + + public ToolboxGroup() + { + RelativeSizeAxes = Axes.X; + Width = 1; + } + } +} diff --git a/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs b/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs new file mode 100644 index 0000000000..dd182dcbdb --- /dev/null +++ b/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Edit.Tools +{ + public class HitObjectCompositionTool : ICompositionTool + where T : HitObject + { + public string Name => typeof(T).Name; + } +} diff --git a/osu.Game/Rulesets/Edit/Tools/ICompositionTool.cs b/osu.Game/Rulesets/Edit/Tools/ICompositionTool.cs new file mode 100644 index 0000000000..eba873f0cf --- /dev/null +++ b/osu.Game/Rulesets/Edit/Tools/ICompositionTool.cs @@ -0,0 +1,10 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Rulesets.Edit.Tools +{ + public interface ICompositionTool + { + string Name { get; } + } +} diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index d4f9c7191a..0d7d617405 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -177,8 +177,8 @@ namespace osu.Game.Rulesets.Objects.Legacy string[] split = str.Split(':'); - var bank = (OsuLegacyDecoder.LegacySampleBank)Convert.ToInt32(split[0]); - var addbank = (OsuLegacyDecoder.LegacySampleBank)Convert.ToInt32(split[1]); + var bank = (LegacyDecoder.LegacySampleBank)Convert.ToInt32(split[0]); + var addbank = (LegacyDecoder.LegacySampleBank)Convert.ToInt32(split[1]); // Let's not implement this for now, because this doesn't fit nicely into the bank structure //string sampleFile = split2.Length > 4 ? split2[4] : string.Empty; diff --git a/osu.Game/Rulesets/Replays/ReplayFrame.cs b/osu.Game/Rulesets/Replays/ReplayFrame.cs index b0f62e5271..02c969f648 100644 --- a/osu.Game/Rulesets/Replays/ReplayFrame.cs +++ b/osu.Game/Rulesets/Replays/ReplayFrame.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Replays { public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0); - public bool IsImportant => MouseX.HasValue && MouseY.HasValue && (MouseLeft || MouseRight); + public virtual bool IsImportant => MouseX.HasValue && MouseY.HasValue && (MouseLeft || MouseRight); public float? MouseX; public float? MouseY; @@ -68,4 +68,4 @@ namespace osu.Game.Rulesets.Replays return $"{Time}\t({MouseX},{MouseY})\t{MouseLeft}\t{MouseRight}\t{MouseLeft1}\t{MouseRight1}\t{MouseLeft2}\t{MouseRight2}\t{ButtonState}"; } } -} \ No newline at end of file +} diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index ed2fdf4157..df5e12a24f 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -9,7 +9,9 @@ using osu.Framework.Input.Bindings; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets @@ -49,6 +51,10 @@ namespace osu.Game.Rulesets public abstract DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null); + public virtual PerformanceCalculator CreatePerformanceCalculator(Beatmap beatmap, Score score) => null; + + public virtual HitObjectComposer CreateHitObjectComposer() => null; + public virtual Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_question_circle }; public abstract string Description { get; } diff --git a/osu.Game/Rulesets/Scoring/PerformanceCalculator.cs b/osu.Game/Rulesets/Scoring/PerformanceCalculator.cs new file mode 100644 index 0000000000..4f603049db --- /dev/null +++ b/osu.Game/Rulesets/Scoring/PerformanceCalculator.cs @@ -0,0 +1,35 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Scoring +{ + public abstract class PerformanceCalculator + { + public abstract double Calculate(Dictionary categoryDifficulty = null); + } + + public abstract class PerformanceCalculator : PerformanceCalculator + where TObject : HitObject + { + private readonly Dictionary attributes = new Dictionary(); + protected IDictionary Attributes => attributes; + + protected readonly Beatmap Beatmap; + protected readonly Score Score; + + protected PerformanceCalculator(Ruleset ruleset, Beatmap beatmap, Score score) + { + Beatmap = CreateBeatmapConverter().Convert(beatmap); + Score = score; + + var diffCalc = ruleset.CreateDifficultyCalculator(beatmap, score.Mods); + diffCalc.Calculate(attributes); + } + + protected abstract BeatmapConverter CreateBeatmapConverter(); + } +} diff --git a/osu.Game/Rulesets/UI/RulesetContainer.cs b/osu.Game/Rulesets/UI/RulesetContainer.cs index ec26f6f310..69bf6bba29 100644 --- a/osu.Game/Rulesets/UI/RulesetContainer.cs +++ b/osu.Game/Rulesets/UI/RulesetContainer.cs @@ -55,6 +55,11 @@ namespace osu.Game.Rulesets.UI public abstract IEnumerable Objects { get; } + /// + /// The playfield. + /// + public Playfield Playfield { get; protected set; } + protected readonly Ruleset Ruleset; /// @@ -135,11 +140,6 @@ namespace osu.Game.Rulesets.UI public override ScoreProcessor CreateScoreProcessor() => new ScoreProcessor(this); - /// - /// The playfield. - /// - public Playfield Playfield { get; private set; } - protected override Container Content => content; private Container content; diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 8c4d6de1fe..5cd79cff29 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -5,6 +5,8 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Timing; @@ -16,11 +18,24 @@ using OpenTK.Input; namespace osu.Game.Rulesets.UI { - public abstract class RulesetInputManager : DatabasedKeyBindingInputManager, ICanAttachKeyCounter, IHasReplayHandler + public abstract class RulesetInputManager : PassThroughInputManager, ICanAttachKeyCounter, IHasReplayHandler where T : struct { - protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) : base(ruleset, variant, unique) + public class RulesetKeyBindingContainer : DatabasedKeyBindingInputManager { + public RulesetKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + : base(ruleset, variant, unique) + { + } + } + + protected readonly KeyBindingContainer KeyBindingContainer; + + protected override Container Content => KeyBindingContainer; + + protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) + { + InternalChild = KeyBindingContainer = new RulesetKeyBindingContainer(ruleset, variant, unique); } #region Action mapping (for replays) @@ -41,10 +56,10 @@ namespace osu.Game.Rulesets.UI List newActions = replayState.PressedActions; foreach (var released in lastPressedActions.Except(newActions)) - PropagateReleased(KeyBindingInputQueue, released); + KeyBindingContainer.TriggerReleased(released); foreach (var pressed in newActions.Except(lastPressedActions)) - PropagatePressed(KeyBindingInputQueue, pressed); + KeyBindingContainer.TriggerPressed(pressed); lastPressedActions = newActions; } @@ -203,7 +218,7 @@ namespace osu.Game.Rulesets.UI Add(receptor); keyCounter.SetReceptor(receptor); - keyCounter.AddRange(DefaultKeyBindings.Select(b => b.GetAction()).Distinct().Select(b => new KeyCounterAction(b))); + keyCounter.AddRange(KeyBindingContainer.DefaultKeyBindings.Select(b => b.GetAction()).Distinct().Select(b => new KeyCounterAction(b))); } public class ActionReceptor : KeyCounterCollection.Receptor, IKeyBindingHandler diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs index 229d06ef09..df95a5c384 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs @@ -40,7 +40,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts return; } - timeline.RelativeChildSize = new Vector2((float)Math.Max(1, Beatmap.Value.Track.Length), 1); + // Todo: This should be handled more gracefully + timeline.RelativeChildSize = Beatmap.Value.Track.Length == double.PositiveInfinity ? Vector2.One : new Vector2((float)Math.Max(1, Beatmap.Value.Track.Length), 1); } protected void Add(Drawable visualisation) => timeline.Add(visualisation); diff --git a/osu.Game/Screens/Edit/Screens/Compose/Compose.cs b/osu.Game/Screens/Edit/Screens/Compose/Compose.cs index 2349c261cf..6bc7356f26 100644 --- a/osu.Game/Screens/Edit/Screens/Compose/Compose.cs +++ b/osu.Game/Screens/Edit/Screens/Compose/Compose.cs @@ -6,49 +6,99 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Logging; +using osu.Game.Beatmaps; using osu.Game.Screens.Edit.Screens.Compose.Timeline; namespace osu.Game.Screens.Edit.Screens.Compose { public class Compose : EditorScreen { + private const float vertical_margins = 10; + private const float horizontal_margins = 20; + + private readonly Container composerContainer; + public Compose() { ScrollableTimeline timeline; - Children = new[] + Children = new Drawable[] { - new Container + new GridContainer { - Name = "Timeline", - RelativeSizeAxes = Axes.X, - Height = 110, - Children = new Drawable[] + RelativeSizeAxes = Axes.Both, + Content = new[] { - new Box + new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black.Opacity(0.5f) - }, - new Container - { - Name = "Content", - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = 17, Vertical = 10 }, - Children = new Drawable[] + new Container { - new Container + Name = "Timeline", + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = 115 }, - Child = timeline = new ScrollableTimeline { RelativeSizeAxes = Axes.Both } + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.5f) + }, + new Container + { + Name = "Timeline content", + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = horizontal_margins, Vertical = vertical_margins }, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = 115 }, + Child = timeline = new ScrollableTimeline { RelativeSizeAxes = Axes.Both } + } + } + } } } + }, + new Drawable[] + { + composerContainer = new Container + { + Name = "Composer content", + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = horizontal_margins, Vertical = vertical_margins }, + } } - } - } + }, + RowDimensions = new[] { new Dimension(GridSizeMode.Absolute, 110) } + }, }; timeline.Beatmap.BindTo(Beatmap); + Beatmap.ValueChanged += beatmapChanged; + } + + private void beatmapChanged(WorkingBeatmap newBeatmap) + { + composerContainer.Clear(); + + var ruleset = newBeatmap.BeatmapInfo.Ruleset?.CreateInstance(); + if (ruleset == null) + { + Logger.Log("Beatmap doesn't have a ruleset assigned."); + // ExitRequested?.Invoke(); + return; + } + + var composer = ruleset.CreateHitObjectComposer(); + if (composer == null) + { + Logger.Log($"Ruleset {ruleset.Description} doesn't support hitobject composition."); + // ExitRequested?.Invoke(); + return; + } + + composerContainer.Child = composer; } } } diff --git a/osu.Game/Screens/Edit/Screens/Compose/RadioButtons/DrawableRadioButton.cs b/osu.Game/Screens/Edit/Screens/Compose/RadioButtons/DrawableRadioButton.cs new file mode 100644 index 0000000000..10b6c07f3d --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/RadioButtons/DrawableRadioButton.cs @@ -0,0 +1,123 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Screens.Edit.Screens.Compose.RadioButtons +{ + public class DrawableRadioButton : TriangleButton + { + /// + /// Invoked when this has been selected. + /// + public Action Selected; + + private Color4 defaultBackgroundColour; + private Color4 defaultBubbleColour; + private Color4 selectedBackgroundColour; + private Color4 selectedBubbleColour; + + private readonly Drawable bubble; + private readonly RadioButton button; + + public DrawableRadioButton(RadioButton button) + { + this.button = button; + + Text = button.Text; + Action = button.Action; + + RelativeSizeAxes = Axes.X; + + bubble = new CircularContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Scale = new Vector2(0.5f), + X = 10, + Masking = true, + Blending = BlendingMode.Additive, + Child = new Box { RelativeSizeAxes = Axes.Both } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + defaultBackgroundColour = colours.Gray3; + defaultBubbleColour = defaultBackgroundColour.Darken(0.5f); + selectedBackgroundColour = colours.BlueDark; + selectedBubbleColour = selectedBackgroundColour.Lighten(0.5f); + + Triangles.Alpha = 0; + + Content.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = 2, + Offset = new Vector2(0, 1), + Colour = Color4.Black.Opacity(0.5f) + }; + + Add(bubble); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + button.Selected.ValueChanged += v => + { + updateSelectionState(); + if (v) + Selected?.Invoke(button); + }; + + updateSelectionState(); + } + + private void updateSelectionState() + { + if (!IsLoaded) + return; + + BackgroundColour = button.Selected ? selectedBackgroundColour : defaultBackgroundColour; + bubble.Colour = button.Selected ? selectedBubbleColour : defaultBubbleColour; + } + + protected override bool OnClick(InputState state) + { + if (button.Selected) + return true; + + if (!Enabled) + return true; + + button.Selected.Value = true; + + return base.OnClick(state); + } + + protected override SpriteText CreateText() => new OsuSpriteText + { + Depth = -1, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + X = 40f + }; + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/RadioButtons/RadioButton.cs b/osu.Game/Screens/Edit/Screens/Compose/RadioButtons/RadioButton.cs new file mode 100644 index 0000000000..055362d9e1 --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/RadioButtons/RadioButton.cs @@ -0,0 +1,51 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Configuration; + +namespace osu.Game.Screens.Edit.Screens.Compose.RadioButtons +{ + public class RadioButton + { + /// + /// Whether this is selected. + /// + /// + public readonly BindableBool Selected; + + /// + /// The text that should be displayed in this button. + /// + public string Text; + + /// + /// The that should be invoked when this button is selected. + /// + public Action Action; + + public RadioButton(string text, Action action) + { + Text = text; + Action = action; + Selected = new BindableBool(); + } + + public RadioButton(string text) + : this(text, null) + { + Text = text; + Action = null; + } + + /// + /// Selects this . + /// + public void Select() => Selected.Value = true; + + /// + /// Deselects this . + /// + public void Deselect() => Selected.Value = false; + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/RadioButtons/RadioButtonCollection.cs b/osu.Game/Screens/Edit/Screens/Compose/RadioButtons/RadioButtonCollection.cs new file mode 100644 index 0000000000..5f1def4a2e --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/RadioButtons/RadioButtonCollection.cs @@ -0,0 +1,61 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using OpenTK; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Screens.Edit.Screens.Compose.RadioButtons +{ + public class RadioButtonCollection : CompositeDrawable + { + private IReadOnlyList items; + public IReadOnlyList Items + { + get { return items; } + set + { + if (ReferenceEquals(items, value)) + return; + items = value; + + buttonContainer.Clear(); + items.ForEach(addButton); + } + } + + private readonly FlowContainer buttonContainer; + + public RadioButtonCollection() + { + AutoSizeAxes = Axes.Y; + + InternalChild = buttonContainer = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5) + }; + } + + private RadioButton currentlySelected; + private void addButton(RadioButton button) + { + button.Selected.ValueChanged += v => + { + if (v) + { + currentlySelected?.Deselect(); + currentlySelected = button; + } + else + currentlySelected = null; + }; + + buttonContainer.Add(new DrawableRadioButton(button)); + } + } +} diff --git a/osu.Game/Screens/Loader.cs b/osu.Game/Screens/Loader.cs index ca541ea552..ec2e8e0cb1 100644 --- a/osu.Game/Screens/Loader.cs +++ b/osu.Game/Screens/Loader.cs @@ -1,8 +1,12 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Shaders; using osu.Game.Screens.Menu; using OpenTK; using osu.Framework.Screens; @@ -33,14 +37,28 @@ namespace osu.Game.Screens logo.FadeInFromZero(5000, Easing.OutQuint); } + private OsuScreen loadScreen; + private ShaderPrecompiler precompiler; + protected override void OnEntering(Screen last) { base.OnEntering(last); - if (showDisclaimer) - LoadComponentAsync(new Disclaimer(), d => Push(d)); - else - LoadComponentAsync(new Intro(), d => Push(d)); + LoadComponentAsync(precompiler = new ShaderPrecompiler(loadIfReady), Add); + LoadComponentAsync(loadScreen = showDisclaimer ? (OsuScreen)new Disclaimer() : new Intro(), s => loadIfReady()); + } + + private void loadIfReady() + { + if (ChildScreen == loadScreen) return; + + if (loadScreen.LoadState != LoadState.Ready) + return; + + if (!precompiler.FinishedCompiling) + return; + + Push(loadScreen); } protected override void LogoSuspending(OsuLogo logo) @@ -54,5 +72,49 @@ namespace osu.Game.Screens { showDisclaimer = game.IsDeployedBuild; } + + /// + /// Compiles a set of shaders before continuing. Attempts to draw some frames between compilation by limiting to one compile per draw frame. + /// + public class ShaderPrecompiler : Drawable + { + private readonly Action onLoaded; + private readonly List loadTargets = new List(); + + public bool FinishedCompiling { get; private set; } + + public ShaderPrecompiler(Action onLoaded) + { + this.onLoaded = onLoaded; + } + + [BackgroundDependencyLoader] + private void load(ShaderManager manager) + { + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED)); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR)); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE)); + + loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE)); + + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE_ROUNDED)); + loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE)); + } + + private Shader currentLoadTarget; + + protected override void Update() + { + base.Update(); + + // if our target is null we are done. + if (loadTargets.All(s => s.Loaded)) + { + FinishedCompiling = true; + Expire(); + onLoaded?.Invoke(); + } + } + } } } diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 76ee4a607e..4a27c7f1ea 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -13,6 +13,8 @@ using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Game.Rulesets; using osu.Game.Screens.Menu; +using osu.Framework.Input; +using OpenTK.Input; namespace osu.Game.Screens { @@ -73,6 +75,20 @@ namespace osu.Game.Screens sampleExit = audio.Sample.Get(@"UI/screen-back"); } + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (args.Repeat || !IsCurrentScreen) return false; + + switch (args.Key) + { + case Key.Escape: + Exit(); + return true; + } + + return base.OnKeyDown(state, args); + } + protected override void OnResuming(Screen last) { base.OnResuming(last); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index dc746b305c..b5b09504da 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -245,7 +245,7 @@ namespace osu.Game.Screens.Play private void initializeStoryboard(bool asyncLoad) { - var beatmap = Beatmap.Value.Beatmap; + var beatmap = Beatmap.Value; storyboard = beatmap.Storyboard.CreateDrawable(Beatmap.Value); storyboard.Masking = true; @@ -388,7 +388,7 @@ namespace osu.Game.Screens.Play initializeStoryboard(true); var beatmap = Beatmap.Value; - var storyboardVisible = showStoryboard && beatmap.Beatmap.Storyboard.HasDrawable; + var storyboardVisible = showStoryboard && beatmap.Storyboard.HasDrawable; storyboardContainer.FadeColour(new Color4(opacity, opacity, opacity, 1), 800); storyboardContainer.FadeTo(storyboardVisible && opacity > 0 ? 1 : 0); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index b0a636dfb3..3f42ae11ac 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Select Task.Run(() => { - newGroups = value.Select(createGroup).ToList(); + newGroups = value.Select(createGroup).Where(g => g != null).ToList(); criteria.Filter(newGroups); }).ContinueWith(t => { @@ -124,16 +124,19 @@ namespace osu.Game.Screens.Select // todo: this method should be smarter as to not recreate panels that haven't changed, etc. var group = groups.Find(b => b.BeatmapSet.ID == set.ID); - if (group == null) - return; - int i = groups.IndexOf(group); - groups.RemoveAt(i); + if (i >= 0) + groups.RemoveAt(i); var newGroup = createGroup(set); if (newGroup != null) - groups.Insert(i, newGroup); + { + if (i >= 0) + groups.Insert(i, newGroup); + else + groups.Add(newGroup); + } bool hadSelection = selectedGroup == group; diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index d7c509d979..a9a778fe17 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -261,6 +261,7 @@ namespace osu.Game.Screens.Select description.Text = null; source.Text = null; tags.Text = null; + advanced.Beatmap = new BeatmapInfo { StarDifficulty = 0, @@ -306,36 +307,16 @@ namespace osu.Game.Screens.Select private class MetadataSection : Container { - private readonly TextFlowContainer textFlow; - - public string Text - { - set - { - if (string.IsNullOrEmpty(value)) - { - this.FadeOut(transition_duration); - return; - } - - this.FadeIn(transition_duration); - textFlow.Clear(); - textFlow.AddText(value, s => s.TextSize = 14); - } - } - - public Color4 TextColour - { - get { return textFlow.Colour; } - set { textFlow.Colour = value; } - } + private readonly FillFlowContainer textContainer; + private TextFlowContainer textFlow; public MetadataSection(string title) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; + Alpha = 0; - InternalChild = new FillFlowContainer + InternalChild = textContainer = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -361,6 +342,44 @@ namespace osu.Game.Screens.Select }, }; } + + public string Text + { + set + { + if (string.IsNullOrEmpty(value)) + { + this.FadeOut(transition_duration); + return; + } + + setTextAsync(value); + } + } + + private void setTextAsync(string text) + { + LoadComponentAsync(new TextFlowContainer(s => s.TextSize = 14) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Colour = textFlow.Colour, + Text = text + }, loaded => + { + textFlow?.Expire(); + textContainer.Add(textFlow = loaded); + + // fade in if we haven't yet. + this.FadeIn(transition_duration); + }); + } + + public Color4 TextColour + { + get { return textFlow.Colour; } + set { textFlow.Colour = value; } + } } private class DimmedLoadingAnimation : VisibilityContainer diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index a0b788d777..46284226d7 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -211,7 +211,7 @@ namespace osu.Game.Screens.Select if (Beatmap.Value.BeatmapSetInfo?.DeletePending == false) carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo, false); else - carousel.SelectNext(); + carousel.SelectNextRandom(); } private void carouselRaisedStart(InputState state = null) @@ -263,10 +263,7 @@ namespace osu.Game.Screens.Select beatmapNoDebounce = beatmap; if (beatmap == null) - { - if (!Beatmap.IsDefault) - performLoad(); - } + performLoad(); else { if (beatmap.BeatmapSetInfoID == beatmapNoDebounce?.BeatmapSetInfoID) diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index 59cbe74650..4eca910c1e 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -5,10 +5,11 @@ using osu.Game.Beatmaps; using osu.Game.Storyboards.Drawables; using System.Collections.Generic; using System.Linq; +using System; namespace osu.Game.Storyboards { - public class Storyboard + public class Storyboard : IDisposable { private readonly Dictionary layers = new Dictionary(); public IEnumerable Layers => layers.Values; @@ -59,5 +60,29 @@ namespace osu.Game.Storyboards } return drawable; } + + #region Disposal + + ~Storyboard() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private bool isDisposed; + + protected virtual void Dispose(bool isDisposing) + { + if (isDisposed) + return; + isDisposed = true; + } + + #endregion } } diff --git a/osu.Game/Tests/Visual/TestCasePerformancePoints.cs b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs new file mode 100644 index 0000000000..6da14e9b12 --- /dev/null +++ b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs @@ -0,0 +1,395 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using System.Linq; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Caching; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; +using osu.Game.Beatmaps; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Tests.Visual +{ + public abstract class TestCasePerformancePoints : OsuTestCase + { + protected TestCasePerformancePoints(Ruleset ruleset) + { + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.5f, + }, + new ScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = new BeatmapList(ruleset) + } + } + }, + null, + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.5f, + }, + new ScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = new StarRatingGrid() + } + } + }, + null, + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.5f, + }, + new ScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = new PerformanceList() + } + } + }, + } + }, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 20), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 20) + } + }; + } + + private class BeatmapList : CompositeDrawable + { + private readonly Container beatmapDisplays; + private readonly Ruleset ruleset; + + public BeatmapList(Ruleset ruleset) + { + this.ruleset = ruleset; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = beatmapDisplays = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 4) + }; + } + + [BackgroundDependencyLoader] + private void load(BeatmapManager beatmaps) + { + var sets = beatmaps.GetAllUsableBeatmapSets(); + var allBeatmaps = sets.SelectMany(s => s.Beatmaps).Where(b => ruleset.LegacyID < 0 || b.RulesetID == ruleset.LegacyID); + + allBeatmaps.ForEach(b => beatmapDisplays.Add(new BeatmapDisplay(b))); + } + + private class BeatmapDisplay : CompositeDrawable, IHasTooltip + { + private readonly OsuSpriteText text; + private readonly BeatmapInfo beatmap; + + private BeatmapManager beatmaps; + private OsuGameBase osuGame; + + private bool isSelected; + + public string TooltipText => text.Text; + + public BeatmapDisplay(BeatmapInfo beatmap) + { + this.beatmap = beatmap; + + AutoSizeAxes = Axes.Both; + InternalChild = text = new OsuSpriteText(); + } + + protected override bool OnClick(InputState state) + { + if (osuGame.Beatmap.Value.BeatmapInfo.ID == beatmap.ID) + return false; + + osuGame.Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); + isSelected = true; + return true; + } + + protected override bool OnHover(InputState state) + { + if (isSelected) + return false; + this.FadeColour(Color4.Yellow, 100); + return true; + } + + protected override void OnHoverLost(InputState state) + { + if (isSelected) + return; + this.FadeColour(Color4.White, 100); + } + + [BackgroundDependencyLoader] + private void load(OsuGameBase osuGame, BeatmapManager beatmaps) + { + this.osuGame = osuGame; + this.beatmaps = beatmaps; + + var working = beatmaps.GetWorkingBeatmap(beatmap); + text.Text = $"{working.Metadata.Artist} - {working.Metadata.Title} ({working.Metadata.AuthorString}) [{working.BeatmapInfo.Version}]"; + + osuGame.Beatmap.ValueChanged += beatmapChanged; + } + + private void beatmapChanged(WorkingBeatmap newBeatmap) + { + if (isSelected) + this.FadeColour(Color4.White, 100); + isSelected = false; + } + } + } + + private class PerformanceList : CompositeDrawable + { + private readonly FillFlowContainer scores; + private APIAccess api; + + public PerformanceList() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = scores = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 4) + }; + } + + [BackgroundDependencyLoader] + private void load(OsuGameBase osuGame, APIAccess api) + { + this.api = api; + + if (!api.IsLoggedIn) + { + InternalChild = new SpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = "Please login to see online scores", + }; + } + + osuGame.Beatmap.ValueChanged += beatmapChanged; + } + + private GetScoresRequest lastRequest; + private void beatmapChanged(WorkingBeatmap newBeatmap) + { + lastRequest?.Cancel(); + scores.Clear(); + + if (!api.IsLoggedIn) + return; + + lastRequest = new GetScoresRequest(newBeatmap.BeatmapInfo); + lastRequest.Success += res => res.Scores.ForEach(s => scores.Add(new PerformanceDisplay(s, newBeatmap.Beatmap))); + api.Queue(lastRequest); + } + + private class PerformanceDisplay : CompositeDrawable + { + private readonly OsuSpriteText text; + + private readonly Score score; + private readonly Beatmap beatmap; + + public PerformanceDisplay(Score score, Beatmap beatmap) + { + this.score = score; + this.beatmap = beatmap; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = text = new OsuSpriteText(); + } + + [BackgroundDependencyLoader] + private void load() + { + var ruleset = beatmap.BeatmapInfo.Ruleset.CreateInstance(); + var calculator = ruleset.CreatePerformanceCalculator(beatmap, score); + if (calculator == null) + return; + + var attributes = new Dictionary(); + double performance = calculator.Calculate(attributes); + + text.Text = $"{score.User.Username} -> online: {score.PP:n2}pp | local: {performance:n2}pp"; + } + } + } + + private class StarRatingGrid : CompositeDrawable + { + private readonly FillFlowContainer modFlow; + private readonly OsuSpriteText totalText; + private readonly FillFlowContainer categoryTexts; + + public StarRatingGrid() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + modFlow = new FillFlowContainer + { + Name = "Checkbox flow", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(4, 4) + }, + new FillFlowContainer + { + Name = "Information display", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0, 4), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + totalText = new OsuSpriteText { TextSize = 24 }, + categoryTexts = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical + } + } + } + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuGameBase osuGame) + { + osuGame.Beatmap.ValueChanged += beatmapChanged; + } + + private Cached informationCache = new Cached(); + + private Ruleset ruleset; + private WorkingBeatmap beatmap; + + private void beatmapChanged(WorkingBeatmap newBeatmap) + { + beatmap = newBeatmap; + + modFlow.Clear(); + + ruleset = newBeatmap.BeatmapInfo.Ruleset.CreateInstance(); + foreach (var mod in ruleset.GetAllMods()) + { + var checkBox = new OsuCheckbox + { + RelativeSizeAxes = Axes.None, + Width = 50, + LabelText = mod.ShortenedName + }; + + checkBox.Current.ValueChanged += v => informationCache.Invalidate(); + modFlow.Add(checkBox); + } + + informationCache.Invalidate(); + } + + protected override void Update() + { + base.Update(); + + if (ruleset == null) + return; + + if (!informationCache.IsValid) + { + totalText.Text = string.Empty; + categoryTexts.Clear(); + + var allMods = ruleset.GetAllMods().ToList(); + Mod[] activeMods = modFlow.Where(c => c.Current.Value).Select(c => allMods.First(m => m.ShortenedName == c.LabelText)).ToArray(); + + var diffCalc = ruleset.CreateDifficultyCalculator(beatmap.Beatmap, activeMods); + if (diffCalc != null) + { + var categories = new Dictionary(); + double totalSr = diffCalc.Calculate(categories); + + totalText.Text = $"Star rating: {totalSr:n2}"; + foreach (var kvp in categories) + categoryTexts.Add(new OsuSpriteText { Text = $"{kvp.Key}: {kvp.Value:n2}" }); + } + + informationCache.Validate(); + } + } + } + } +} diff --git a/osu.Game/Tests/Visual/TestCasePlayer.cs b/osu.Game/Tests/Visual/TestCasePlayer.cs index f3a6d1efc3..106f0fa8f3 100644 --- a/osu.Game/Tests/Visual/TestCasePlayer.cs +++ b/osu.Game/Tests/Visual/TestCasePlayer.cs @@ -8,7 +8,6 @@ using System.Text; using osu.Framework.Allocation; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play; @@ -50,7 +49,11 @@ namespace osu.Game.Tests.Visual string instantiation = ruleset?.AssemblyQualifiedName; foreach (var r in rulesets.AvailableRulesets.Where(rs => instantiation == null || rs.InstantiationInfo == instantiation)) - AddStep(r.Name, () => loadPlayerFor(r)); + { + Player p = null; + AddStep(r.Name, () => p = loadPlayerFor(r)); + AddUntilStep(() => p.IsLoaded); + } } protected virtual Beatmap CreateBeatmap() @@ -59,12 +62,12 @@ namespace osu.Game.Tests.Visual using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(test_beatmap_data))) using (var reader = new StreamReader(stream)) - beatmap = BeatmapDecoder.GetDecoder(reader).Decode(reader); + beatmap = Game.Beatmaps.Formats.Decoder.GetDecoder(reader).DecodeBeatmap(reader); return beatmap; } - private void loadPlayerFor(RulesetInfo r) + private Player loadPlayerFor(RulesetInfo r) { var beatmap = CreateBeatmap(); @@ -78,7 +81,11 @@ namespace osu.Game.Tests.Visual if (Player != null) Remove(Player); - LoadScreen(CreatePlayer(working, instance)); + var player = CreatePlayer(working, instance); + + LoadComponentAsync(player, LoadScreen); + + return player; } protected virtual Player CreatePlayer(WorkingBeatmap beatmap, Ruleset ruleset) => new Player diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index ce9e8eff9c..59f6682569 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -1,5 +1,6 @@  - + + {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D} Debug @@ -61,7 +62,6 @@ false - 6 none @@ -195,9 +195,6 @@ - - osu.licenseheader - @@ -269,6 +266,8 @@ + + @@ -283,7 +282,7 @@ 20171025071459_AddMissingIndexRules.cs - + 20171119065731_AddBeatmapOnlineIDUniqueConstraint.cs @@ -306,16 +305,19 @@ - - - + + + + + + @@ -559,6 +561,10 @@ + + + + @@ -632,6 +638,7 @@ + @@ -787,6 +794,7 @@ + diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index fdfbf25144..76929dcbb3 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -34,6 +34,7 @@ HINT WARNING WARNING + HINT WARNING WARNING DO_NOT_SHOW @@ -44,13 +45,16 @@ WARNING ERROR HINT + HINT HINT WARNING WARNING + HINT DO_NOT_SHOW HINT HINT HINT + HINT WARNING WARNING WARNING @@ -149,6 +153,7 @@ WARNING WARNING WARNING + HINT WARNING WARNING HINT