diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index ff930b07a3..2bff304fba 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,9 +1,11 @@ -osu!lazer is currently in early stages of development and is not yet ready for end users. Please avoid creating issues or bugs if you do not personally intend to fix them. Some acceptable topics include: +osu!lazer is currently still under heavy development! +Please ensure that you are making an issue for one of the following: + +- A bug with currently implemented features (not features that don't exist) +- A feature you are considering adding, so we can collaborate on feedback and design. - Discussions about technical design decisions -- Bugs that you have found and are personally willing and able to fix -- TODO lists of smaller tasks around larger features - -Basically, issues are not a place for you to get help. They are a place for developers to collaborate on the game. If your issue qualifies, replace this text with a detailed description of your issue with as much relevant information as you can provide. + +Screenshots and log files are highly welcomed. \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index 9cf68803a2..e63f6ea55c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -12,7 +12,7 @@ install: - 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 + - cmd: appveyor DownloadFile https://github.com/peppy/CodeFileSanity/releases/download/v0.2.4/CodeFileSanity.exe before_build: - cmd: CodeFileSanity.exe - cmd: nuget restore -verbosity quiet @@ -20,6 +20,10 @@ build: project: osu.sln parallel: true verbosity: minimal +test: + assemblies: + only: + - 'osu.Desktop\**\*.dll' after_build: - 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.Desktop.Deploy/Program.cs b/osu.Desktop.Deploy/Program.cs index 88d6ffbca7..e5e0702e6d 100644 --- a/osu.Desktop.Deploy/Program.cs +++ b/osu.Desktop.Deploy/Program.cs @@ -39,7 +39,7 @@ namespace osu.Desktop.Deploy /// /// How many previous build deltas we want to keep when publishing. /// - private const int keep_delta_count = 3; + private const int keep_delta_count = 4; private static string codeSigningCmd => string.IsNullOrEmpty(codeSigningPassword) ? "" : $"-n \"/a /f {codeSigningCertPath} /p {codeSigningPassword} /t http://timestamp.comodoca.com/authenticode\""; diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 22ef8f1a34..d8c1d47efb 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -109,16 +109,11 @@ namespace osu.Desktop { var filePaths = new[] { e.FileName }; - if (filePaths.All(f => Path.GetExtension(f) == @".osz")) - Task.Factory.StartNew(() => BeatmapManager.Import(filePaths), TaskCreationOptions.LongRunning); - else if (filePaths.All(f => Path.GetExtension(f) == @".osr")) - Task.Run(() => - { - var score = ScoreStore.ReadReplayFile(filePaths.First()); - Schedule(() => LoadScore(score)); - }); - } + var firstExtension = Path.GetExtension(filePaths.First()); - private static readonly string[] allowed_extensions = { @".osz", @".osr" }; + if (filePaths.Any(f => Path.GetExtension(f) != firstExtension)) return; + + Task.Factory.StartNew(() => Import(filePaths), TaskCreationOptions.LongRunning); + } } } diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index a91e728ce7..e878167536 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Linq; +using System.Runtime; using osu.Framework; using osu.Framework.Platform; using osu.Game.IPC; @@ -18,6 +19,9 @@ namespace osu.Desktop // required to initialise native SQLite libraries on some platforms. SQLitePCL.Batteries_V2.Init(); + if (!RuntimeInfo.IsMono) + useMulticoreJit(); + // Back up the cwd before DesktopGameHost changes it var cwd = Environment.CurrentDirectory; @@ -25,7 +29,7 @@ namespace osu.Desktop { if (!host.IsPrimaryInstance) { - var importer = new BeatmapIPCChannel(host); + var importer = new ArchiveImportIPCChannel(host); // Restore the cwd so relative paths given at the command line work correctly Directory.SetCurrentDirectory(cwd); foreach (var file in args) @@ -47,8 +51,16 @@ namespace osu.Desktop break; } } + return 0; } } + + private static void useMulticoreJit() + { + var directory = Directory.CreateDirectory(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Profiles")); + ProfileOptimization.SetProfileRoot(directory.FullName); + ProfileOptimization.StartProfile("Startup.Profile"); + } } } diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec index bb7d382cee..316a5443ef 100644 --- a/osu.Desktop/osu.nuspec +++ b/osu.Desktop/osu.nuspec @@ -16,11 +16,9 @@ en-AU - - - - - + + + diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index d3012b1981..1bebe9dae0 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -16,29 +16,13 @@ namespace osu.Game.Rulesets.Catch.Beatmaps { public override void PostProcess(Beatmap beatmap) { - if (beatmap.ComboColors.Count == 0) - return; - - int index = 0; - int colourIndex = 0; - - CatchHitObject lastObj = null; - initialiseHyperDash(beatmap.HitObjects); + base.PostProcess(beatmap); + + int index = 0; foreach (var obj in beatmap.HitObjects) - { - if (obj.NewCombo) - { - if (lastObj != null) lastObj.LastInCombo = true; - colourIndex = (colourIndex + 1) % beatmap.ComboColors.Count; - } - obj.IndexInBeatmap = index++; - obj.ComboColour = beatmap.ComboColors[colourIndex]; - - lastObj = obj; - } } private void initialiseHyperDash(List objects) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 5e70239c7c..4dbe65b3ce 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -10,6 +10,8 @@ using osu.Game.Rulesets.UI; using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.Catch.Replays; +using osu.Game.Rulesets.Replays.Types; namespace osu.Game.Rulesets.Catch { @@ -99,7 +101,9 @@ namespace osu.Game.Rulesets.Catch public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new CatchDifficultyCalculator(beatmap); - public override int LegacyID => 2; + public override int? LegacyID => 2; + + public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame(); public CatchRuleset(RulesetInfo rulesetInfo = null) : base(rulesetInfo) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs index 124af06d56..8eb8fd8435 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDaycore.cs @@ -7,6 +7,6 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModDaycore : ModDaycore { - public override double ScoreMultiplier => 0.5; + public override double ScoreMultiplier => 0.3; } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs index 5c025bdea0..07bc8b825a 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs @@ -7,5 +7,6 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModEasy : ModEasy { + public override string Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!"; } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs index 303fa6011d..947990cce5 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModHalfTime.cs @@ -7,6 +7,6 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModHalfTime : ModHalfTime { - public override double ScoreMultiplier => 0.5; + public override double ScoreMultiplier => 0.3; } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs index ed33bf7124..9479c9d9b0 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModHardRock.cs @@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Catch.Mods public class CatchModHardRock : ModHardRock { public override double ScoreMultiplier => 1.12; - public override bool Ranked => true; } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs index 981ebda9eb..14291f744c 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs @@ -7,7 +7,7 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModHidden : ModHidden { - public override string Description => @"Play with fading notes for a slight score advantage."; + public override string Description => @"Play with fading fruits."; public override double ScoreMultiplier => 1.06; } } diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs index 89bd73f8fb..487345019b 100644 --- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs +++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs @@ -3,7 +3,6 @@ using osu.Framework.MathUtils; using osu.Game.Rulesets.Objects.Types; -using OpenTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects { @@ -32,25 +31,11 @@ namespace osu.Game.Rulesets.Catch.Objects AddNested(new Banana { Samples = Samples, - ComboColour = getNextComboColour(), StartTime = i, X = RNG.NextSingle() }); } - private Color4 getNextComboColour() - { - switch (RNG.Next(0, 3)) - { - default: - return new Color4(255, 240, 0, 255); - case 1: - return new Color4(255, 192, 0, 255); - case 2: - return new Color4(214, 221, 28, 255); - } - } - public double EndTime => StartTime + Duration; public double Duration { get; set; } diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index 559bf47842..1a0ccc9b1e 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -5,24 +5,25 @@ 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 abstract class CatchHitObject : HitObject, IHasXPosition, IHasComboInformation { public const double OBJECT_RADIUS = 44; public float X { get; set; } - public Color4 ComboColour { get; set; } - public int IndexInBeatmap { get; set; } public virtual FruitVisualRepresentation VisualRepresentation => (FruitVisualRepresentation)(IndexInBeatmap % 4); public virtual bool NewCombo { get; set; } + public int IndexInCurrentCombo { get; set; } + + public int ComboIndex { get; set; } + /// /// The next fruit starts a new combo. Used for explodey. /// diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs index 7b0370ef88..3c6ec0703d 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableBananaShower.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable Origin = Anchor.BottomLeft; X = 0; - Child = bananaContainer = new Container { RelativeSizeAxes = Axes.Both }; + InternalChild = bananaContainer = new Container { RelativeSizeAxes = Axes.Both }; foreach (var b in s.NestedHitObjects.Cast()) AddNested(getVisualRepresentation?.Invoke(b)); diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs index 8d56fc1081..582946ff00 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableCatchHitObject.cs @@ -8,6 +8,8 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using OpenTK; using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; +using OpenTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawable { @@ -57,6 +59,14 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable AddJudgement(new Judgement { Result = CheckPosition.Invoke(HitObject) ? HitResult.Perfect : HitResult.Miss }); } + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + + if (HitObject is IHasComboInformation combo) + AccentColour = skin.GetValue(s => s.ComboColours.Count > 0 ? s.ComboColours[combo.ComboIndex % s.ComboColours.Count] : (Color4?)null) ?? Color4.White; + } + private const float preempt = 1000; protected override void UpdateState(ArmedState state) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs index c2b0552ab3..719cf0a110 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableDroplet.cs @@ -5,28 +5,39 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.Objects.Drawable.Pieces; using OpenTK; +using OpenTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawable { public class DrawableDroplet : PalpableCatchHitObject { + private Pulp pulp; + public DrawableDroplet(Droplet h) : base(h) { Origin = Anchor.Centre; Size = new Vector2((float)CatchHitObject.OBJECT_RADIUS) / 4; - AccentColour = h.ComboColour; Masking = false; } [BackgroundDependencyLoader] private void load() { - Child = new Pulp + InternalChild = pulp = new Pulp { - AccentColour = AccentColour, Size = Size }; } + + public override Color4 AccentColour + { + get { return base.AccentColour; } + set + { + base.AccentColour = value; + pulp.AccentColour = AccentColour; + } + } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs index 93a1483f6f..03c2444d8c 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableFruit.cs @@ -24,7 +24,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable Origin = Anchor.Centre; Size = new Vector2((float)CatchHitObject.OBJECT_RADIUS); - AccentColour = HitObject.ComboColour; Masking = false; Rotation = (float)(RNG.NextDouble() - 0.5f) * 40; @@ -33,7 +32,10 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable [BackgroundDependencyLoader] private void load() { - Children = new[] + // todo: this should come from the skin. + AccentColour = colourForRrepesentation(HitObject.VisualRepresentation); + + InternalChildren = new[] { createPulp(HitObject.VisualRepresentation), border = new Circle @@ -65,7 +67,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable if (HitObject.HyperDash) { - Add(new Pulp + AddInternal(new Pulp { RelativePositionAxes = Axes.Both, Anchor = Anchor.Centre, @@ -273,5 +275,31 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable border.Alpha = (float)MathHelper.Clamp((HitObject.StartTime - Time.Current) / 500, 0, 1); } + + private Color4 colourForRrepesentation(FruitVisualRepresentation representation) + { + switch (representation) + { + default: + case FruitVisualRepresentation.Pear: + return new Color4(17, 136, 170, 255); + case FruitVisualRepresentation.Grape: + return new Color4(204, 102, 0, 255); + case FruitVisualRepresentation.Raspberry: + return new Color4(121, 9, 13, 255); + case FruitVisualRepresentation.Pineapple: + return new Color4(102, 136, 0, 255); + case FruitVisualRepresentation.Banana: + switch (RNG.Next(0, 3)) + { + default: + return new Color4(255, 240, 0, 255); + case 1: + return new Color4(255, 192, 0, 255); + case 2: + return new Color4(214, 221, 28, 255); + } + } + } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs index 965ca62674..b3532e2473 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawable/DrawableJuiceStream.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable Origin = Anchor.BottomLeft; X = 0; - Child = dropletContainer = new Container { RelativeSizeAxes = Axes.Both, }; + InternalChild = dropletContainer = new Container { RelativeSizeAxes = Axes.Both, }; foreach (var o in s.NestedHitObjects.Cast()) AddNested(getVisualRepresentation?.Invoke(o)); @@ -33,7 +33,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawable var catchObject = (DrawableCatchHitObject)h; catchObject.CheckPosition = o => CheckPosition?.Invoke(o) ?? false; - catchObject.AccentColour = HitObject.ComboColour; dropletContainer.Add(h); base.AddNested(h); diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index be1e360fce..29ad3c3956 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -60,74 +60,75 @@ namespace osu.Game.Rulesets.Catch.Objects AddNested(new Fruit { Samples = Samples, - ComboColour = ComboColour, StartTime = StartTime, X = X }); - for (var span = 0; span < this.SpanCount(); span++) + double lastDropletTime = StartTime; + + for (int span = 0; span < this.SpanCount(); span++) { var spanStartTime = StartTime + span * spanDuration; var reversed = span % 2 == 1; - for (var d = tickDistance; d <= length; d += tickDistance) + for (double d = 0; d <= length; d += tickDistance) { - if (d > length - minDistanceFromEnd) - break; - var timeProgress = d / length; var distanceProgress = reversed ? 1 - timeProgress : timeProgress; - var lastTickTime = spanStartTime + timeProgress * spanDuration; - AddNested(new Droplet + double time = spanStartTime + timeProgress * spanDuration; + + double tinyTickInterval = time - lastDropletTime; + while (tinyTickInterval > 100) + tinyTickInterval /= 2; + + for (double t = lastDropletTime + tinyTickInterval; t < time; t += tinyTickInterval) { - StartTime = lastTickTime, - ComboColour = ComboColour, - X = Curve.PositionAt(distanceProgress).X / CatchPlayfield.BASE_WIDTH, - Samples = new List(Samples.Select(s => new SampleInfo + double progress = reversed ? 1 - (t - spanStartTime) / spanDuration : (t - spanStartTime) / spanDuration; + + AddNested(new TinyDroplet { - Bank = s.Bank, - Name = @"slidertick", - Volume = s.Volume - })) - }); - } + StartTime = t, + X = X + Curve.PositionAt(progress).X / CatchPlayfield.BASE_WIDTH, + Samples = new List(Samples.Select(s => new SampleInfo + { + Bank = s.Bank, + Name = @"slidertick", + Volume = s.Volume + })) + }); + } - double tinyTickInterval = tickDistance / length * spanDuration; - while (tinyTickInterval > 100) - tinyTickInterval /= 2; - - for (double t = 0; t < spanDuration; t += tinyTickInterval) - { - double progress = reversed ? 1 - t / spanDuration : t / spanDuration; - - AddNested(new TinyDroplet + if (d > minDistanceFromEnd && Math.Abs(d - length) > minDistanceFromEnd) { - StartTime = spanStartTime + t, - ComboColour = ComboColour, - X = Curve.PositionAt(progress).X / CatchPlayfield.BASE_WIDTH, - Samples = new List(Samples.Select(s => new SampleInfo + AddNested(new Droplet { - Bank = s.Bank, - Name = @"slidertick", - Volume = s.Volume - })) - }); + StartTime = time, + X = X + Curve.PositionAt(distanceProgress).X / CatchPlayfield.BASE_WIDTH, + Samples = new List(Samples.Select(s => new SampleInfo + { + Bank = s.Bank, + Name = @"slidertick", + Volume = s.Volume + })) + }); + } + + lastDropletTime = time; } AddNested(new Fruit { Samples = Samples, - ComboColour = ComboColour, StartTime = spanStartTime + spanDuration, - X = Curve.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH + X = X + Curve.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH }); } } public double EndTime => StartTime + this.SpanCount() * Curve.Distance / Velocity; - public float EndX => Curve.PositionAt(this.ProgressAt(1)).X / CatchPlayfield.BASE_WIDTH; + public float EndX => X + this.CurvePositionAt(1).X / CatchPlayfield.BASE_WIDTH; public double Duration => EndTime - StartTime; diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs index f8ca75fae9..f1503a14ee 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs @@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Catch.Replays } else if (h.HyperDash) { - Replay.Frames.Add(new CatchReplayFrame(h.StartTime - timeAvailable, lastPosition, ReplayButtonState.Right1)); + Replay.Frames.Add(new CatchReplayFrame(h.StartTime - timeAvailable, lastPosition)); Replay.Frames.Add(new CatchReplayFrame(h.StartTime, h.X)); } else if (dashRequired) @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Replays float midPosition = (float)Interpolation.Lerp(lastPosition, h.X, (float)timeAtDashSpeed / timeAvailable); //dash movement - Replay.Frames.Add(new CatchReplayFrame(h.StartTime - timeAvailable + 1, lastPosition, ReplayButtonState.Left1)); + Replay.Frames.Add(new CatchReplayFrame(h.StartTime - timeAvailable + 1, lastPosition, true)); Replay.Frames.Add(new CatchReplayFrame(h.StartTime - timeAvailable + timeAtDashSpeed, midPosition)); Replay.Frames.Add(new CatchReplayFrame(h.StartTime, h.X)); } @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Catch.Replays { double timeBefore = positionChange / movement_speed; - Replay.Frames.Add(new CatchReplayFrame(h.StartTime - timeBefore, lastPosition, ReplayButtonState.Right1)); + Replay.Frames.Add(new CatchReplayFrame(h.StartTime - timeBefore, lastPosition)); Replay.Frames.Add(new CatchReplayFrame(h.StartTime, h.X)); } diff --git a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs index 2f296a2504..9c9b06fcea 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs @@ -3,37 +3,51 @@ using System.Collections.Generic; using osu.Framework.Input; +using osu.Framework.MathUtils; using osu.Game.Rulesets.Replays; namespace osu.Game.Rulesets.Catch.Replays { - public class CatchFramedReplayInputHandler : FramedReplayInputHandler + public class CatchFramedReplayInputHandler : FramedReplayInputHandler { public CatchFramedReplayInputHandler(Replay replay) : base(replay) { } + protected override bool IsImportant(CatchReplayFrame frame) => frame.Position > 0; + + protected float? Position + { + get + { + if (!HasFrames) + return null; + + return Interpolation.ValueAt(CurrentTime, CurrentFrame.Position, NextFrame.Position, CurrentFrame.Time, NextFrame.Time); + } + } + public override List GetPendingStates() { if (!Position.HasValue) return new List(); - var action = new List(); + var actions = new List(); - if (CurrentFrame.ButtonState == ReplayButtonState.Left1) - action.Add(CatchAction.Dash); + if (CurrentFrame.Dashing) + actions.Add(CatchAction.Dash); - if (Position.Value.X > CurrentFrame.Position.X) - action.Add(CatchAction.MoveRight); - else if (Position.Value.X < CurrentFrame.Position.X) - action.Add(CatchAction.MoveLeft); + if (Position.Value > CurrentFrame.Position) + actions.Add(CatchAction.MoveRight); + else if (Position.Value < CurrentFrame.Position) + actions.Add(CatchAction.MoveLeft); return new List { new CatchReplayState { - PressedActions = action, - CatcherX = Position.Value.X + PressedActions = actions, + CatcherX = Position.Value }, }; } diff --git a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs index 0194fc93a4..b444b0d7ba 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs @@ -1,17 +1,34 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Replays.Legacy; +using osu.Game.Rulesets.Replays.Types; namespace osu.Game.Rulesets.Catch.Replays { - public class CatchReplayFrame : ReplayFrame + public class CatchReplayFrame : ReplayFrame, IConvertibleReplayFrame { - public override bool IsImportant => MouseX > 0; + public float Position; + public bool Dashing; - public CatchReplayFrame(double time, float? x = null, ReplayButtonState button = ReplayButtonState.None) - : base(time, x ?? -1, null, button) + public CatchReplayFrame() { } + + public CatchReplayFrame(double time, float? position = null, bool dashing = false) + : base(time) + { + Position = position ?? -1; + Dashing = dashing; + } + + public void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap) + { + Position = legacyFrame.Position.X / CatchPlayfield.BASE_WIDTH; + Dashing = legacyFrame.ButtonState == ReplayButtonState.Left1; + } } } diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-expected-conversion.json new file mode 100644 index 0000000000..9357d3b75c --- /dev/null +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic-expected-conversion.json @@ -0,0 +1,957 @@ +{ + "Mappings": [{ + "StartTime": 500.0, + "Objects": [{ + "StartTime": 500.0, + "Position": 96.0 + }, { + "StartTime": 562.0, + "Position": 100.84 + }, { + "StartTime": 625.0, + "Position": 125.0 + }, { + "StartTime": 687.0, + "Position": 152.84 + }, { + "StartTime": 750.0, + "Position": 191.0 + }, { + "StartTime": 812.0, + "Position": 212.84 + }, { + "StartTime": 875.0, + "Position": 217.0 + }, { + "StartTime": 937.0, + "Position": 234.84 + }, { + "StartTime": 1000.0, + "Position": 256.0 + }, { + "StartTime": 1062.0, + "Position": 267.84 + }, { + "StartTime": 1125.0, + "Position": 284.0 + }, { + "StartTime": 1187.0, + "Position": 311.84 + }, { + "StartTime": 1250.0, + "Position": 350.0 + }, { + "StartTime": 1312.0, + "Position": 359.84 + }, { + "StartTime": 1375.0, + "Position": 367.0 + }, { + "StartTime": 1437.0, + "Position": 400.84 + }, { + "StartTime": 1500.0, + "Position": 416.0 + }, { + "StartTime": 1562.0, + "Position": 377.159973 + }, { + "StartTime": 1625.0, + "Position": 367.0 + }, { + "StartTime": 1687.0, + "Position": 374.159973 + }, { + "StartTime": 1750.0, + "Position": 353.0 + }, { + "StartTime": 1812.0, + "Position": 329.159973 + }, { + "StartTime": 1875.0, + "Position": 288.0 + }, { + "StartTime": 1937.0, + "Position": 259.159973 + }, { + "StartTime": 2000.0, + "Position": 256.0 + }, { + "StartTime": 2058.0, + "Position": 232.44 + }, { + "StartTime": 2116.0, + "Position": 222.879974 + }, { + "StartTime": 2174.0, + "Position": 185.319992 + }, { + "StartTime": 2232.0, + "Position": 177.76001 + }, { + "StartTime": 2290.0, + "Position": 162.200012 + }, { + "StartTime": 2348.0, + "Position": 158.639984 + }, { + "StartTime": 2406.0, + "Position": 111.079994 + }, { + "StartTime": 2500.0, + "Position": 96.0 + }] + }, { + "StartTime": 3000.0, + "Objects": [{ + "StartTime": 3000.0, + "Position": 18.0 + }, { + "StartTime": 3062.0, + "Position": 482.0 + }, { + "StartTime": 3125.0, + "Position": 243.0 + }, { + "StartTime": 3187.0, + "Position": 332.0 + }, { + "StartTime": 3250.0, + "Position": 477.0 + }, { + "StartTime": 3312.0, + "Position": 376.0 + }, { + "StartTime": 3375.0, + "Position": 104.0 + }, { + "StartTime": 3437.0, + "Position": 156.0 + }, { + "StartTime": 3500.0, + "Position": 135.0 + }, { + "StartTime": 3562.0, + "Position": 256.0 + }, { + "StartTime": 3625.0, + "Position": 360.0 + }, { + "StartTime": 3687.0, + "Position": 199.0 + }, { + "StartTime": 3750.0, + "Position": 239.0 + }, { + "StartTime": 3812.0, + "Position": 326.0 + }, { + "StartTime": 3875.0, + "Position": 393.0 + }, { + "StartTime": 3937.0, + "Position": 470.0 + }, { + "StartTime": 4000.0, + "Position": 136.0 + }] + }, { + "StartTime": 4500.0, + "Objects": [{ + "StartTime": 4500.0, + "Position": 317.0 + }, { + "StartTime": 4562.0, + "Position": 354.0 + }, { + "StartTime": 4625.0, + "Position": 414.0 + }, { + "StartTime": 4687.0, + "Position": 39.0 + }, { + "StartTime": 4750.0, + "Position": 172.0 + }, { + "StartTime": 4812.0, + "Position": 479.0 + }, { + "StartTime": 4875.0, + "Position": 18.0 + }, { + "StartTime": 4937.0, + "Position": 151.0 + }, { + "StartTime": 5000.0, + "Position": 342.0 + }, { + "StartTime": 5062.0, + "Position": 400.0 + }, { + "StartTime": 5125.0, + "Position": 420.0 + }, { + "StartTime": 5187.0, + "Position": 90.0 + }, { + "StartTime": 5250.0, + "Position": 220.0 + }, { + "StartTime": 5312.0, + "Position": 80.0 + }, { + "StartTime": 5375.0, + "Position": 421.0 + }, { + "StartTime": 5437.0, + "Position": 473.0 + }, { + "StartTime": 5500.0, + "Position": 97.0 + }] + }, { + "StartTime": 6000.0, + "Objects": [{ + "StartTime": 6000.0, + "Position": 105.0 + }, { + "StartTime": 6062.0, + "Position": 249.0 + }, { + "StartTime": 6125.0, + "Position": 163.0 + }, { + "StartTime": 6187.0, + "Position": 194.0 + }, { + "StartTime": 6250.0, + "Position": 106.0 + }, { + "StartTime": 6312.0, + "Position": 212.0 + }, { + "StartTime": 6375.0, + "Position": 257.0 + }, { + "StartTime": 6437.0, + "Position": 461.0 + }, { + "StartTime": 6500.0, + "Position": 79.0 + }] + }, { + "StartTime": 7000.0, + "Objects": [{ + "StartTime": 7000.0, + "Position": 256.0 + }, { + "StartTime": 7062.0, + "Position": 294.84 + }, { + "StartTime": 7125.0, + "Position": 279.0 + }, { + "StartTime": 7187.0, + "Position": 309.84 + }, { + "StartTime": 7250.0, + "Position": 336.0 + }, { + "StartTime": 7312.0, + "Position": 322.16 + }, { + "StartTime": 7375.0, + "Position": 308.0 + }, { + "StartTime": 7437.0, + "Position": 263.16 + }, { + "StartTime": 7500.0, + "Position": 256.0 + }, { + "StartTime": 7562.0, + "Position": 261.84 + }, { + "StartTime": 7625.0, + "Position": 277.0 + }, { + "StartTime": 7687.0, + "Position": 318.84 + }, { + "StartTime": 7750.0, + "Position": 336.0 + }, { + "StartTime": 7803.0, + "Position": 305.04 + }, { + "StartTime": 7857.0, + "Position": 307.76 + }, { + "StartTime": 7910.0, + "Position": 297.8 + }, { + "StartTime": 8000.0, + "Position": 256.0 + }] + }, { + "StartTime": 8500.0, + "Objects": [{ + "StartTime": 8500.0, + "Position": 32.0 + }, { + "StartTime": 8562.0, + "Position": 22.8515015 + }, { + "StartTime": 8625.0, + "Position": 28.5659637 + }, { + "StartTime": 8687.0, + "Position": 50.3433228 + }, { + "StartTime": 8750.0, + "Position": 56.58974 + }, { + "StartTime": 8812.0, + "Position": 64.23422 + }, { + "StartTime": 8875.0, + "Position": 67.7117844 + }, { + "StartTime": 8937.0, + "Position": 90.52607 + }, { + "StartTime": 9000.0, + "Position": 101.81015 + }, { + "StartTime": 9062.0, + "Position": 113.478188 + }, { + "StartTime": 9125.0, + "Position": 159.414444 + }, { + "StartTime": 9187.0, + "Position": 155.1861 + }, { + "StartTime": 9250.0, + "Position": 179.600418 + }, { + "StartTime": 9312.0, + "Position": 212.293015 + }, { + "StartTime": 9375.0, + "Position": 197.2076 + }, { + "StartTime": 9437.0, + "Position": 243.438324 + }, { + "StartTime": 9500.0, + "Position": 237.2304 + }, { + "StartTime": 9562.0, + "Position": 241.253983 + }, { + "StartTime": 9625.0, + "Position": 258.950623 + }, { + "StartTime": 9687.0, + "Position": 253.3786 + }, { + "StartTime": 9750.0, + "Position": 270.8865 + }, { + "StartTime": 9812.0, + "Position": 244.38974 + }, { + "StartTime": 9875.0, + "Position": 242.701874 + }, { + "StartTime": 9937.0, + "Position": 256.2331 + }, { + "StartTime": 10000.0, + "Position": 270.339874 + }, { + "StartTime": 10062.0, + "Position": 275.9349 + }, { + "StartTime": 10125.0, + "Position": 297.2969 + }, { + "StartTime": 10187.0, + "Position": 307.834137 + }, { + "StartTime": 10250.0, + "Position": 321.6449 + }, { + "StartTime": 10312.0, + "Position": 357.746338 + }, { + "StartTime": 10375.0, + "Position": 358.21875 + }, { + "StartTime": 10437.0, + "Position": 394.943 + }, { + "StartTime": 10500.0, + "Position": 401.0588 + }, { + "StartTime": 10558.0, + "Position": 418.21347 + }, { + "StartTime": 10616.0, + "Position": 424.6034 + }, { + "StartTime": 10674.0, + "Position": 455.835754 + }, { + "StartTime": 10732.0, + "Position": 477.5042 + }, { + "StartTime": 10790.0, + "Position": 476.290955 + }, { + "StartTime": 10848.0, + "Position": 470.943237 + }, { + "StartTime": 10906.0, + "Position": 503.3372 + }, { + "StartTime": 10999.0, + "Position": 508.166229 + }] + }, { + "StartTime": 11500.0, + "Objects": [{ + "StartTime": 11500.0, + "Position": 321.0 + }, { + "StartTime": 11562.0, + "Position": 17.0 + }, { + "StartTime": 11625.0, + "Position": 173.0 + }, { + "StartTime": 11687.0, + "Position": 170.0 + }, { + "StartTime": 11750.0, + "Position": 447.0 + }, { + "StartTime": 11812.0, + "Position": 218.0 + }, { + "StartTime": 11875.0, + "Position": 394.0 + }, { + "StartTime": 11937.0, + "Position": 46.0 + }, { + "StartTime": 12000.0, + "Position": 480.0 + }] + }, { + "StartTime": 12500.0, + "Objects": [{ + "StartTime": 12500.0, + "Position": 512.0 + }, { + "StartTime": 12562.0, + "Position": 491.3132 + }, { + "StartTime": 12625.0, + "Position": 484.3089 + }, { + "StartTime": 12687.0, + "Position": 454.6221 + }, { + "StartTime": 12750.0, + "Position": 433.617767 + }, { + "StartTime": 12812.0, + "Position": 399.930969 + }, { + "StartTime": 12875.0, + "Position": 395.926666 + }, { + "StartTime": 12937.0, + "Position": 361.239868 + }, { + "StartTime": 13000.0, + "Position": 353.235535 + }, { + "StartTime": 13062.0, + "Position": 314.548767 + }, { + "StartTime": 13125.0, + "Position": 315.544434 + }, { + "StartTime": 13187.0, + "Position": 288.857635 + }, { + "StartTime": 13250.0, + "Position": 254.853333 + }, { + "StartTime": 13312.0, + "Position": 239.166534 + }, { + "StartTime": 13375.0, + "Position": 240.1622 + }, { + "StartTime": 13437.0, + "Position": 212.4754 + }, { + "StartTime": 13500.0, + "Position": 194.471069 + }, { + "StartTime": 13562.0, + "Position": 161.784271 + }, { + "StartTime": 13625.0, + "Position": 145.779968 + }, { + "StartTime": 13687.0, + "Position": 129.09314 + }, { + "StartTime": 13750.0, + "Position": 104.088837 + }, { + "StartTime": 13812.0, + "Position": 95.40204 + }, { + "StartTime": 13875.0, + "Position": 61.3977356 + }, { + "StartTime": 13937.0, + "Position": 56.710907 + }, { + "StartTime": 14000.0, + "Position": 35.7066345 + }, { + "StartTime": 14062.0, + "Position": 5.019806 + }, { + "StartTime": 14125.0, + "Position": 0.0 + }, { + "StartTime": 14187.0, + "Position": 39.7696266 + }, { + "StartTime": 14250.0, + "Position": 23.0119171 + }, { + "StartTime": 14312.0, + "Position": 75.94882 + }, { + "StartTime": 14375.0, + "Position": 98.19112 + }, { + "StartTime": 14437.0, + "Position": 82.12803 + }, { + "StartTime": 14500.0, + "Position": 118.370323 + }, { + "StartTime": 14562.0, + "Position": 149.307236 + }, { + "StartTime": 14625.0, + "Position": 168.549515 + }, { + "StartTime": 14687.0, + "Position": 190.486435 + }, { + "StartTime": 14750.0, + "Position": 186.728714 + }, { + "StartTime": 14812.0, + "Position": 199.665634 + }, { + "StartTime": 14875.0, + "Position": 228.907928 + }, { + "StartTime": 14937.0, + "Position": 264.844849 + }, { + "StartTime": 15000.0, + "Position": 271.087128 + }, { + "StartTime": 15062.0, + "Position": 290.024017 + }, { + "StartTime": 15125.0, + "Position": 302.266327 + }, { + "StartTime": 15187.0, + "Position": 344.203247 + }, { + "StartTime": 15250.0, + "Position": 356.445526 + }, { + "StartTime": 15312.0, + "Position": 359.382446 + }, { + "StartTime": 15375.0, + "Position": 401.624725 + }, { + "StartTime": 15437.0, + "Position": 388.561646 + }, { + "StartTime": 15500.0, + "Position": 423.803925 + }, { + "StartTime": 15562.0, + "Position": 425.740845 + }, { + "StartTime": 15625.0, + "Position": 449.983124 + }, { + "StartTime": 15687.0, + "Position": 468.920044 + }, { + "StartTime": 15750.0, + "Position": 492.162323 + }, { + "StartTime": 15812.0, + "Position": 506.784332 + }, { + "StartTime": 15875.0, + "Position": 474.226227 + }, { + "StartTime": 15937.0, + "Position": 482.978638 + }, { + "StartTime": 16000.0, + "Position": 446.420532 + }, { + "StartTime": 16058.0, + "Position": 418.4146 + }, { + "StartTime": 16116.0, + "Position": 425.408844 + }, { + "StartTime": 16174.0, + "Position": 383.402924 + }, { + "StartTime": 16232.0, + "Position": 363.397156 + }, { + "StartTime": 16290.0, + "Position": 343.391235 + }, { + "StartTime": 16348.0, + "Position": 328.385468 + }, { + "StartTime": 16406.0, + "Position": 322.3797 + }, { + "StartTime": 16500.0, + "Position": 291.1977 + }] + }, { + "StartTime": 17000.0, + "Objects": [{ + "StartTime": 17000.0, + "Position": 256.0 + }, { + "StartTime": 17062.0, + "Position": 228.16 + }, { + "StartTime": 17125.0, + "Position": 234.0 + }, { + "StartTime": 17187.0, + "Position": 202.16 + }, { + "StartTime": 17250.0, + "Position": 176.0 + }, { + "StartTime": 17312.0, + "Position": 210.84 + }, { + "StartTime": 17375.0, + "Position": 221.0 + }, { + "StartTime": 17437.0, + "Position": 219.84 + }, { + "StartTime": 17500.0, + "Position": 256.0 + }, { + "StartTime": 17562.0, + "Position": 219.16 + }, { + "StartTime": 17625.0, + "Position": 228.0 + }, { + "StartTime": 17687.0, + "Position": 203.16 + }, { + "StartTime": 17750.0, + "Position": 176.0 + }, { + "StartTime": 17803.0, + "Position": 174.959991 + }, { + "StartTime": 17857.0, + "Position": 214.23999 + }, { + "StartTime": 17910.0, + "Position": 228.200012 + }, { + "StartTime": 18000.0, + "Position": 256.0 + }] + }, { + "StartTime": 18500.0, + "Objects": [{ + "StartTime": 18500.0, + "Position": 362.0 + }, { + "StartTime": 18559.0, + "Position": 249.0 + }, { + "StartTime": 18618.0, + "Position": 357.0 + }, { + "StartTime": 18678.0, + "Position": 167.0 + }, { + "StartTime": 18737.0, + "Position": 477.0 + }, { + "StartTime": 18796.0, + "Position": 411.0 + }, { + "StartTime": 18856.0, + "Position": 254.0 + }, { + "StartTime": 18915.0, + "Position": 308.0 + }, { + "StartTime": 18975.0, + "Position": 399.0 + }, { + "StartTime": 19034.0, + "Position": 176.0 + }, { + "StartTime": 19093.0, + "Position": 14.0 + }, { + "StartTime": 19153.0, + "Position": 258.0 + }, { + "StartTime": 19212.0, + "Position": 221.0 + }, { + "StartTime": 19271.0, + "Position": 481.0 + }, { + "StartTime": 19331.0, + "Position": 92.0 + }, { + "StartTime": 19390.0, + "Position": 211.0 + }, { + "StartTime": 19450.0, + "Position": 135.0 + }] + }, { + "StartTime": 19875.0, + "Objects": [{ + "StartTime": 19875.0, + "Position": 216.0 + }, { + "StartTime": 19937.0, + "Position": 215.307053 + }, { + "StartTime": 20000.0, + "Position": 236.036865 + }, { + "StartTime": 20062.0, + "Position": 236.312088 + }, { + "StartTime": 20125.0, + "Position": 235.838928 + }, { + "StartTime": 20187.0, + "Position": 269.9743 + }, { + "StartTime": 20250.0, + "Position": 285.999146 + }, { + "StartTime": 20312.0, + "Position": 283.669067 + }, { + "StartTime": 20375.0, + "Position": 317.446747 + }, { + "StartTime": 20437.0, + "Position": 330.750275 + }, { + "StartTime": 20500.0, + "Position": 344.0156 + }, { + "StartTime": 20562.0, + "Position": 318.472168 + }, { + "StartTime": 20625.0, + "Position": 309.165466 + }, { + "StartTime": 20687.0, + "Position": 317.044617 + }, { + "StartTime": 20750.0, + "Position": 280.457367 + }, { + "StartTime": 20812.0, + "Position": 272.220581 + }, { + "StartTime": 20875.0, + "Position": 270.3294 + }, { + "StartTime": 20937.0, + "Position": 262.57605 + }, { + "StartTime": 21000.0, + "Position": 244.803329 + }, { + "StartTime": 21062.0, + "Position": 215.958359 + }, { + "StartTime": 21125.0, + "Position": 177.79332 + }, { + "StartTime": 21187.0, + "Position": 190.948349 + }, { + "StartTime": 21250.0, + "Position": 158.78334 + }, { + "StartTime": 21312.0, + "Position": 136.93837 + }, { + "StartTime": 21375.0, + "Position": 119.121056 + }, { + "StartTime": 21437.0, + "Position": 132.387573 + }, { + "StartTime": 21500.0, + "Position": 124.503014 + }, { + "StartTime": 21562.0, + "Position": 118.749374 + }, { + "StartTime": 21625.0, + "Position": 123.165535 + }, { + "StartTime": 21687.0, + "Position": 96.02999 + }, { + "StartTime": 21750.0, + "Position": 118.547928 + }, { + "StartTime": 21812.0, + "Position": 128.856232 + }, { + "StartTime": 21875.0, + "Position": 124.28746 + }, { + "StartTime": 21937.0, + "Position": 150.754929 + }, { + "StartTime": 22000.0, + "Position": 149.528732 + }, { + "StartTime": 22062.0, + "Position": 145.1691 + }, { + "StartTime": 22125.0, + "Position": 182.802155 + }, { + "StartTime": 22187.0, + "Position": 178.6452 + }, { + "StartTime": 22250.0, + "Position": 213.892181 + }, { + "StartTime": 22312.0, + "Position": 218.713028 + }, { + "StartTime": 22375.0, + "Position": 240.4715 + }, { + "StartTime": 22437.0, + "Position": 239.371887 + }, { + "StartTime": 22500.0, + "Position": 261.907257 + }, { + "StartTime": 22562.0, + "Position": 314.353119 + }, { + "StartTime": 22625.0, + "Position": 299.273376 + }, { + "StartTime": 22687.0, + "Position": 356.98288 + }, { + "StartTime": 22750.0, + "Position": 339.078552 + }, { + "StartTime": 22812.0, + "Position": 377.8958 + }, { + "StartTime": 22875.0, + "Position": 398.054047 + }, { + "StartTime": 22937.0, + "Position": 398.739441 + }, { + "StartTime": 23000.0, + "Position": 407.178467 + }, { + "StartTime": 23062.0, + "Position": 444.8687 + }, { + "StartTime": 23125.0, + "Position": 417.069977 + }, { + "StartTime": 23187.0, + "Position": 454.688477 + }, { + "StartTime": 23250.0, + "Position": 428.9612 + }, { + "StartTime": 23312.0, + "Position": 441.92807 + }, { + "StartTime": 23375.0, + "Position": 439.749878 + }, { + "StartTime": 23433.0, + "Position": 455.644684 + }, { + "StartTime": 23491.0, + "Position": 440.7359 + }, { + "StartTime": 23549.0, + "Position": 430.0944 + }, { + "StartTime": 23607.0, + "Position": 420.796173 + }, { + "StartTime": 23665.0, + "Position": 435.897461 + }, { + "StartTime": 23723.0, + "Position": 418.462555 + }, { + "StartTime": 23781.0, + "Position": 405.53775 + }, { + "StartTime": 23874.0, + "Position": 408.720825 + }] + }] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic.osu b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic.osu new file mode 100644 index 0000000000..40b4409760 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/basic.osu @@ -0,0 +1,27 @@ +osu file format v14 + +[Difficulty] +HPDrainRate:6 +CircleSize:4 +OverallDifficulty:7 +ApproachRate:8.3 +SliderMultiplier:1.6 +SliderTickRate:1 + +[TimingPoints] +500,500,4,2,1,50,1,0 +13426,-100,4,3,1,45,0,0 +14884,-100,4,2,1,50,0,0 + +[HitObjects] +96,192,500,6,0,L|416:192,2,320 +256,192,3000,12,0,4000,0:0:0:0: +256,192,4500,12,0,5500,0:0:0:0: +256,192,6000,12,0,6500,0:0:0:0: +256,128,7000,6,0,L|352:128,4,80 +32,192,8500,6,0,B|32:384|256:384|256:192|256:192|256:0|512:0|512:192,1,800 +256,192,11500,12,0,12000,0:0:0:0: +512,320,12500,6,0,B|0:256|0:256|512:96|512:96|256:32,1,1280 +256,256,17000,6,0,L|160:256,4,80 +256,192,18500,12,0,19450,0:0:0:0: +216,231,19875,6,0,B|216:135|280:135|344:135|344:199|344:263|248:327|248:327|120:327|120:327|56:39|408:39|408:39|472:150|408:342,1,1280 diff --git a/osu.Game.Rulesets.Catch/Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch/Tests/CatchBeatmapConversionTest.cs new file mode 100644 index 0000000000..e40510b71b --- /dev/null +++ b/osu.Game.Rulesets.Catch/Tests/CatchBeatmapConversionTest.cs @@ -0,0 +1,67 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.MathUtils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.Beatmaps; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Objects; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Rulesets.Catch.Tests +{ + public class CatchBeatmapConversionTest : BeatmapConversionTest + { + protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; + + [TestCase("basic"), Ignore("See: https://github.com/ppy/osu/issues/2232")] + public new void Test(string name) + { + base.Test(name); + } + + protected override IEnumerable CreateConvertValue(HitObject hitObject) + { + if (hitObject is JuiceStream stream) + { + foreach (var nested in stream.NestedHitObjects) + { + yield return new ConvertValue + { + StartTime = nested.StartTime, + Position = ((CatchHitObject)nested).X * CatchPlayfield.BASE_WIDTH + }; + } + } + else + { + yield return new ConvertValue + { + StartTime = hitObject.StartTime, + Position = ((CatchHitObject)hitObject).X * CatchPlayfield.BASE_WIDTH + }; + } + } + + protected override IBeatmapConverter CreateConverter(Beatmap beatmap) => new CatchBeatmapConverter(); + } + + public struct ConvertValue : IEquatable + { + /// + /// A sane value to account for osu!stable using ints everwhere. + /// + private const float conversion_lenience = 2; + + public double StartTime; + public float Position; + + public bool Equals(ConvertValue other) + => Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience) + && Precision.AlmostEquals(Position, other.Position, conversion_lenience); + } +} diff --git a/osu.Game.Rulesets.Catch/Tests/TestCaseBananaShower.cs b/osu.Game.Rulesets.Catch/Tests/TestCaseBananaShower.cs index e23e7633ca..ec9dd15673 100644 --- a/osu.Game.Rulesets.Catch/Tests/TestCaseBananaShower.cs +++ b/osu.Game.Rulesets.Catch/Tests/TestCaseBananaShower.cs @@ -12,7 +12,6 @@ using osu.Game.Rulesets.Catch.UI; namespace osu.Game.Rulesets.Catch.Tests { [TestFixture] - [Ignore("getting CI working")] public class TestCaseBananaShower : Game.Tests.Visual.TestCasePlayer { public override IReadOnlyList RequiredTypes => new[] @@ -29,16 +28,14 @@ namespace osu.Game.Rulesets.Catch.Tests { } - protected override Beatmap CreateBeatmap() + protected override Beatmap CreateBeatmap(Ruleset ruleset) { var beatmap = new Beatmap { BeatmapInfo = new BeatmapInfo { - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 6, - } + BaseDifficulty = new BeatmapDifficulty { CircleSize = 6 }, + Ruleset = ruleset.RulesetInfo } }; diff --git a/osu.Game.Rulesets.Catch/Tests/TestCaseCatchPlayer.cs b/osu.Game.Rulesets.Catch/Tests/TestCaseCatchPlayer.cs index dbd5e5b36c..efebfa9739 100644 --- a/osu.Game.Rulesets.Catch/Tests/TestCaseCatchPlayer.cs +++ b/osu.Game.Rulesets.Catch/Tests/TestCaseCatchPlayer.cs @@ -6,7 +6,6 @@ using NUnit.Framework; namespace osu.Game.Rulesets.Catch.Tests { [TestFixture] - [Ignore("getting CI working")] public class TestCaseCatchPlayer : Game.Tests.Visual.TestCasePlayer { public TestCaseCatchPlayer() : base(new CatchRuleset()) diff --git a/osu.Game.Rulesets.Catch/Tests/TestCaseCatchStacker.cs b/osu.Game.Rulesets.Catch/Tests/TestCaseCatchStacker.cs index b9fa38f74e..8e5843f40a 100644 --- a/osu.Game.Rulesets.Catch/Tests/TestCaseCatchStacker.cs +++ b/osu.Game.Rulesets.Catch/Tests/TestCaseCatchStacker.cs @@ -8,7 +8,6 @@ using osu.Game.Rulesets.Catch.Objects; namespace osu.Game.Rulesets.Catch.Tests { [TestFixture] - [Ignore("getting CI working")] public class TestCaseCatchStacker : Game.Tests.Visual.TestCasePlayer { public TestCaseCatchStacker() @@ -16,19 +15,18 @@ namespace osu.Game.Rulesets.Catch.Tests { } - protected override Beatmap CreateBeatmap() + protected override Beatmap CreateBeatmap(Ruleset ruleset) { var beatmap = new Beatmap { BeatmapInfo = new BeatmapInfo { - BaseDifficulty = new BeatmapDifficulty - { - CircleSize = 6, - } + BaseDifficulty = new BeatmapDifficulty { CircleSize = 6 }, + Ruleset = ruleset.RulesetInfo } }; + 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 }); diff --git a/osu.Game.Rulesets.Catch/Tests/TestCaseCatcherArea.cs b/osu.Game.Rulesets.Catch/Tests/TestCaseCatcherArea.cs index a2d18520d7..0329921c92 100644 --- a/osu.Game.Rulesets.Catch/Tests/TestCaseCatcherArea.cs +++ b/osu.Game.Rulesets.Catch/Tests/TestCaseCatcherArea.cs @@ -13,7 +13,6 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Catch.Tests { [TestFixture] - [Ignore("getting CI working")] public class TestCaseCatcherArea : OsuTestCase { private RulesetInfo catchRuleset; diff --git a/osu.Game.Rulesets.Catch/Tests/TestCaseFruitObjects.cs b/osu.Game.Rulesets.Catch/Tests/TestCaseFruitObjects.cs index d406231cc9..595ca6cb24 100644 --- a/osu.Game.Rulesets.Catch/Tests/TestCaseFruitObjects.cs +++ b/osu.Game.Rulesets.Catch/Tests/TestCaseFruitObjects.cs @@ -6,17 +6,15 @@ using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.MathUtils; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawable; using osu.Game.Rulesets.Catch.Objects.Drawable.Pieces; using osu.Game.Tests.Visual; using OpenTK; -using OpenTK.Graphics; namespace osu.Game.Rulesets.Catch.Tests { - [Ignore("getting CI working")] + [TestFixture] public class TestCaseFruitObjects : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] @@ -62,8 +60,6 @@ namespace osu.Game.Rulesets.Catch.Tests Scale = 1.5f, }; - fruit.ComboColour = colourForRrepesentation(fruit.VisualRepresentation); - return new DrawableFruit(fruit) { Anchor = Anchor.Centre, @@ -74,31 +70,5 @@ namespace osu.Game.Rulesets.Catch.Tests LifetimeEnd = double.PositiveInfinity, }; } - - private Color4 colourForRrepesentation(FruitVisualRepresentation representation) - { - switch (representation) - { - default: - case FruitVisualRepresentation.Pear: - return new Color4(17, 136, 170, 255); - case FruitVisualRepresentation.Grape: - return new Color4(204, 102, 0, 255); - case FruitVisualRepresentation.Raspberry: - return new Color4(121, 9, 13, 255); - case FruitVisualRepresentation.Pineapple: - return new Color4(102, 136, 0, 255); - case FruitVisualRepresentation.Banana: - switch (RNG.Next(0, 3)) - { - default: - return new Color4(255, 240, 0, 255); - case 1: - return new Color4(255, 192, 0, 255); - case 2: - return new Color4(214, 221, 28, 255); - } - } - } } } diff --git a/osu.Game.Rulesets.Catch/Tests/TestCaseHyperdash.cs b/osu.Game.Rulesets.Catch/Tests/TestCaseHyperdash.cs index 59659b3d0d..7564adea8c 100644 --- a/osu.Game.Rulesets.Catch/Tests/TestCaseHyperdash.cs +++ b/osu.Game.Rulesets.Catch/Tests/TestCaseHyperdash.cs @@ -8,7 +8,6 @@ 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() @@ -16,9 +15,10 @@ namespace osu.Game.Rulesets.Catch.Tests { } - protected override Beatmap CreateBeatmap() + protected override Beatmap CreateBeatmap(Ruleset ruleset) { - var beatmap = new Beatmap(); + var beatmap = new Beatmap { BeatmapInfo = { Ruleset = ruleset.RulesetInfo } }; + for (int i = 0; i < 512; i++) if (i % 5 < 3) diff --git a/osu.Game.Rulesets.Catch/Tests/TestCasePerformancePoints.cs b/osu.Game.Rulesets.Catch/Tests/TestCasePerformancePoints.cs index 725eb5cf76..2be6dd005d 100644 --- a/osu.Game.Rulesets.Catch/Tests/TestCasePerformancePoints.cs +++ b/osu.Game.Rulesets.Catch/Tests/TestCasePerformancePoints.cs @@ -5,7 +5,7 @@ using NUnit.Framework; namespace osu.Game.Rulesets.Catch.Tests { - [Ignore("getting CI working")] + [TestFixture] public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints { public TestCasePerformancePoints() diff --git a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs index 956a524121..41dd7fdf4e 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs @@ -3,6 +3,7 @@ using osu.Framework.Input; using osu.Game.Beatmaps; +using osu.Game.Input.Handlers; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawable; @@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Catch.UI public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(this); - protected override FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay); + protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay); protected override BeatmapProcessor CreateBeatmapProcessor() => new CatchBeatmapProcessor(); diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 7c548f70d4..bf2f9db4a8 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -54,7 +54,6 @@ namespace osu.Game.Rulesets.Catch.UI if (caughtFruit == null) return; - caughtFruit.AccentColour = fruit.AccentColour; caughtFruit.RelativePositionAxes = Axes.None; caughtFruit.Position = new Vector2(MovableCatcher.ToLocalSpace(fruit.ScreenSpaceDrawQuad.Centre).X - MovableCatcher.DrawSize.X / 2, 0); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 9922d4c8ad..4734e40803 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps yield break; } - var objects = IsForCurrentRuleset ? generateSpecific(original) : generateConverted(original); + var objects = IsForCurrentRuleset ? generateSpecific(original, beatmap) : generateConverted(original, beatmap); if (objects == null) yield break; @@ -110,10 +110,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// Method that generates hit objects for osu!mania specific beatmaps. /// /// The original hit object. + /// The original beatmap. This is used to look-up any values dependent on a fully-loaded beatmap. /// The hit objects generated. - private IEnumerable generateSpecific(HitObject original) + private IEnumerable generateSpecific(HitObject original, Beatmap originalBeatmap) { - var generator = new SpecificBeatmapPatternGenerator(random, original, beatmap, lastPattern); + var generator = new SpecificBeatmapPatternGenerator(random, original, beatmap, lastPattern, originalBeatmap); Pattern newPattern = generator.Generate(); lastPattern = newPattern; @@ -125,26 +126,25 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// Method that generates hit objects for non-osu!mania beatmaps. /// /// The original hit object. + /// The original beatmap. This is used to look-up any values dependent on a fully-loaded beatmap. /// The hit objects generated. - private IEnumerable generateConverted(HitObject original) + private IEnumerable generateConverted(HitObject original, Beatmap originalBeatmap) { var endTimeData = original as IHasEndTime; var distanceData = original as IHasDistance; var positionData = original as IHasPosition; - // Following lines currently commented out to appease resharper - Patterns.PatternGenerator conversion = null; if (distanceData != null) - conversion = new DistanceObjectPatternGenerator(random, original, beatmap, lastPattern); + conversion = new DistanceObjectPatternGenerator(random, original, beatmap, lastPattern, originalBeatmap); else if (endTimeData != null) - conversion = new EndTimeObjectPatternGenerator(random, original, beatmap); + conversion = new EndTimeObjectPatternGenerator(random, original, beatmap, originalBeatmap); else if (positionData != null) { computeDensity(original.StartTime); - conversion = new HitObjectPatternGenerator(random, original, beatmap, lastPattern, lastTime, lastPosition, density, lastStair); + conversion = new HitObjectPatternGenerator(random, original, beatmap, lastPattern, lastTime, lastPosition, density, lastStair, originalBeatmap); recordNote(original.StartTime, positionData.Position); } @@ -153,10 +153,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps return null; Pattern newPattern = conversion.Generate(); - lastPattern = newPattern; - var stairPatternGenerator = conversion as HitObjectPatternGenerator; - lastStair = stairPatternGenerator?.StairType ?? lastStair; + lastPattern = conversion is EndTimeObjectPatternGenerator ? lastPattern : newPattern; + lastStair = (conversion as HitObjectPatternGenerator)?.StairType ?? lastStair; return newPattern.HitObjects; } @@ -166,8 +165,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// private class SpecificBeatmapPatternGenerator : Patterns.Legacy.PatternGenerator { - public SpecificBeatmapPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern) - : base(random, hitObject, beatmap, previousPattern) + public SpecificBeatmapPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, Beatmap originalBeatmap) + : base(random, hitObject, beatmap, previousPattern, originalBeatmap) { } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index a102781e70..28cf119833 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Audio; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.MathUtils; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; @@ -29,11 +30,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy private PatternType convertType; - public DistanceObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern) - : base(random, hitObject, beatmap, previousPattern) + public DistanceObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, Beatmap originalBeatmap) + : base(random, hitObject, beatmap, previousPattern, originalBeatmap) { convertType = PatternType.None; - if (Beatmap.ControlPointInfo.EffectPointAt(hitObject.StartTime).KiaiMode) + if (!Beatmap.ControlPointInfo.EffectPointAt(hitObject.StartTime).KiaiMode) convertType = PatternType.LowProbability; var distanceData = hitObject as IHasDistance; @@ -305,19 +306,19 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy p4 = 0; break; case 3: - p2 = Math.Max(p2, 0.1); + p2 = Math.Min(p2, 0.1); p3 = 0; p4 = 0; break; case 4: - p2 = Math.Max(p2, 0.3); - p3 = Math.Max(p3, 0.04); + p2 = Math.Min(p2, 0.3); + p3 = Math.Min(p3, 0.04); p4 = 0; break; case 5: - p2 = Math.Max(p2, 0.34); - p3 = Math.Max(p3, 0.1); - p4 = Math.Max(p4, 0.03); + p2 = Math.Min(p2, 0.34); + p3 = Math.Min(p3, 0.1); + p4 = Math.Min(p4, 0.03); break; } @@ -396,17 +397,19 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy // Create the hold note addToPattern(pattern, holdColumn, startTime, endTime); - int noteCount = 1; + int nextColumn = Random.Next(RandomStart, TotalColumns); + int noteCount; if (ConversionDifficulty > 6.5) noteCount = GetRandomNoteCount(0.63, 0); else if (ConversionDifficulty > 4) noteCount = GetRandomNoteCount(TotalColumns < 6 ? 0.12 : 0.45, 0); else if (ConversionDifficulty > 2.5) noteCount = GetRandomNoteCount(TotalColumns < 6 ? 0 : 0.24, 0); + else + noteCount = 0; noteCount = Math.Min(TotalColumns - 1, noteCount); bool ignoreHead = !sampleInfoListAt(startTime).Any(s => s.Name == SampleInfo.HIT_WHISTLE || s.Name == SampleInfo.HIT_FINISH || s.Name == SampleInfo.HIT_CLAP); - int nextColumn = Random.Next(RandomStart, TotalColumns); var rowPattern = new Pattern(); for (int i = 0; i <= spanCount; i++) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs index 278a4c4aab..ffbabba75a 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs @@ -7,6 +7,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using System.Linq; using osu.Game.Audio; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Objects; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy @@ -15,8 +16,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { private readonly double endTime; - public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap) - : base(random, hitObject, beatmap, new Pattern()) + public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Beatmap originalBeatmap) + : base(random, hitObject, beatmap, new Pattern(), originalBeatmap) { var endtimeData = HitObject as IHasEndTime; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs index c4ef23a982..e126534c54 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using OpenTK; using osu.Game.Audio; +using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Mania.MathUtils; using osu.Game.Rulesets.Mania.Objects; @@ -19,8 +20,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy private readonly PatternType convertType; - public HitObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density, PatternType lastStair) - : base(random, hitObject, beatmap, previousPattern) + public HitObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density, PatternType lastStair, Beatmap originalBeatmap) + : base(random, hitObject, beatmap, previousPattern, originalBeatmap) { if (previousTime > hitObject.StartTime) throw new ArgumentOutOfRangeException(nameof(previousTime)); if (density < 0) throw new ArgumentOutOfRangeException(nameof(density)); @@ -308,20 +309,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy p5 = 0; break; case 3: - p2 = Math.Max(p2, 0.1); + p2 = Math.Min(p2, 0.1); p3 = 0; p4 = 0; p5 = 0; break; case 4: - p2 = Math.Max(p2, 0.23); - p3 = Math.Max(p3, 0.04); + p2 = Math.Min(p2, 0.23); + p3 = Math.Min(p3, 0.04); p4 = 0; p5 = 0; break; case 5: - p3 = Math.Max(p3, 0.15); - p4 = Math.Max(p4, 0.03); + p3 = Math.Min(p3, 0.15); + p4 = Math.Min(p4, 0.03); p5 = 0; break; } @@ -355,23 +356,23 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy p3 = 0; break; case 3: - centreProbability = Math.Max(centreProbability, 0.03); - p2 = Math.Max(p2, 0.1); + centreProbability = Math.Min(centreProbability, 0.03); + p2 = 0; p3 = 0; break; case 4: centreProbability = 0; - p2 = Math.Max(p2 * 2, 0.2); + p2 = Math.Min(p2 * 2, 0.2); p3 = 0; break; case 5: - centreProbability = Math.Max(centreProbability, 0.03); + centreProbability = Math.Min(centreProbability, 0.03); p3 = 0; break; case 6: centreProbability = 0; - p2 = Math.Max(p2 * 2, 0.5); - p3 = Math.Max(p3 * 2, 0.15); + p2 = Math.Min(p2 * 2, 0.5); + p3 = Math.Min(p3 * 2, 0.15); break; } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs index 5f98749f0c..501950cdcd 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs @@ -25,14 +25,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// protected readonly FastRandom Random; - protected PatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern) + /// + /// The beatmap which is being converted from. + /// + protected readonly Beatmap OriginalBeatmap; + + protected PatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, Beatmap originalBeatmap) : base(hitObject, beatmap, previousPattern) { if (random == null) throw new ArgumentNullException(nameof(random)); - if (beatmap == null) throw new ArgumentNullException(nameof(beatmap)); - if (previousPattern == null) throw new ArgumentNullException(nameof(previousPattern)); + if (originalBeatmap == null) throw new ArgumentNullException(nameof(originalBeatmap)); Random = random; + OriginalBeatmap = originalBeatmap; + RandomStart = TotalColumns == 8 ? 1 : 0; } @@ -94,17 +100,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (conversionDifficulty != null) return conversionDifficulty.Value; - HitObject lastObject = Beatmap.HitObjects.LastOrDefault(); - HitObject firstObject = Beatmap.HitObjects.FirstOrDefault(); + HitObject lastObject = OriginalBeatmap.HitObjects.LastOrDefault(); + HitObject firstObject = OriginalBeatmap.HitObjects.FirstOrDefault(); double drainTime = (lastObject?.StartTime ?? 0) - (firstObject?.StartTime ?? 0); - drainTime -= Beatmap.TotalBreakTime; + drainTime -= OriginalBeatmap.TotalBreakTime; if (drainTime == 0) - drainTime = 10000; + drainTime = 10000000; - BeatmapDifficulty difficulty = Beatmap.BeatmapInfo.BaseDifficulty; - conversionDifficulty = ((difficulty.DrainRate + MathHelper.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + Beatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15; + // We need this in seconds + drainTime /= 1000; + + BeatmapDifficulty difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty; + conversionDifficulty = ((difficulty.DrainRate + MathHelper.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15; conversionDifficulty = Math.Min(conversionDifficulty.Value, 12); return conversionDifficulty.Value; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs index aeefc2f396..cb500735f7 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs @@ -14,5 +14,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// The number of s which this stage contains. /// public int Columns; + + /// + /// Whether the column index is a special column for this stage. + /// + /// The 0-based column index. + /// Whether the column is a special column. + public bool IsSpecialColumn(int column) => Columns % 2 == 1 && column == Columns / 2; } } diff --git a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs index 75a8543548..f4e3d54a3d 100644 --- a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs @@ -4,18 +4,142 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mods; +using System; using System.Collections.Generic; namespace osu.Game.Rulesets.Mania { - public class ManiaDifficultyCalculator : DifficultyCalculator + internal class ManiaDifficultyCalculator : DifficultyCalculator { + private const double star_scaling_factor = 0.018; + + /// + /// In milliseconds. For difficulty calculation we will only look at the highest strain value in each time interval of size strain_step. + /// This is to eliminate higher influence of stream over aim by simply having more HitObjects with high strain. + /// The higher this value, the less strains there will be, indirectly giving long beatmaps an advantage. + /// + private const double strain_step = 400; + + /// + /// The weighting of each strain value decays to this number * it's previous value + /// + private const double decay_weight = 0.9; + + /// + /// HitObjects are stored as a member variable. + /// + private readonly List difficultyHitObjects = new List(); + public ManiaDifficultyCalculator(Beatmap beatmap) : base(beatmap) { } - public override double Calculate(Dictionary categoryDifficulty = null) => 0; + public ManiaDifficultyCalculator(Beatmap beatmap, Mod[] mods) + : base(beatmap, mods) + { + } + + public override double Calculate(Dictionary categoryDifficulty = null) + { + // Fill our custom DifficultyHitObject class, that carries additional information + difficultyHitObjects.Clear(); + + int columnCount = (Beatmap as ManiaBeatmap)?.TotalColumns ?? 7; + + foreach (var hitObject in Beatmap.HitObjects) + difficultyHitObjects.Add(new ManiaHitObjectDifficulty(hitObject, columnCount)); + + // Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure. + difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime)); + + if (!calculateStrainValues()) + return 0; + + double starRating = calculateDifficulty() * star_scaling_factor; + + categoryDifficulty?.Add("Strain", starRating); + + return starRating; + } + + private bool calculateStrainValues() + { + // Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment. + using (List.Enumerator hitObjectsEnumerator = difficultyHitObjects.GetEnumerator()) + { + if (!hitObjectsEnumerator.MoveNext()) + return false; + + ManiaHitObjectDifficulty current = hitObjectsEnumerator.Current; + + // First hitObject starts at strain 1. 1 is the default for strain values, so we don't need to set it here. See DifficultyHitObject. + while (hitObjectsEnumerator.MoveNext()) + { + var next = hitObjectsEnumerator.Current; + next?.CalculateStrains(current, TimeRate); + current = next; + } + + return true; + } + } + + private double calculateDifficulty() + { + double actualStrainStep = strain_step * TimeRate; + + // Find the highest strain value within each strain step + List highestStrains = new List(); + double intervalEndTime = actualStrainStep; + double maximumStrain = 0; // We need to keep track of the maximum strain in the current interval + + ManiaHitObjectDifficulty previousHitObject = null; + foreach (var hitObject in difficultyHitObjects) + { + // While we are beyond the current interval push the currently available maximum to our strain list + while (hitObject.BaseHitObject.StartTime > intervalEndTime) + { + highestStrains.Add(maximumStrain); + + // The maximum strain of the next interval is not zero by default! We need to take the last hitObject we encountered, take its strain and apply the decay + // until the beginning of the next interval. + if (previousHitObject == null) + { + maximumStrain = 0; + } + else + { + double individualDecay = Math.Pow(ManiaHitObjectDifficulty.INDIVIDUAL_DECAY_BASE, (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000); + double overallDecay = Math.Pow(ManiaHitObjectDifficulty.OVERALL_DECAY_BASE, (intervalEndTime - previousHitObject.BaseHitObject.StartTime) / 1000); + maximumStrain = previousHitObject.IndividualStrain * individualDecay + previousHitObject.OverallStrain * overallDecay; + } + + // Go to the next time interval + intervalEndTime += actualStrainStep; + } + + // Obtain maximum strain + double strain = hitObject.IndividualStrain + hitObject.OverallStrain; + maximumStrain = Math.Max(strain, maximumStrain); + + previousHitObject = hitObject; + } + + // Build the weighted sum over the highest strains for each interval + double difficulty = 0; + double weight = 1; + highestStrains.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain. + + foreach (double strain in highestStrains) + { + difficulty += weight * strain; + weight *= decay_weight; + } + + return difficulty; + } protected override BeatmapConverter CreateBeatmapConverter(Beatmap beatmap) => new ManiaBeatmapConverter(true, beatmap); } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 3bfb4d3e44..e135e14001 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -12,6 +12,8 @@ using System.Linq; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Game.Graphics; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Replays.Types; namespace osu.Game.Rulesets.Mania { @@ -89,6 +91,7 @@ namespace osu.Game.Rulesets.Mania }, new ManiaModRandom(), new ManiaModDualStages(), + new ManiaModMirror(), new MultiMod { Mods = new Mod[] @@ -110,9 +113,11 @@ namespace osu.Game.Rulesets.Mania public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_mania_o }; - public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new ManiaDifficultyCalculator(beatmap); + public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new ManiaDifficultyCalculator(beatmap, mods); - public override int LegacyID => 3; + public override int? LegacyID => 3; + + public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new ManiaReplayFrame(); public ManiaRuleset(RulesetInfo rulesetInfo = null) : base(rulesetInfo) diff --git a/osu.Game.Rulesets.Mania/MathUtils/FastRandom.cs b/osu.Game.Rulesets.Mania/MathUtils/FastRandom.cs index e14473c478..c8277af415 100644 --- a/osu.Game.Rulesets.Mania/MathUtils/FastRandom.cs +++ b/osu.Game.Rulesets.Mania/MathUtils/FastRandom.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Mania.MathUtils /// internal class FastRandom { - private const double uint_to_real = 1.0 / (uint.MaxValue + 1.0); + private const double int_to_real = 1.0 / (int.MaxValue + 1.0); private const uint int_mask = 0x7FFFFFFF; private const uint y = 842502087; private const uint z = 3579807591; @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Mania.MathUtils /// Generates a random double value within the range [0, 1). /// /// The random value. - public double NextDouble() => uint_to_real * NextUInt(); + public double NextDouble() => int_to_real * Next(); private uint bitBuffer; private int bitIndex = 32; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs index 3c5179cef0..9ceb0ab7ea 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mods; @@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Mods return new Score { User = new User { Username = "osu!topus!" }, - Replay = new ManiaAutoGenerator(beatmap).Generate(), + Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), }; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs index 7c7dc5e4f7..99f49e6620 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDaycore.cs @@ -7,6 +7,6 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModDaycore : ModDaycore { - public override double ScoreMultiplier => 0.3; + public override double ScoreMultiplier => 0.5; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs index 64ce86e748..a9d77988c8 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDoubleTime.cs @@ -7,6 +7,6 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModDoubleTime : ModDoubleTime { - public override double ScoreMultiplier => 1.0; + public override double ScoreMultiplier => 1; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs index 3330d87e88..a1f9e0290e 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModDualStages.cs @@ -16,8 +16,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override string Name => "Dual Stages"; public override string ShortenedName => "DS"; public override string Description => @"Double the stages, double the fun!"; - public override double ScoreMultiplier => 1; - public override bool Ranked => false; + public override double ScoreMultiplier => 0; public void ApplyToBeatmapConverter(BeatmapConverter beatmapConverter) { diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs index 1faed5e1c0..0b3e851c64 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs @@ -7,5 +7,6 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModEasy : ModEasy { + public override string Description => @"More forgiving HP drain, less accuracy required, and three lives!"; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs index 03442507d6..ca5667a400 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs @@ -9,10 +9,11 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModFadeIn : Mod { - public override string Name => "FadeIn"; + public override string Name => "Fade In"; public override string ShortenedName => "FI"; public override FontAwesome Icon => FontAwesome.fa_osu_mod_hidden; public override ModType Type => ModType.DifficultyIncrease; + public override string Description => @"Keys appear out of nowhere!"; public override double ScoreMultiplier => 1; public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) }; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs index 89eb02268e..8d8693d11f 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFlashlight.cs @@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModFlashlight : ModFlashlight { - public override double ScoreMultiplier => 1.0; + public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(ModHidden) }; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs index 2f8404609f..c00bb4275a 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHalfTime.cs @@ -7,6 +7,6 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModHalfTime : ModHalfTime { - public override double ScoreMultiplier => 0.3; + public override double ScoreMultiplier => 0.5; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHardRock.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHardRock.cs index 91edbaf0cf..8b77ea4c25 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHardRock.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHardRock.cs @@ -7,6 +7,6 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModHardRock : ModHardRock { - public override double ScoreMultiplier => 1.0; + public override double ScoreMultiplier => 1; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index c2fc07da89..9317dba19f 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs @@ -8,8 +8,8 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModHidden : ModHidden { - public override string Description => @"The notes fade out before you hit them!"; - public override double ScoreMultiplier => 1.0; + public override string Description => @"Keys fade out before you hit them!"; + public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) }; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs index 8a6943d99b..c0107e3758 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey1.cs @@ -6,6 +6,8 @@ namespace osu.Game.Rulesets.Mania.Mods public class ManiaModKey1 : ManiaKeyMod { public override int KeyCount => 1; - public override string Name => "1K"; + public override string Name => "One Key"; + public override string ShortenedName => "1K"; + public override string Description => @"Play with one key."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs index 553827ac1c..11dbe0ba76 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey2.cs @@ -6,6 +6,8 @@ namespace osu.Game.Rulesets.Mania.Mods public class ManiaModKey2 : ManiaKeyMod { public override int KeyCount => 2; - public override string Name => "2K"; + public override string Name => "Two Keys"; + public override string ShortenedName => "2K"; + public override string Description => @"Play with two keys."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs index ef048c848e..94ad53d8ea 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey3.cs @@ -6,6 +6,8 @@ namespace osu.Game.Rulesets.Mania.Mods public class ManiaModKey3 : ManiaKeyMod { public override int KeyCount => 3; - public override string Name => "3K"; + public override string Name => "Three Keys"; + public override string ShortenedName => "3K"; + public override string Description => @"Play with three keys."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey4.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey4.cs index 9c713d920f..d9c27c5ef1 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey4.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey4.cs @@ -6,6 +6,8 @@ namespace osu.Game.Rulesets.Mania.Mods public class ManiaModKey4 : ManiaKeyMod { public override int KeyCount => 4; - public override string Name => "4K"; + public override string Name => "Four Keys"; + public override string ShortenedName => "4K"; + public override string Description => @"Play with four keys."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey5.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey5.cs index a83faf4627..e54bae93a7 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey5.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey5.cs @@ -6,6 +6,8 @@ namespace osu.Game.Rulesets.Mania.Mods public class ManiaModKey5 : ManiaKeyMod { public override int KeyCount => 5; - public override string Name => "5K"; + public override string Name => "Five Keys"; + public override string ShortenedName => "5K"; + public override string Description => @"Play with five keys."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey6.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey6.cs index d7df901048..9c3bdf46b9 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey6.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey6.cs @@ -6,6 +6,8 @@ namespace osu.Game.Rulesets.Mania.Mods public class ManiaModKey6 : ManiaKeyMod { public override int KeyCount => 6; - public override string Name => "6K"; + public override string Name => "Six Keys"; + public override string ShortenedName => "6K"; + public override string Description => @"Play with six keys."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey7.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey7.cs index 4a3f9857e5..f17ac80be5 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey7.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey7.cs @@ -6,6 +6,8 @@ namespace osu.Game.Rulesets.Mania.Mods public class ManiaModKey7 : ManiaKeyMod { public override int KeyCount => 7; - public override string Name => "7K"; + public override string Name => "Seven Keys"; + public override string ShortenedName => "7K"; + public override string Description => @"Play with seven keys."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey8.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey8.cs index 22c301fb7a..36a6fc838f 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey8.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey8.cs @@ -6,6 +6,8 @@ namespace osu.Game.Rulesets.Mania.Mods public class ManiaModKey8 : ManiaKeyMod { public override int KeyCount => 8; - public override string Name => "8K"; + public override string Name => "Eight Keys"; + public override string ShortenedName => "8K"; + public override string Description => @"Play with eight keys."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModKey9.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModKey9.cs index b2a0bc4ddf..10f03e2480 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModKey9.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModKey9.cs @@ -6,6 +6,8 @@ namespace osu.Game.Rulesets.Mania.Mods public class ManiaModKey9 : ManiaKeyMod { public override int KeyCount => 9; - public override string Name => "9K"; + public override string Name => "Nine Keys"; + public override string ShortenedName => "9K"; + public override string Description => @"Play with nine keys."; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs new file mode 100644 index 0000000000..cfa5ef88b8 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModMirror.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; +using System.Linq; + +namespace osu.Game.Rulesets.Mania.Mods +{ + public class ManiaModMirror : Mod, IApplicableToRulesetContainer + { + public override string Name => "Mirror"; + public override string ShortenedName => "MR"; + public override ModType Type => ModType.Special; + public override double ScoreMultiplier => 1; + public override bool Ranked => true; + + public void ApplyToRulesetContainer(RulesetContainer rulesetContainer) + { + var availableColumns = ((ManiaRulesetContainer)rulesetContainer).Beatmap.TotalColumns; + + rulesetContainer.Objects.OfType().ForEach(h => h.Column = availableColumns - 1 - h.Column); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs index a977eef5e3..a007224b74 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs @@ -7,6 +7,6 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModNightcore : ModNightcore { - public override double ScoreMultiplier => 1.0; + public override double ScoreMultiplier => 1; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs index a6cbad44d7..df0f9a5437 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModRandom.cs @@ -17,8 +17,8 @@ namespace osu.Game.Rulesets.Mania.Mods public override string Name => "Random"; public override string ShortenedName => "RD"; public override FontAwesome Icon => FontAwesome.fa_osu_dice; - public override string Description => @"Shuffle around the notes!"; - public override double ScoreMultiplier => 1; + public override string Description => @"Shuffle around the keys!"; + public override double ScoreMultiplier => 0; public void ApplyToRulesetContainer(RulesetContainer rulesetContainer) { diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs index 91c83a62f0..83d67c855e 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables RelativeSizeAxes = Axes.X; Height = 1; - Add(new Box + AddInternal(new Box { Name = "Bar line", Anchor = Anchor.BottomCentre, @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (isMajor) { - Add(new EquilateralTriangle + AddInternal(new EquilateralTriangle { Name = "Left triangle", Anchor = Anchor.BottomLeft, @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables Rotation = 90 }); - Add(new EquilateralTriangle + AddInternal(new EquilateralTriangle { Name = "Right triangle", Anchor = Anchor.BottomRight, diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 5a9ff592bc..c3d6a69a72 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -8,7 +8,6 @@ using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using OpenTK.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Judgements; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Scoring; @@ -24,7 +23,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables private readonly GlowPiece glowPiece; private readonly BodyPiece bodyPiece; - private readonly Container tickContainer; private readonly Container fullHeightContainer; /// @@ -40,9 +38,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public DrawableHoldNote(HoldNote hitObject, ManiaAction action) : base(hitObject, action) { + Container tickContainer; RelativeSizeAxes = Axes.X; - AddRange(new Drawable[] + InternalChildren = new Drawable[] { // The hit object itself cannot be used for various elements because the tail overshoots it // So a specialized container that is updated to contain the tail height is used @@ -57,7 +56,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, }, - tickContainer = new Container { RelativeSizeAxes = Axes.Both }, + tickContainer = new Container + { + RelativeSizeAxes = Axes.Both, + ChildrenEnumerable = HitObject.NestedHitObjects.OfType().Select(tick => new DrawableHoldNoteTick(tick) + { + HoldStartTime = () => holdStartTime + }) + }, head = new DrawableHeadNote(this, action) { Anchor = Anchor.TopCentre, @@ -68,18 +74,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre } - }); + }; - foreach (var tick in HitObject.NestedHitObjects.OfType()) - { - var drawableTick = new DrawableHoldNoteTick(tick) - { - HoldStartTime = () => holdStartTime - }; - - tickContainer.Add(drawableTick); - AddNested(drawableTick); - } + foreach (var tick in tickContainer) + AddNested(tick); AddNested(head); AddNested(tail); @@ -90,12 +88,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables get { return base.AccentColour; } set { - if (base.AccentColour == value) - return; base.AccentColour = value; - tickContainer.Children.ForEach(t => t.AccentColour = value); - glowPiece.AccentColour = value; bodyPiece.AccentColour = value; head.AccentColour = value; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs index f9c0b96d37..b50a5e897e 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables RelativeSizeAxes = Axes.X; Size = new Vector2(1); - Children = new[] + InternalChildren = new[] { glowContainer = new CircularContainer { diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index 0a1624b464..3aec8d25f9 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Graphics; -using OpenTK.Graphics; using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Mania.Objects.Drawables @@ -28,16 +27,5 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (action != null) Action = action.Value; } - - public override Color4 AccentColour - { - get { return base.AccentColour; } - set - { - if (base.AccentColour == value) - return; - base.AccentColour = value; - } - } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 8944978bdd..c171325fb2 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - Children = new Drawable[] + InternalChildren = new Drawable[] { laneGlowPiece = new LaneGlowPiece { @@ -48,13 +48,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables get { return base.AccentColour; } set { - if (base.AccentColour == value) - return; base.AccentColour = value; - - laneGlowPiece.AccentColour = value; - GlowPiece.AccentColour = value; - headPiece.AccentColour = value; + laneGlowPiece.AccentColour = AccentColour; + GlowPiece.AccentColour = AccentColour; + headPiece.AccentColour = AccentColour; } } diff --git a/osu.Game.Rulesets.Mania/Objects/ManiaHitObjectDifficulty.cs b/osu.Game.Rulesets.Mania/Objects/ManiaHitObjectDifficulty.cs new file mode 100644 index 0000000000..2b59279972 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/ManiaHitObjectDifficulty.cs @@ -0,0 +1,113 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Objects.Types; +using System; + +namespace osu.Game.Rulesets.Mania.Objects +{ + internal class ManiaHitObjectDifficulty + { + /// + /// Factor by how much individual / overall strain decays per second. + /// + /// + /// These values are results of tweaking a lot and taking into account general feedback. + /// + internal const double INDIVIDUAL_DECAY_BASE = 0.125; + internal const double OVERALL_DECAY_BASE = 0.30; + + internal ManiaHitObject BaseHitObject; + + private readonly int beatmapColumnCount; + + + private readonly double endTime; + private readonly double[] heldUntil; + + /// + /// Measures jacks or more generally: repeated presses of the same button + /// + private readonly double[] individualStrains; + + internal double IndividualStrain + { + get + { + return individualStrains[BaseHitObject.Column]; + } + + set + { + individualStrains[BaseHitObject.Column] = value; + } + } + + /// + /// Measures note density in a way + /// + internal double OverallStrain = 1; + + public ManiaHitObjectDifficulty(ManiaHitObject baseHitObject, int columnCount) + { + BaseHitObject = baseHitObject; + + endTime = (baseHitObject as IHasEndTime)?.EndTime ?? baseHitObject.StartTime; + + beatmapColumnCount = columnCount; + heldUntil = new double[beatmapColumnCount]; + individualStrains = new double[beatmapColumnCount]; + + for (int i = 0; i < beatmapColumnCount; ++i) + { + individualStrains[i] = 0; + heldUntil[i] = 0; + } + } + + internal void CalculateStrains(ManiaHitObjectDifficulty previousHitObject, double timeRate) + { + // TODO: Factor in holds + double timeElapsed = (BaseHitObject.StartTime - previousHitObject.BaseHitObject.StartTime) / timeRate; + double individualDecay = Math.Pow(INDIVIDUAL_DECAY_BASE, timeElapsed / 1000); + double overallDecay = Math.Pow(OVERALL_DECAY_BASE, timeElapsed / 1000); + + double holdFactor = 1.0; // Factor to all additional strains in case something else is held + double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly + + // Fill up the heldUntil array + for (int i = 0; i < beatmapColumnCount; ++i) + { + heldUntil[i] = previousHitObject.heldUntil[i]; + + // If there is at least one other overlapping end or note, then we get an addition, buuuuuut... + if (BaseHitObject.StartTime < heldUntil[i] && endTime > heldUntil[i]) + { + holdAddition = 1.0; + } + + // ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1 + if (endTime == heldUntil[i]) + { + holdAddition = 0; + } + + // We give a slight bonus to everything if something is held meanwhile + if (heldUntil[i] > endTime) + { + holdFactor = 1.25; + } + + // Decay individual strains + individualStrains[i] = previousHitObject.individualStrains[i] * individualDecay; + } + + heldUntil[BaseHitObject.Column] = endTime; + + // Increase individual strain in own column + IndividualStrain += 2.0 * holdFactor; + + OverallStrain = previousHitObject.OverallStrain * overallDecay + (1.0 + holdAddition) * holdFactor; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs index 6f6217f540..5a992bb970 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; -using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Replays; @@ -15,10 +15,31 @@ namespace osu.Game.Rulesets.Mania.Replays { public const double RELEASE_DELAY = 20; - public ManiaAutoGenerator(Beatmap beatmap) + public new ManiaBeatmap Beatmap => (ManiaBeatmap)base.Beatmap; + + private readonly ManiaAction[] columnActions; + + public ManiaAutoGenerator(ManiaBeatmap beatmap) : base(beatmap) { Replay = new Replay { User = new User { Username = @"Autoplay" } }; + + columnActions = new ManiaAction[Beatmap.TotalColumns]; + + var normalAction = ManiaAction.Key1; + var specialAction = ManiaAction.Special1; + int totalCounter = 0; + foreach (var stage in Beatmap.Stages) + { + for (int i = 0; i < stage.Columns; i++) + { + if (stage.IsSpecialColumn(i)) + columnActions[totalCounter] = specialAction++; + else + columnActions[totalCounter] = normalAction++; + totalCounter++; + } + } } protected Replay Replay; @@ -30,18 +51,18 @@ namespace osu.Game.Rulesets.Mania.Replays var pointGroups = generateActionPoints().GroupBy(a => a.Time).OrderBy(g => g.First().Time); - int activeColumns = 0; + var actions = new List(); foreach (var group in pointGroups) { foreach (var point in group) { if (point is HitPoint) - activeColumns |= 1 << point.Column; + actions.Add(columnActions[point.Column]); if (point is ReleasePoint) - activeColumns ^= 1 << point.Column; + actions.Remove(columnActions[point.Column]); } - Replay.Frames.Add(new ManiaReplayFrame(group.First().Time, activeColumns)); + Replay.Frames.Add(new ManiaReplayFrame(group.First().Time, actions.ToArray())); } return Replay; diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs b/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs index fd084f138f..3541561418 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs @@ -4,40 +4,19 @@ 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 + internal class ManiaFramedReplayInputHandler : FramedReplayInputHandler { - private readonly ManiaRulesetContainer container; - - public ManiaFramedReplayInputHandler(Replay replay, ManiaRulesetContainer container) + public ManiaFramedReplayInputHandler(Replay replay) : base(replay) { - this.container = container; } - private ManiaPlayfield playfield; - public override List GetPendingStates() - { - var actions = new List(); + protected override bool IsImportant(ManiaReplayFrame frame) => frame.Actions.Any(); - 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(playfield.Columns.ElementAt(counter).Action); - counter++; - activeColumns >>= 1; - } - - return new List { new ReplayState { PressedActions = actions } }; - } + public override List GetPendingStates() => new List { new ReplayState { PressedActions = CurrentFrame.Actions } }; } } diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs index e5c5ac9eeb..9990f89b99 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs @@ -1,17 +1,59 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Replays.Legacy; +using osu.Game.Rulesets.Replays.Types; namespace osu.Game.Rulesets.Mania.Replays { - public class ManiaReplayFrame : ReplayFrame + public class ManiaReplayFrame : ReplayFrame, IConvertibleReplayFrame { - public override bool IsImportant => MouseX > 0; + public List Actions = new List(); - public ManiaReplayFrame(double time, int activeColumns) - : base(time, activeColumns, null, ReplayButtonState.None) + public ManiaReplayFrame() { } + + public ManiaReplayFrame(double time, params ManiaAction[] actions) + : base(time) + { + Actions.AddRange(actions); + } + + public void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap) + { + // We don't need to fully convert, just create the converter + var converter = new ManiaBeatmapConverter(beatmap.BeatmapInfo.RulesetID == 3, beatmap); + + // NB: Via co-op mod, osu-stable can have two stages with floor(col/2) and ceil(col/2) columns. This will need special handling + // elsewhere in the game if we do choose to support the old co-op mod anyway. For now, assume that there is only one stage. + + var stage = new StageDefinition { Columns = converter.TargetColumns }; + + var normalAction = ManiaAction.Key1; + var specialAction = ManiaAction.Special1; + + int activeColumns = (int)(legacyFrame.MouseX ?? 0); + int counter = 0; + while (activeColumns > 0) + { + var isSpecial = stage.IsSpecialColumn(counter); + + if ((activeColumns & 1) > 0) + Actions.Add(isSpecial ? specialAction : normalAction); + + if (isSpecial) + specialAction++; + else + normalAction++; + + counter++; + activeColumns >>= 1; + } + } } } diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/basic-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/basic-expected-conversion.json new file mode 100644 index 0000000000..d593b2b052 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/basic-expected-conversion.json @@ -0,0 +1,103 @@ +{ + "Mappings": [{ + "StartTime": 500, + "Objects": [{ + "StartTime": 500, + "EndTime": 2500, + "Column": 0 + }, + { + "StartTime": 1500, + "EndTime": 2500, + "Column": 1 + } + ] + }, + { + "StartTime": 3000, + "Objects": [{ + "StartTime": 3000, + "EndTime": 4000, + "Column": 2 + }] + }, + { + "StartTime": 4500, + "Objects": [{ + "StartTime": 4500, + "EndTime": 5500, + "Column": 4 + }] + }, + { + "StartTime": 6000, + "Objects": [{ + "StartTime": 6000, + "EndTime": 6500, + "Column": 2 + }] + }, + { + "StartTime": 7000, + "Objects": [{ + "StartTime": 7000, + "EndTime": 8000, + "Column": 2 + }] + }, + { + "StartTime": 8500, + "Objects": [{ + "StartTime": 8500, + "EndTime": 11000, + "Column": 0 + }] + }, + { + "StartTime": 11500, + "Objects": [{ + "StartTime": 11500, + "EndTime": 12000, + "Column": 1 + }] + }, + { + "StartTime": 12500, + "Objects": [{ + "StartTime": 12500, + "EndTime": 16500, + "Column": 4 + }] + }, + { + "StartTime": 17000, + "Objects": [{ + "StartTime": 17000, + "EndTime": 18000, + "Column": 2 + }] + }, + { + "StartTime": 18500, + "Objects": [{ + "StartTime": 18500, + "EndTime": 19450, + "Column": 0 + }] + }, + { + "StartTime": 19875, + "Objects": [{ + "StartTime": 19875, + "EndTime": 23875, + "Column": 1 + }, + { + "StartTime": 19875, + "EndTime": 23875, + "Column": 0 + } + ] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/basic.osu b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/basic.osu new file mode 100644 index 0000000000..40b4409760 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/basic.osu @@ -0,0 +1,27 @@ +osu file format v14 + +[Difficulty] +HPDrainRate:6 +CircleSize:4 +OverallDifficulty:7 +ApproachRate:8.3 +SliderMultiplier:1.6 +SliderTickRate:1 + +[TimingPoints] +500,500,4,2,1,50,1,0 +13426,-100,4,3,1,45,0,0 +14884,-100,4,2,1,50,0,0 + +[HitObjects] +96,192,500,6,0,L|416:192,2,320 +256,192,3000,12,0,4000,0:0:0:0: +256,192,4500,12,0,5500,0:0:0:0: +256,192,6000,12,0,6500,0:0:0:0: +256,128,7000,6,0,L|352:128,4,80 +32,192,8500,6,0,B|32:384|256:384|256:192|256:192|256:0|512:0|512:192,1,800 +256,192,11500,12,0,12000,0:0:0:0: +512,320,12500,6,0,B|0:256|0:256|512:96|512:96|256:32,1,1280 +256,256,17000,6,0,L|160:256,4,80 +256,192,18500,12,0,19450,0:0:0:0: +216,231,19875,6,0,B|216:135|280:135|344:135|344:199|344:263|248:327|248:327|120:327|120:327|56:39|408:39|408:39|472:150|408:342,1,1280 diff --git a/osu.Game.Rulesets.Mania/Tests/ManiaBeatmapConversionTest.cs b/osu.Game.Rulesets.Mania/Tests/ManiaBeatmapConversionTest.cs new file mode 100644 index 0000000000..9d55ab643d --- /dev/null +++ b/osu.Game.Rulesets.Mania/Tests/ManiaBeatmapConversionTest.cs @@ -0,0 +1,60 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.MathUtils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public class ManiaBeatmapConversionTest : BeatmapConversionTest + { + protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; + + private bool isForCurrentRuleset; + + [NonParallelizable] + [TestCase("basic", false)] + public void Test(string name, bool isForCurrentRuleset) + { + this.isForCurrentRuleset = isForCurrentRuleset; + base.Test(name); + } + + protected override IEnumerable CreateConvertValue(HitObject hitObject) + { + yield return new ConvertValue + { + StartTime = hitObject.StartTime, + EndTime = (hitObject as IHasEndTime)?.EndTime ?? hitObject.StartTime, + Column = ((ManiaHitObject)hitObject).Column + }; + } + + protected override IBeatmapConverter CreateConverter(Beatmap beatmap) => new ManiaBeatmapConverter(isForCurrentRuleset, beatmap); + } + + public struct ConvertValue : IEquatable + { + /// + /// A sane value to account for osu!stable using ints everwhere. + /// + private const float conversion_lenience = 2; + + public double StartTime; + public double EndTime; + public int Column; + + public bool Equals(ConvertValue other) + => Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience) + && Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience) + && Column == other.Column; + } +} diff --git a/osu.Game.Rulesets.Mania/Tests/TestCaseAutoGeneration.cs b/osu.Game.Rulesets.Mania/Tests/TestCaseAutoGeneration.cs index 81c6c5c9d5..2453d8281a 100644 --- a/osu.Game.Rulesets.Mania/Tests/TestCaseAutoGeneration.cs +++ b/osu.Game.Rulesets.Mania/Tests/TestCaseAutoGeneration.cs @@ -1,15 +1,17 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Linq; using NUnit.Framework; -using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Replays; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Mania.Tests { - [Ignore("getting CI working")] + [TestFixture] public class TestCaseAutoGeneration : OsuTestCase { [Test] @@ -19,7 +21,7 @@ namespace osu.Game.Rulesets.Mania.Tests // | - | // | | - var beatmap = new Beatmap(); + var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 }); beatmap.HitObjects.Add(new Note { StartTime = 1000 }); var generated = new ManiaAutoGenerator(beatmap).Generate(); @@ -27,8 +29,8 @@ namespace osu.Game.Rulesets.Mania.Tests 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"); + Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Special1), "Special1 has not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Special1), "Special1 has not been released"); } [Test] @@ -40,7 +42,7 @@ namespace osu.Game.Rulesets.Mania.Tests // | * | // | | - var beatmap = new Beatmap(); + var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 }); beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 }); var generated = new ManiaAutoGenerator(beatmap).Generate(); @@ -48,8 +50,8 @@ namespace osu.Game.Rulesets.Mania.Tests 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"); + Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Special1), "Special1 has not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Special1), "Special1 has not been released"); } [Test] @@ -59,7 +61,7 @@ namespace osu.Game.Rulesets.Mania.Tests // | - | - | // | | | - var beatmap = new Beatmap(); + var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 }); beatmap.HitObjects.Add(new Note { StartTime = 1000 }); beatmap.HitObjects.Add(new Note { StartTime = 1000, Column = 1 }); @@ -68,8 +70,8 @@ namespace osu.Game.Rulesets.Mania.Tests 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"); + Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released"); } [Test] @@ -81,7 +83,7 @@ namespace osu.Game.Rulesets.Mania.Tests // | * | * | // | | | - var beatmap = new Beatmap(); + var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 }); beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 }); beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000, Column = 1 }); @@ -90,8 +92,8 @@ namespace osu.Game.Rulesets.Mania.Tests 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"); + Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released"); } [Test] @@ -102,7 +104,7 @@ namespace osu.Game.Rulesets.Mania.Tests // | - | | // | | | - var beatmap = new Beatmap(); + var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 }); beatmap.HitObjects.Add(new Note { StartTime = 1000 }); beatmap.HitObjects.Add(new Note { StartTime = 2000, Column = 1 }); @@ -113,10 +115,10 @@ namespace osu.Game.Rulesets.Mania.Tests 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"); + Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1), "Key1 has not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Key1), "Key1 has not been released"); + Assert.IsTrue(checkContains(generated.Frames[3], ManiaAction.Key2), "Key2 has not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[4], ManiaAction.Key2), "Key2 has not been released"); } [Test] @@ -129,7 +131,7 @@ namespace osu.Game.Rulesets.Mania.Tests // | * | | // | | | - var beatmap = new Beatmap(); + var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 }); beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 }); beatmap.HitObjects.Add(new HoldNote { StartTime = 2000, Duration = 2000, Column = 1 }); @@ -140,10 +142,11 @@ namespace osu.Game.Rulesets.Mania.Tests 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"); + Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1), "Key1 has not been pressed"); + Assert.IsTrue(checkContains(generated.Frames[2], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[3], ManiaAction.Key1), "Key1 has not been released"); + Assert.IsTrue(checkContains(generated.Frames[3], ManiaAction.Key2), "Key2 has been released"); + Assert.IsFalse(checkContains(generated.Frames[4], ManiaAction.Key2), "Key2 has not been released"); } [Test] @@ -155,7 +158,7 @@ namespace osu.Game.Rulesets.Mania.Tests // | * | | // | | | - var beatmap = new Beatmap(); + var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 }); beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 - ManiaAutoGenerator.RELEASE_DELAY }); beatmap.HitObjects.Add(new Note { StartTime = 3000, Column = 1 }); @@ -165,9 +168,12 @@ namespace osu.Game.Rulesets.Mania.Tests 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"); + Assert.IsTrue(checkContains(generated.Frames[1], ManiaAction.Key1), "Key1 has not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[2], ManiaAction.Key1), "Key1 has not been released"); + Assert.IsTrue(checkContains(generated.Frames[2], ManiaAction.Key2), "Key2 has not been pressed"); + Assert.IsFalse(checkContains(generated.Frames[3], ManiaAction.Key2), "Key2 has not been released"); } + + private bool checkContains(ReplayFrame frame, params ManiaAction[] actions) => actions.All(action => ((ManiaReplayFrame)frame).Actions.Contains(action)); } } diff --git a/osu.Game.Rulesets.Mania/Tests/TestCaseManiaHitObjects.cs b/osu.Game.Rulesets.Mania/Tests/TestCaseManiaHitObjects.cs index 0e6d40dc67..fe8749e830 100644 --- a/osu.Game.Rulesets.Mania/Tests/TestCaseManiaHitObjects.cs +++ b/osu.Game.Rulesets.Mania/Tests/TestCaseManiaHitObjects.cs @@ -13,7 +13,6 @@ using OpenTK.Graphics; namespace osu.Game.Rulesets.Mania.Tests { [TestFixture] - [Ignore("getting CI working")] public class TestCaseManiaHitObjects : OsuTestCase { public TestCaseManiaHitObjects() diff --git a/osu.Game.Rulesets.Mania/Tests/TestCaseManiaPlayfield.cs b/osu.Game.Rulesets.Mania/Tests/TestCaseManiaPlayfield.cs index 7d35ab2f4d..4793b1ce94 100644 --- a/osu.Game.Rulesets.Mania/Tests/TestCaseManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/Tests/TestCaseManiaPlayfield.cs @@ -8,7 +8,9 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Timing; +using osu.Game.Configuration; using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; @@ -19,7 +21,6 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Mania.Tests { [TestFixture] - [Ignore("getting CI working")] public class TestCaseManiaPlayfield : OsuTestCase { private const double start_time = 500; @@ -92,10 +93,17 @@ namespace osu.Game.Rulesets.Mania.Tests }); } + private DependencyContainer dependencies; + + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) + => dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); + [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) + private void load(RulesetStore rulesets, SettingsStore settings) { maniaRuleset = rulesets.GetRuleset(3); + + dependencies.Cache(new ManiaConfigManager(settings, maniaRuleset, 4)); } private ManiaPlayfield createPlayfield(int cols, bool inverted = false) diff --git a/osu.Game.Rulesets.Mania/Tests/TestCasePerformancePoints.cs b/osu.Game.Rulesets.Mania/Tests/TestCasePerformancePoints.cs index c76816db6a..3c776a2f4c 100644 --- a/osu.Game.Rulesets.Mania/Tests/TestCasePerformancePoints.cs +++ b/osu.Game.Rulesets.Mania/Tests/TestCasePerformancePoints.cs @@ -5,7 +5,7 @@ using NUnit.Framework; namespace osu.Game.Rulesets.Mania.Tests { - [Ignore("getting CI working")] + [TestFixture] public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints { public TestCasePerformancePoints() diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index 8a03f5a785..f50f077c76 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -1,17 +1,25 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Mania.UI { internal class DrawableManiaJudgement : DrawableJudgement { - public DrawableManiaJudgement(Judgement judgement) - : base(judgement) + public DrawableManiaJudgement(Judgement judgement, DrawableHitObject judgedObject) + : base(judgement, judgedObject) { - JudgementText.TextSize = 25; + } + + [BackgroundDependencyLoader] + private void load() + { + if (JudgementText != null) + JudgementText.TextSize = 25; } protected override void LoadComplete() diff --git a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs index 436d5c1ea6..3ecfee1e8c 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs @@ -11,6 +11,7 @@ using osu.Framework.MathUtils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; +using osu.Game.Input.Handlers; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Mods; @@ -101,9 +102,9 @@ namespace osu.Game.Rulesets.Mania.UI return null; } - protected override Vector2 GetPlayfieldAspectAdjust() => new Vector2(1, 0.8f); + protected override Vector2 PlayfieldArea => new Vector2(1, 0.8f); - protected override FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay, this); + protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay); protected override IRulesetConfigManager CreateConfig(Ruleset ruleset, SettingsStore settings) => new ManiaConfigManager(settings, Ruleset.RulesetInfo, Variant); } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs index ebd73d7dca..d4ca704829 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs @@ -15,6 +15,7 @@ using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using OpenTK; using OpenTK.Graphics; @@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.UI private readonly Container content; public Container Judgements => judgements; - private readonly Container judgements; + private readonly JudgementContainer judgements; private readonly Container topLevelContainer; @@ -48,13 +49,11 @@ namespace osu.Game.Rulesets.Mania.UI private Color4 specialColumnColour; private readonly int firstColumnIndex; - private readonly StageDefinition definition; public ManiaStage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction) : base(ScrollingDirection.Up) { this.firstColumnIndex = firstColumnIndex; - this.definition = definition; Name = "Stage"; @@ -116,7 +115,7 @@ namespace osu.Game.Rulesets.Mania.UI Padding = new MarginPadding { Top = HIT_TARGET_POSITION } } }, - judgements = new Container + judgements = new JudgementContainer { Anchor = Anchor.TopCentre, Origin = Anchor.Centre, @@ -131,7 +130,7 @@ namespace osu.Game.Rulesets.Mania.UI for (int i = 0; i < definition.Columns; i++) { - var isSpecial = isSpecialColumn(i); + var isSpecial = definition.IsSpecialColumn(i); var column = new Column { IsSpecial = isSpecial, @@ -160,13 +159,6 @@ namespace osu.Game.Rulesets.Mania.UI AddNested(c); } - /// - /// Whether the column index is a special column for this playfield. - /// - /// The 0-based column index. - /// Whether the column is a special column. - private bool isSpecialColumn(int column) => definition.Columns % 2 == 1 && column == definition.Columns / 2; - public override void Add(DrawableHitObject h) { var maniaObject = (ManiaHitObject)h.HitObject; @@ -180,7 +172,7 @@ namespace osu.Game.Rulesets.Mania.UI internal void OnJudgement(DrawableHitObject judgedObject, Judgement judgement) { judgements.Clear(); - judgements.Add(new DrawableManiaJudgement(judgement) + judgements.Add(new DrawableManiaJudgement(judgement, judgedObject) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs index 3dad5b508c..42b22a71ec 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapProcessor.cs @@ -13,24 +13,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps public override void PostProcess(Beatmap beatmap) { applyStacking(beatmap); - - if (beatmap.ComboColors.Count == 0) - return; - - int comboIndex = 0; - int colourIndex = 0; - - foreach (var obj in beatmap.HitObjects) - { - if (obj.NewCombo) - { - comboIndex = 0; - colourIndex = (colourIndex + 1) % beatmap.ComboColors.Count; - } - - obj.IndexInCurrentCombo = comboIndex++; - obj.ComboColour = beatmap.ComboColors[colourIndex]; - } + base.PostProcess(beatmap); } private void applyStacking(Beatmap beatmap) diff --git a/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/HitCircleMask.cs b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/HitCircleMask.cs new file mode 100644 index 0000000000..b48dd73bb5 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/HitCircleMask.cs @@ -0,0 +1,35 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Allocation; +using osu.Game.Graphics; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; + +namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays +{ + public class HitCircleMask : HitObjectMask + { + public HitCircleMask(DrawableHitCircle hitCircle) + : base(hitCircle) + { + Origin = Anchor.Centre; + + Position = hitCircle.Position; + Size = hitCircle.Size; + Scale = hitCircle.Scale; + + AddInternal(new RingPiece()); + + hitCircle.HitObject.PositionChanged += _ => Position = hitCircle.Position; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Colour = colours.Yellow; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderCircleMask.cs b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderCircleMask.cs new file mode 100644 index 0000000000..586b516a11 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderCircleMask.cs @@ -0,0 +1,56 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using OpenTK; + +namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays +{ + public class SliderCircleMask : HitObjectMask + { + public SliderCircleMask(DrawableHitCircle sliderHead, DrawableSlider slider) + : this(sliderHead, Vector2.Zero, slider) + { + } + + public SliderCircleMask(DrawableSliderTail sliderTail, DrawableSlider slider) + : this(sliderTail, ((Slider)slider.HitObject).Curve.PositionAt(1), slider) + { + } + + private readonly DrawableOsuHitObject hitObject; + + private SliderCircleMask(DrawableOsuHitObject hitObject, Vector2 position, DrawableSlider slider) + : base(hitObject) + { + this.hitObject = hitObject; + + Origin = Anchor.Centre; + + Position = position; + Size = slider.HeadCircle.Size; + Scale = slider.HeadCircle.Scale; + + AddInternal(new RingPiece()); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Colour = colours.Yellow; + } + + protected override void Update() + { + base.Update(); + + RelativeAnchorPosition = hitObject.RelativeAnchorPosition; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderMask.cs b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderMask.cs new file mode 100644 index 0000000000..53f02617cd --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/Layers/Selection/Overlays/SliderMask.cs @@ -0,0 +1,63 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays +{ + public class SliderMask : HitObjectMask + { + private readonly SliderBody body; + private readonly DrawableSlider slider; + + public SliderMask(DrawableSlider slider) + : base(slider) + { + this.slider = slider; + + Position = slider.Position; + + var sliderObject = (Slider)slider.HitObject; + + InternalChildren = new Drawable[] + { + body = new SliderBody(sliderObject) + { + AccentColour = Color4.Transparent, + PathWidth = sliderObject.Scale * 64 + }, + new SliderCircleMask(slider.HeadCircle, slider), + new SliderCircleMask(slider.TailCircle, slider), + }; + + sliderObject.PositionChanged += _ => Position = slider.Position; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + body.BorderColour = colours.Yellow; + } + + protected override void Update() + { + base.Update(); + + Size = slider.Size; + OriginPosition = slider.OriginPosition; + + // Need to cause one update + body.UpdateProgress(0); + } + + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => body.ReceiveMouseInputAt(screenSpacePos); + } +} diff --git a/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs index 56efc25fa5..a8d895bc1d 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuEditRulesetContainer.cs @@ -5,6 +5,7 @@ using osu.Framework.Graphics.Cursor; using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; +using OpenTK; namespace osu.Game.Rulesets.Osu.Edit { @@ -17,6 +18,8 @@ namespace osu.Game.Rulesets.Osu.Edit protected override Playfield CreatePlayfield() => new OsuEditPlayfield(); + protected override Vector2 PlayfieldArea => Vector2.One; + protected override CursorContainer CreateCursor() => null; } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 6652a5fde2..026c85d909 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -2,10 +2,15 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.Edit @@ -25,5 +30,20 @@ namespace osu.Game.Rulesets.Osu.Edit new HitObjectCompositionTool(), new HitObjectCompositionTool() }; + + protected override ScalableContainer CreateLayerContainer() => new ScalableContainer(OsuPlayfield.BASE_SIZE.X) { RelativeSizeAxes = Axes.Both }; + + public override HitObjectMask CreateMaskFor(DrawableHitObject hitObject) + { + switch (hitObject) + { + case DrawableHitCircle circle: + return new HitCircleMask(circle); + case DrawableSlider slider: + return new SliderMask(slider); + } + + return base.CreateMaskFor(hitObject); + } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs index eb90338e2f..987bb28932 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDaycore.cs @@ -7,6 +7,6 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModDaycore : ModDaycore { - public override double ScoreMultiplier => 0.5; + public override double ScoreMultiplier => 0.3; } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs b/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs index 80c83bf5d8..d842b607c6 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs @@ -7,5 +7,6 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModEasy : ModEasy { + public override string Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!"; } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs index 7d009b0344..1b9291bcf3 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHalfTime.cs @@ -7,6 +7,6 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModHalfTime : ModHalfTime { - public override double ScoreMultiplier => 0.5; + public override double ScoreMultiplier => 0.3; } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs index dfbe9ad021..74c3585d3d 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHardRock.cs @@ -2,6 +2,8 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using System.Linq; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; @@ -12,7 +14,6 @@ namespace osu.Game.Rulesets.Osu.Mods public class OsuModHardRock : ModHardRock, IApplicableToHitObject { public override double ScoreMultiplier => 1.06; - public override bool Ranked => true; public void ApplyToHitObject(OsuHitObject hitObject) { @@ -22,8 +23,14 @@ namespace osu.Game.Rulesets.Osu.Mods if (slider == null) return; + slider.HeadCircle.Position = new Vector2(slider.HeadCircle.Position.X, OsuPlayfield.BASE_SIZE.Y - slider.HeadCircle.Position.Y); + slider.TailCircle.Position = new Vector2(slider.TailCircle.Position.X, OsuPlayfield.BASE_SIZE.Y - slider.TailCircle.Position.Y); + + slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); + slider.NestedHitObjects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y)); + var newControlPoints = new List(); - slider.ControlPoints.ForEach(c => newControlPoints.Add(new Vector2(c.X, OsuPlayfield.BASE_SIZE.Y - c.Y))); + slider.ControlPoints.ForEach(c => newControlPoints.Add(new Vector2(c.X, -c.Y))); slider.ControlPoints = newControlPoints; slider.Curve?.Calculate(); // Recalculate the slider curve diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index b4dd08eadb..1117b5bbd5 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -1,19 +1,21 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; namespace osu.Game.Rulesets.Osu.Mods { public class OsuModHidden : ModHidden, IApplicableToDrawableHitObjects { - public override string Description => @"Play with no approach circles and fading notes for a slight score advantage."; + public override string Description => @"Play with no approach circles and fading circles/sliders."; public override double ScoreMultiplier => 1.06; private const double fade_in_duration_multiplier = 0.4; @@ -24,7 +26,10 @@ namespace osu.Game.Rulesets.Osu.Mods foreach (var d in drawables.OfType()) { d.ApplyCustomUpdateState += ApplyHiddenState; + d.HitObject.TimeFadein = d.HitObject.TimePreempt * fade_in_duration_multiplier; + foreach (var h in d.HitObject.NestedHitObjects.OfType()) + h.TimeFadein = h.TimePreempt * fade_in_duration_multiplier; } } @@ -33,30 +38,37 @@ namespace osu.Game.Rulesets.Osu.Mods if (!(drawable is DrawableOsuHitObject d)) return; - var fadeOutStartTime = d.HitObject.StartTime - d.HitObject.TimePreempt + d.HitObject.TimeFadein; - var fadeOutDuration = d.HitObject.TimePreempt * fade_out_duration_multiplier; + var h = d.HitObject; + + var fadeOutStartTime = h.StartTime - h.TimePreempt + h.TimeFadein; + var fadeOutDuration = h.TimePreempt * fade_out_duration_multiplier; // new duration from completed fade in to end (before fading out) - var longFadeDuration = ((d.HitObject as IHasEndTime)?.EndTime ?? d.HitObject.StartTime) - fadeOutStartTime; + var longFadeDuration = ((h as IHasEndTime)?.EndTime ?? h.StartTime) - fadeOutStartTime; switch (drawable) { case DrawableHitCircle circle: // we don't want to see the approach circle - circle.ApproachCircle.Hide(); + using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true)) + circle.ApproachCircle.Hide(); // fade out immediately after fade in. using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true)) - { circle.FadeOut(fadeOutDuration); - } break; case DrawableSlider slider: using (slider.BeginAbsoluteSequence(fadeOutStartTime, true)) - { slider.Body.FadeOut(longFadeDuration, Easing.Out); - } + + break; + case DrawableSliderTick sliderTick: + // slider ticks fade out over up to one second + var tickFadeOutDuration = Math.Min(sliderTick.HitObject.TimePreempt - DrawableSliderTick.ANIM_DURATION, 1000); + + using (sliderTick.BeginAbsoluteSequence(sliderTick.HitObject.StartTime - tickFadeOutDuration, true)) + sliderTick.FadeOut(tickFadeOutDuration); break; case DrawableSpinner spinner: @@ -66,9 +78,7 @@ namespace osu.Game.Rulesets.Osu.Mods spinner.Background.Hide(); using (spinner.BeginAbsoluteSequence(fadeOutStartTime + longFadeDuration, true)) - { spinner.FadeOut(fadeOutDuration); - } break; } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs index 057916c04b..c9def8c8cf 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModRelax : ModRelax { - public override string Description => "You don't need to click.\nGive your clicking/tapping finger a break from the heat of things."; + public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things."; public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot) }).ToArray(); } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 18b212f781..401e56a3c8 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Name => "Spun Out"; public override string ShortenedName => "SO"; public override FontAwesome Icon => FontAwesome.fa_osu_mod_spunout; - public override string Description => @"Spinners will be automatically completed"; + public override string Description => @"Spinners will be automatically completed."; public override double ScoreMultiplier => 0.9; public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(OsuModAutopilot) }; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index b2b5130be3..613fbc4e32 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Name => "Target"; public override string ShortenedName => "TP"; public override FontAwesome Icon => FontAwesome.fa_osu_mod_target; - public override string Description => @""; + public override string Description => @"Practice keeping up with the beat of the song."; public override double ScoreMultiplier => 1; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 959c87bbba..9066a9ef92 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -8,6 +8,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using OpenTK; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; +using OpenTK.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -21,22 +22,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly NumberPiece number; private readonly GlowPiece glow; - public DrawableHitCircle(HitCircle h) : base(h) + public DrawableHitCircle(HitCircle h) + : base(h) { Origin = Anchor.Centre; Position = HitObject.StackedPosition; Scale = new Vector2(h.Scale); - Children = new Drawable[] + InternalChildren = new Drawable[] { - glow = new GlowPiece - { - Colour = AccentColour - }, + glow = new GlowPiece(), circle = new CirclePiece { - Colour = AccentColour, Hit = () => { if (AllJudged) @@ -52,20 +50,31 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables }, ring = new RingPiece(), flash = new FlashPiece(), - explode = new ExplodePiece - { - Colour = AccentColour, - }, + explode = new ExplodePiece(), ApproachCircle = new ApproachCircle { Alpha = 0, Scale = new Vector2(4), - Colour = AccentColour, } }; //may not be so correct Size = circle.DrawSize; + + HitObject.PositionChanged += _ => Position = HitObject.StackedPosition; + } + + public override Color4 AccentColour + { + get { return base.AccentColour; } + set + { + base.AccentColour = value; + explode.Colour = AccentColour; + glow.Colour = AccentColour; + circle.Colour = AccentColour; + ApproachCircle.Colour = AccentColour; + } } protected override void CheckForJudgements(bool userTriggered, double timeOffset) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index c8e42fa44f..d4d89c2aa3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -5,6 +5,9 @@ using System.ComponentModel; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Graphics; using System.Linq; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Skinning; +using OpenTK.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -15,7 +18,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected DrawableOsuHitObject(OsuHitObject hitObject) : base(hitObject) { - AccentColour = HitObject.ComboColour; Alpha = 0; } @@ -35,6 +37,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + + if (HitObject is IHasComboInformation combo) + AccentColour = skin.GetValue(s => s.ComboColours.Count > 0 ? s.ComboColours[combo.ComboIndex % s.ComboColours.Count] : (Color4?)null) ?? Color4.White; + } + protected virtual void UpdatePreemptState() => this.FadeIn(HitObject.TimeFadein); protected virtual void UpdateCurrentState(ArmedState state) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 716f4b629b..1468c82b57 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -2,24 +2,24 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Graphics; -using osu.Game.Rulesets.Osu.Judgements; using OpenTK; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableOsuJudgement : DrawableJudgement { - public DrawableOsuJudgement(OsuJudgement judgement) - : base(judgement) + public DrawableOsuJudgement(Judgement judgement, DrawableHitObject judgedObject) + : base(judgement, judgedObject) { } protected override void LoadComplete() { if (Judgement.Result != HitResult.Miss) - JudgementText.TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint); + JudgementText?.TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint); base.LoadComplete(); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs index 79a4714e33..94179f30d3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableRepeatPoint.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Blending = BlendingMode.Additive; Origin = Anchor.Centre; - Children = new Drawable[] + InternalChildren = new Drawable[] { new SpriteIcon { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 5795bb8405..3872821b96 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -7,11 +7,13 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Osu.Judgements; using osu.Framework.Graphics.Primitives; -using osu.Game.Rulesets.Objects.Types; +using osu.Game.Configuration; using osu.Game.Rulesets.Scoring; +using OpenTK.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -21,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly List components = new List(); public readonly DrawableHitCircle HeadCircle; + public readonly DrawableSliderTail TailCircle; + public readonly SliderBody Body; public readonly SliderBall Ball; @@ -29,29 +33,28 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { slider = s; - DrawableSliderTail tail; + Position = s.StackedPosition; + Container ticks; Container repeatPoints; - Children = new Drawable[] + InternalChildren = new Drawable[] { Body = new SliderBody(s) { - AccentColour = AccentColour, - Position = s.StackedPosition, PathWidth = s.Scale * 64, }, - ticks = new Container(), - repeatPoints = new Container(), + ticks = new Container { RelativeSizeAxes = Axes.Both }, + repeatPoints = new Container { RelativeSizeAxes = Axes.Both }, Ball = new SliderBall(s) { + BypassAutoSizeAxes = Axes.Both, Scale = new Vector2(s.Scale), - AccentColour = AccentColour, AlwaysPresent = true, Alpha = 0 }, - HeadCircle = new DrawableHitCircle(s.HeadCircle), - tail = new DrawableSliderTail(s.TailCircle) + HeadCircle = new DrawableSliderHead(s, s.HeadCircle), + TailCircle = new DrawableSliderTail(s, s.TailCircle) }; components.Add(Body); @@ -59,15 +62,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables AddNested(HeadCircle); - AddNested(tail); - components.Add(tail); + AddNested(TailCircle); + components.Add(TailCircle); foreach (var tick in s.NestedHitObjects.OfType()) { - var drawableTick = new DrawableSliderTick(tick) - { - Position = tick.Position - }; + var drawableTick = new DrawableSliderTick(tick) { Position = tick.Position - s.Position }; ticks.Add(drawableTick); components.Add(drawableTick); @@ -76,18 +76,34 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables foreach (var repeatPoint in s.NestedHitObjects.OfType()) { - var drawableRepeatPoint = new DrawableRepeatPoint(repeatPoint, this) - { - Position = repeatPoint.Position - }; + var drawableRepeatPoint = new DrawableRepeatPoint(repeatPoint, this) { Position = repeatPoint.Position - s.Position }; repeatPoints.Add(drawableRepeatPoint); components.Add(drawableRepeatPoint); AddNested(drawableRepeatPoint); } + + HitObject.PositionChanged += _ => Position = HitObject.StackedPosition; + } + + public override Color4 AccentColour + { + get { return base.AccentColour; } + set + { + base.AccentColour = value; + Body.AccentColour = AccentColour; + Ball.AccentColour = AccentColour; + } + } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + config.BindWith(OsuSetting.SnakingInSliders, Body.SnakingIn); + config.BindWith(OsuSetting.SnakingOutSliders, Body.SnakingOut); } - private int currentSpan; public bool Tracking; protected override void Update() @@ -96,21 +112,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Tracking = Ball.Tracking; - double progress = MathHelper.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1); + double completionProgress = MathHelper.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1); - int span = slider.SpanAt(progress); - progress = slider.ProgressAt(progress); - - if (span > currentSpan) - currentSpan = span; - - //todo: we probably want to reconsider this before adding scoring, but it looks and feels nice. - if (!HeadCircle.IsHit) - HeadCircle.Position = slider.Curve.PositionAt(progress); - - foreach (var c in components.OfType()) c.UpdateProgress(progress, span); + foreach (var c in components.OfType()) c.UpdateProgress(completionProgress); foreach (var c in components.OfType()) c.UpdateSnakingPosition(slider.Curve.PositionAt(Body.SnakedStart ?? 0), slider.Curve.PositionAt(Body.SnakedEnd ?? 0)); foreach (var t in components.OfType()) t.Tracking = Ball.Tracking; + + Size = Body.Size; + OriginPosition = Body.PathOffset; + + if (DrawSize != Vector2.Zero) + { + var childAnchorPosition = Vector2.Divide(OriginPosition, DrawSize); + foreach (var obj in NestedHitObjects) + obj.RelativeAnchorPosition = childAnchorPosition; + Ball.RelativeAnchorPosition = childAnchorPosition; + } } protected override void CheckForJudgements(bool userTriggered, double timeOffset) @@ -153,11 +170,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables this.FadeOut(fade_out_time, Easing.OutQuint).Expire(); } + + Expire(true); } public Drawable ProxiedLayer => HeadCircle.ApproachCircle; - public override Vector2 SelectionPoint => ToScreenSpace(Body.Position); + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => Body.ReceiveMouseInputAt(screenSpacePos); + + public override Vector2 SelectionPoint => ToScreenSpace(OriginPosition); public override Quad SelectionQuad => Body.PathDrawQuad; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs new file mode 100644 index 0000000000..cf36d5fc14 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -0,0 +1,32 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Objects.Types; +using OpenTK; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables +{ + public class DrawableSliderHead : DrawableHitCircle + { + private readonly Slider slider; + + public DrawableSliderHead(Slider slider, HitCircle h) + : base(h) + { + this.slider = slider; + + Position = HitObject.Position - slider.Position; + } + + protected override void Update() + { + base.Update(); + + double completionProgress = MathHelper.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1); + + //todo: we probably want to reconsider this before adding scoring, but it looks and feels nice. + if (!IsHit) + Position = slider.CurvePositionAt(completionProgress); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 8835fc2b29..b277e7df7a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -16,11 +16,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public bool Tracking { get; set; } - public DrawableSliderTail(HitCircle hitCircle) + public DrawableSliderTail(Slider slider, HitCircle hitCircle) : base(hitCircle) { - AlwaysPresent = true; + Origin = Anchor.Centre; + RelativeSizeAxes = Axes.Both; + FillMode = FillMode.Fit; + + AlwaysPresent = true; + + Position = HitObject.Position - slider.Position; } protected override void CheckForJudgements(bool userTriggered, double timeOffset) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index 41d73a745a..22bf63814c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSliderTick : DrawableOsuHitObject, IRequireTracking { - private const double anim_duration = 150; + public const double ANIM_DURATION = 150; public bool Tracking { get; set; } @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables BorderThickness = 2; BorderColour = Color4.White; - Children = new Drawable[] + InternalChildren = new Drawable[] { new Box { @@ -50,10 +50,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void UpdatePreemptState() { - this.Animate( - d => d.FadeIn(anim_duration), - d => d.ScaleTo(0.5f).ScaleTo(1f, anim_duration * 4, Easing.OutElasticHalf) - ); + this.FadeOut().FadeIn(ANIM_DURATION); + this.ScaleTo(0.5f).ScaleTo(1f, ANIM_DURATION * 4, Easing.OutElasticHalf); } protected override void UpdateCurrentState(ArmedState state) @@ -64,12 +62,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables this.Delay(HitObject.TimePreempt).FadeOut(); break; case ArmedState.Miss: - this.FadeOut(anim_duration) - .FadeColour(Color4.Red, anim_duration / 2); + this.FadeOut(ANIM_DURATION); + this.FadeColour(Color4.Red, ANIM_DURATION / 2); break; case ArmedState.Hit: - this.FadeOut(anim_duration, Easing.OutQuint) - .ScaleTo(Scale * 1.5f, anim_duration, Easing.Out); + this.FadeOut(ANIM_DURATION, Easing.OutQuint); + this.ScaleTo(Scale * 1.5f, ANIM_DURATION, Easing.Out); break; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 722ab4c6d5..2705c213d9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Spinner = s; - Children = new Drawable[] + InternalChildren = new Drawable[] { circleContainer = new Container { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ApproachCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ApproachCircle.cs index 61e9027157..51f8b7026a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ApproachCircle.cs @@ -6,30 +6,24 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public class ApproachCircle : Container { - private readonly Sprite approachCircle; - public ApproachCircle() { Anchor = Anchor.Centre; Origin = Anchor.Centre; - AutoSizeAxes = Axes.Both; - - Children = new Drawable[] - { - approachCircle = new Sprite() - }; + RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] private void load(TextureStore textures) { - approachCircle.Texture = textures.Get(@"Play/osu/approachcircle"); + Child = new SkinnableDrawable("Play/osu/approachcircle", name => new Sprite { Texture = textures.Get(name) }); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs index 286df14056..e7b6598cf2 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs @@ -2,20 +2,16 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; using osu.Framework.Input.Bindings; +using osu.Game.Skinning; using OpenTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public class CirclePiece : Container, IKeyBindingHandler { - private readonly Sprite disc; - public Func Hit; public CirclePiece() @@ -27,26 +23,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Anchor = Anchor.Centre; Origin = Anchor.Centre; - Children = new Drawable[] - { - disc = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }, - new TrianglesPiece - { - RelativeSizeAxes = Axes.Both, - Blending = BlendingMode.Additive, - Alpha = 0.5f, - } - }; - } - - [BackgroundDependencyLoader] - private void load(TextureStore textures) - { - disc.Texture = textures.Get(@"Play/osu/disc"); + InternalChild = new SkinnableDrawable("Play/osu/hitcircle", _ => new DefaultCirclePiece()); } public bool OnPressed(OsuAction action) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultCirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultCirclePiece.cs new file mode 100644 index 0000000000..61f73b6d66 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultCirclePiece.cs @@ -0,0 +1,35 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +{ + public class DefaultCirclePiece : Container + { + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + RelativeSizeAxes = Axes.Both; + Children = new Drawable[] + { + new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = textures.Get(@"Play/osu/disc"), + }, + new TrianglesPiece + { + RelativeSizeAxes = Axes.Both, + Blending = BlendingMode.Additive, + Alpha = 0.5f, + } + }; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs index 9be951e29c..28552e6c36 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Skinning; using OpenTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces @@ -19,15 +20,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Blending = BlendingMode.Additive; Alpha = 0; - Children = new Drawable[] + Child = new SkinnableDrawable("Play/osu/hitcircle-explode", _ => new TrianglesPiece { - new TrianglesPiece - { - Blending = BlendingMode.Additive, - RelativeSizeAxes = Axes.Both, - Alpha = 0.2f, - } - }; + Blending = BlendingMode.Additive, + RelativeSizeAxes = Axes.Both, + Alpha = 0.2f, + }, s => s.GetTexture("Play/osu/hitcircle") == null); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/FlashPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/FlashPiece.cs index 56faa335b1..50dc473750 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/FlashPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/FlashPiece.cs @@ -5,6 +5,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using OpenTK; using osu.Framework.Graphics.Shapes; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { @@ -14,22 +15,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { Size = new Vector2(128); - Masking = true; - CornerRadius = Size.X / 2; - Anchor = Anchor.Centre; Origin = Anchor.Centre; Blending = BlendingMode.Additive; Alpha = 0; - Children = new Drawable[] + Child = new SkinnableDrawable("Play/osu/hitcircle-flash", name => new CircularContainer { - new Box + Masking = true, + RelativeSizeAxes = Axes.Both, + Child = new Box { RelativeSizeAxes = Axes.Both } - }; + }, s => s.GetTexture("Play/osu/hitcircle") == null); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/GlowPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/GlowPiece.cs index 9a1208f998..211e138b65 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/GlowPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/GlowPiece.cs @@ -6,34 +6,30 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public class GlowPiece : Container { - private readonly Sprite layer; - public GlowPiece() { Anchor = Anchor.Centre; Origin = Anchor.Centre; - - Children = new[] - { - layer = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Blending = BlendingMode.Additive, - Alpha = 0.5f - } - }; + RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] private void load(TextureStore textures) { - layer.Texture = textures.Get(@"Play/osu/ring-glow"); + Child = new SkinnableDrawable("Play/osu/ring-glow", name => new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = textures.Get(name), + Blending = BlendingMode.Additive, + Alpha = 0.5f + }, s => s.GetTexture("Play/osu/hitcircle") == null); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs index afbf00f320..0c1fd4c364 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.Sprites; using OpenTK.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { @@ -28,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Children = new Drawable[] { - new CircularContainer + new SkinnableDrawable("Play/osu/number-glow", name => new CircularContainer { Masking = true, Origin = Anchor.Centre, @@ -38,11 +39,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Radius = 60, Colour = Color4.White.Opacity(0.5f), }, - Children = new[] - { - new Box() - } - }, + Child = new Box() + }, s => s.GetTexture("Play/osu/hitcircle") == null), number = new OsuSpriteText { Text = @"1", diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs index 2347927f2e..12cc0dc5d9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics.Containers; using OpenTK; using OpenTK.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { @@ -15,24 +16,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { Size = new Vector2(128); - Masking = true; - CornerRadius = Size.X / 2; - Anchor = Anchor.Centre; Origin = Anchor.Centre; - BorderThickness = 10; - BorderColour = Color4.White; - - Children = new Drawable[] + InternalChild = new SkinnableDrawable("Play/osu/hitcircleoverlay", _ => new Container { - new Box + Masking = true, + CornerRadius = Size.X / 2, + BorderThickness = 10, + BorderColour = Color4.White, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - AlwaysPresent = true, - Alpha = 0, - RelativeSizeAxes = Axes.Both + new Box + { + AlwaysPresent = true, + Alpha = 0, + RelativeSizeAxes = Axes.Both + } } - }; + }); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index 2fda299389..1921c51889 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input; +using osu.Game.Rulesets.Objects.Types; using OpenTK; using OpenTK.Graphics; @@ -139,9 +140,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } } - public void UpdateProgress(double progress, int span) + public void UpdateProgress(double completionProgress) { - Position = slider.Curve.PositionAt(progress); + Position = slider.CurvePositionAt(completionProgress); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs index 901d1c568d..c59c22c771 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Lines; using osu.Framework.Graphics.Textures; -using osu.Game.Configuration; using OpenTK; using OpenTK.Graphics.ES30; using OpenTK.Graphics; @@ -30,10 +29,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces set { path.PathWidth = value; } } + /// + /// Offset in absolute coordinates from the start of the curve. + /// + public Vector2 PathOffset { get; private set; } + + public readonly List CurrentCurve = new List(); + + public readonly Bindable SnakingIn = new Bindable(); + public readonly Bindable SnakingOut = new Bindable(); + public double? SnakedStart { get; private set; } public double? SnakedEnd { get; private set; } - private Color4 accentColour; + private Color4 accentColour = Color4.White; /// /// Used to colour the path. /// @@ -46,8 +55,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces return; accentColour = value; - if (LoadState == LoadState.Ready) - Schedule(reloadTexture); + if (LoadState >= LoadState.Ready) + reloadTexture(); + } + } + + private Color4 borderColour = Color4.White; + /// + /// Used to colour the path border. + /// + public new Color4 BorderColour + { + get { return borderColour; } + set + { + if (borderColour == value) + return; + borderColour = value; + + if (LoadState >= LoadState.Ready) + reloadTexture(); } } @@ -55,6 +82,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private int textureWidth => (int)PathWidth * 2; + private Vector2 topLeftOffset; + private readonly Slider slider; public SliderBody(Slider s) { @@ -64,6 +93,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { container = new BufferedContainer { + RelativeSizeAxes = Axes.Both, CacheDrawnFrameBuffer = true, Children = new Drawable[] { @@ -78,6 +108,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces container.Attach(RenderbufferInternalFormat.DepthComponent16); } + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => path.ReceiveMouseInputAt(screenSpacePos); + public void SetRange(double p0, double p1) { if (p0 > p1) @@ -85,26 +117,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces if (updateSnaking(p0, p1)) { - // Autosizing does not give us the desired behaviour here. - // We want the container to have the same size as the slider, - // and to be positioned such that the slider head is at (0,0). - container.Size = path.Size; - container.Position = -path.PositionInBoundingBox(slider.Curve.PositionAt(0) - CurrentCurve[0]); + // The path is generated such that its size encloses it. This change of size causes the path + // to move around while snaking, so we need to offset it to make sure it maintains the + // same position as when it is fully snaked. + var newTopLeftOffset = path.PositionInBoundingBox(Vector2.Zero); + path.Position = topLeftOffset - newTopLeftOffset; container.ForceRedraw(); } } - private Bindable snakingIn; - private Bindable snakingOut; - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load() { - snakingIn = config.GetBindable(OsuSetting.SnakingInSliders); - snakingOut = config.GetBindable(OsuSetting.SnakingOutSliders); - reloadTexture(); + computeSize(); } private void reloadTexture() @@ -128,10 +155,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces if (progress <= border_portion) { - bytes[i * 4] = 255; - bytes[i * 4 + 1] = 255; - bytes[i * 4 + 2] = 255; - bytes[i * 4 + 3] = (byte)(Math.Min(progress / aa_portion, 1) * 255); + bytes[i * 4] = (byte)(BorderColour.R * 255); + bytes[i * 4 + 1] = (byte)(BorderColour.G * 255); + bytes[i * 4 + 2] = (byte)(BorderColour.B * 255); + bytes[i * 4 + 3] = (byte)(Math.Min(progress / aa_portion, 1) * (BorderColour.A * 255)); } else { @@ -146,9 +173,23 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces texture.SetData(upload); path.Texture = texture; + + container.ForceRedraw(); + } + + private void computeSize() + { + // Generate the entire curve + slider.Curve.GetPathToProgress(CurrentCurve, 0, 1); + foreach (Vector2 p in CurrentCurve) + path.AddVertex(p); + + Size = path.Size; + + topLeftOffset = path.PositionInBoundingBox(Vector2.Zero); + PathOffset = path.PositionInBoundingBox(CurrentCurve[0]); } - public readonly List CurrentCurve = new List(); private bool updateSnaking(double p0, double p1) { if (SnakedStart == p0 && SnakedEnd == p1) return false; @@ -160,26 +201,29 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces path.ClearVertices(); foreach (Vector2 p in CurrentCurve) - path.AddVertex(p - CurrentCurve[0]); + path.AddVertex(p); return true; } - public void UpdateProgress(double progress, int span) + public void UpdateProgress(double completionProgress) { + var span = slider.SpanAt(completionProgress); + var spanProgress = slider.ProgressAt(completionProgress); + double start = 0; - double end = snakingIn ? MathHelper.Clamp((Time.Current - (slider.StartTime - slider.TimePreempt)) / slider.TimeFadein, 0, 1) : 1; + double end = SnakingIn ? MathHelper.Clamp((Time.Current - (slider.StartTime - slider.TimePreempt)) / slider.TimeFadein, 0, 1) : 1; if (span >= slider.SpanCount() - 1) { if (Math.Min(span, slider.SpanCount() - 1) % 2 == 1) { start = 0; - end = snakingOut ? progress : 1; + end = SnakingOut ? spanProgress : 1; } else { - start = snakingOut ? progress : 0; + start = SnakingOut ? spanProgress : 0; } } diff --git a/osu.Game.Rulesets.Osu/Objects/ISliderProgress.cs b/osu.Game.Rulesets.Osu/Objects/ISliderProgress.cs index 54f783b664..a0566eaf17 100644 --- a/osu.Game.Rulesets.Osu/Objects/ISliderProgress.cs +++ b/osu.Game.Rulesets.Osu/Objects/ISliderProgress.cs @@ -5,6 +5,10 @@ namespace osu.Game.Rulesets.Osu.Objects { public interface ISliderProgress { - void UpdateProgress(double progress, int span); + /// + /// Updates the progress of this element along the slider. + /// + /// Amount of the slider completed. + void UpdateProgress(double completionProgress); } } diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 9b9d88f0f6..c00c30ced9 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -1,23 +1,39 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using OpenTK; using osu.Game.Rulesets.Objects.Types; -using OpenTK.Graphics; using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Rulesets.Osu.Objects { - public abstract class OsuHitObject : HitObject, IHasCombo, IHasPosition + public abstract class OsuHitObject : HitObject, IHasComboInformation, IHasPosition { public const double OBJECT_RADIUS = 64; + public event Action PositionChanged; + public double TimePreempt = 600; public double TimeFadein = 400; - public Vector2 Position { get; set; } + private Vector2 position; + + public Vector2 Position + { + get => position; + set + { + if (position == value) + return; + position = value; + + PositionChanged?.Invoke(value); + } + } + public float X => Position.X; public float Y => Position.Y; @@ -35,10 +51,14 @@ namespace osu.Game.Rulesets.Osu.Objects public float Scale { get; set; } = 1; - public Color4 ComboColour { get; set; } = Color4.Gray; public virtual bool NewCombo { get; set; } + public int IndexInCurrentCombo { get; set; } + public int ComboIndex { get; set; } + + public bool LastInCombo { get; set; } + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); @@ -48,5 +68,7 @@ namespace osu.Game.Rulesets.Osu.Objects Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2; } + + public virtual void OffsetPosition(Vector2 offset) => Position += offset; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 5dd3d7aa89..469c4ddcb4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -3,7 +3,6 @@ using OpenTK; using osu.Game.Rulesets.Objects.Types; -using System; using System.Collections.Generic; using osu.Game.Rulesets.Objects; using System.Linq; @@ -23,8 +22,8 @@ namespace osu.Game.Rulesets.Osu.Objects public double EndTime => StartTime + this.SpanCount() * Curve.Distance / Velocity; public double Duration => EndTime - StartTime; - public Vector2 StackedPositionAt(double t) => this.PositionAt(t) + StackOffset; - public override Vector2 EndPosition => this.PositionAt(1); + public Vector2 StackedPositionAt(double t) => StackedPosition + this.CurvePositionAt(t); + public override Vector2 EndPosition => Position + this.CurvePositionAt(1); public SliderCurve Curve { get; } = new SliderCurve(); @@ -66,18 +65,6 @@ namespace osu.Game.Rulesets.Osu.Objects /// public double SpanDuration => Duration / this.SpanCount(); - private int stackHeight; - - public override int StackHeight - { - get { return stackHeight; } - set - { - stackHeight = value; - Curve.Offset = StackOffset; - } - } - public double Velocity; public double TickDistance; @@ -108,22 +95,22 @@ namespace osu.Game.Rulesets.Osu.Objects private void createSliderEnds() { - HeadCircle = new HitCircle + HeadCircle = new SliderCircle(this) { StartTime = StartTime, - Position = StackedPosition, - IndexInCurrentCombo = IndexInCurrentCombo, - ComboColour = ComboColour, + Position = Position, Samples = Samples, - SampleControlPoint = SampleControlPoint + SampleControlPoint = SampleControlPoint, + IndexInCurrentCombo = IndexInCurrentCombo, + ComboIndex = ComboIndex, }; - TailCircle = new HitCircle + TailCircle = new SliderCircle(this) { StartTime = EndTime, - Position = StackedEndPosition, + Position = EndPosition, IndexInCurrentCombo = IndexInCurrentCombo, - ComboColour = ComboColour + ComboIndex = ComboIndex, }; AddNested(HeadCircle); @@ -132,14 +119,16 @@ namespace osu.Game.Rulesets.Osu.Objects private void createTicks() { - if (TickDistance == 0) return; - var length = Curve.Distance; - var tickDistance = Math.Min(TickDistance, length); + var tickDistance = MathHelper.Clamp(TickDistance, 0, length); + + if (tickDistance == 0) return; var minDistanceFromEnd = Velocity * 0.01; - for (var span = 0; span < this.SpanCount(); span++) + var spanCount = this.SpanCount(); + + for (var span = 0; span < spanCount; span++) { var spanStartTime = StartTime + span * SpanDuration; var reversed = span % 2 == 1; @@ -168,10 +157,9 @@ namespace osu.Game.Rulesets.Osu.Objects SpanIndex = span, SpanStartTime = spanStartTime, StartTime = spanStartTime + timeProgress * SpanDuration, - Position = Curve.PositionAt(distanceProgress), + Position = Position + Curve.PositionAt(distanceProgress), StackHeight = StackHeight, Scale = Scale, - ComboColour = ComboColour, Samples = sampleList }); } @@ -187,10 +175,9 @@ namespace osu.Game.Rulesets.Osu.Objects RepeatIndex = repeatIndex, SpanDuration = SpanDuration, StartTime = StartTime + repeat * SpanDuration, - Position = Curve.PositionAt(repeat % 2), + Position = Position + Curve.PositionAt(repeat % 2), StackHeight = StackHeight, Scale = Scale, - ComboColour = ComboColour, Samples = new List(RepeatSamples[repeatIndex]) }); } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderCircle.cs new file mode 100644 index 0000000000..1e83d02735 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/SliderCircle.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; + +namespace osu.Game.Rulesets.Osu.Objects +{ + public class SliderCircle : HitCircle + { + private readonly Slider slider; + + public SliderCircle(Slider slider) + { + this.slider = slider; + } + + public override void OffsetPosition(Vector2 offset) => slider.OffsetPosition(offset); + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 2f238bb74b..b30e4cb932 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects.Types; using osu.Game.Beatmaps.ControlPoints; @@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Objects SpinsRequired = (int)(Duration / 1000 * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5)); // spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being. - SpinsRequired = (int)(SpinsRequired * 0.6); + SpinsRequired = (int)Math.Max(1, SpinsRequired * 0.6); } } } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index b38f95694f..d407835a96 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -19,6 +19,8 @@ using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.Replays.Types; namespace osu.Game.Rulesets.Osu { @@ -143,7 +145,9 @@ namespace osu.Game.Rulesets.Osu public override SettingsSubsection CreateSettings() => new OsuSettings(); - public override int LegacyID => 0; + public override int? LegacyID => 0; + + public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame(); public OsuRuleset(RulesetInfo rulesetInfo = null) : base(rulesetInfo) diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index a22ac6aed1..7aa4108428 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -6,7 +6,7 @@ using osu.Framework.MathUtils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using System; -using System.Diagnostics; +using System.Linq; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Replays; @@ -64,9 +64,9 @@ namespace osu.Game.Rulesets.Osu.Replays { buttonIndex = 0; - AddFrameToReplay(new ReplayFrame(-100000, 256, 500, ReplayButtonState.None)); - AddFrameToReplay(new ReplayFrame(Beatmap.HitObjects[0].StartTime - 1500, 256, 500, ReplayButtonState.None)); - AddFrameToReplay(new ReplayFrame(Beatmap.HitObjects[0].StartTime - 1000, 256, 192, ReplayButtonState.None)); + AddFrameToReplay(new OsuReplayFrame(-100000, new Vector2(256, 500))); + AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 1500, new Vector2(256, 500))); + AddFrameToReplay(new OsuReplayFrame(Beatmap.HitObjects[0].StartTime - 1500, new Vector2(256, 500))); for (int i = 0; i < Beatmap.HitObjects.Count; i++) { @@ -91,18 +91,18 @@ namespace osu.Game.Rulesets.Osu.Replays // Make the cursor stay at a hitObject as long as possible (mainly for autopilot). if (h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Miss) > endTime + h.HitWindows.HalfWindowFor(HitResult.Meh) + 50) { - if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindows.HalfWindowFor(HitResult.Meh), prev.StackedEndPosition.X, prev.StackedEndPosition.Y, ReplayButtonState.None)); - if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Meh), h.StackedPosition.X, h.StackedPosition.Y, ReplayButtonState.None)); + if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new OsuReplayFrame(endTime + h.HitWindows.HalfWindowFor(HitResult.Meh), new Vector2(prev.StackedEndPosition.X, prev.StackedEndPosition.Y))); + if (!(h is Spinner)) AddFrameToReplay(new OsuReplayFrame(h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Meh), new Vector2(h.StackedPosition.X, h.StackedPosition.Y))); } else if (h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Meh) > endTime + h.HitWindows.HalfWindowFor(HitResult.Meh) + 50) { - if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindows.HalfWindowFor(HitResult.Meh), prev.StackedEndPosition.X, prev.StackedEndPosition.Y, ReplayButtonState.None)); - if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Meh), h.StackedPosition.X, h.StackedPosition.Y, ReplayButtonState.None)); + if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new OsuReplayFrame(endTime + h.HitWindows.HalfWindowFor(HitResult.Meh), new Vector2(prev.StackedEndPosition.X, prev.StackedEndPosition.Y))); + if (!(h is Spinner)) AddFrameToReplay(new OsuReplayFrame(h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Meh), new Vector2(h.StackedPosition.X, h.StackedPosition.Y))); } else if (h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Meh) > endTime + h.HitWindows.HalfWindowFor(HitResult.Meh) + 50) { - if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new ReplayFrame(endTime + h.HitWindows.HalfWindowFor(HitResult.Meh), prev.StackedEndPosition.X, prev.StackedEndPosition.Y, ReplayButtonState.None)); - if (!(h is Spinner)) AddFrameToReplay(new ReplayFrame(h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Meh), h.StackedPosition.X, h.StackedPosition.Y, ReplayButtonState.None)); + if (!(prev is Spinner) && h.StartTime - endTime < 1000) AddFrameToReplay(new OsuReplayFrame(endTime + h.HitWindows.HalfWindowFor(HitResult.Meh), new Vector2(prev.StackedEndPosition.X, prev.StackedEndPosition.Y))); + if (!(h is Spinner)) AddFrameToReplay(new OsuReplayFrame(h.StartTime - h.HitWindows.HalfWindowFor(HitResult.Meh), new Vector2(h.StackedPosition.X, h.StackedPosition.Y))); } } @@ -118,9 +118,9 @@ namespace osu.Game.Rulesets.Osu.Replays // TODO: Shouldn't the spinner always spin in the same direction? if (h is Spinner) { - calcSpinnerStartPosAndDirection(Frames[Frames.Count - 1].Position, out startPosition, out spinnerDirection); + calcSpinnerStartPosAndDirection(((OsuReplayFrame)Frames[Frames.Count - 1]).Position, out startPosition, out spinnerDirection); - Vector2 spinCentreOffset = SPINNER_CENTRE - Frames[Frames.Count - 1].Position; + Vector2 spinCentreOffset = SPINNER_CENTRE - ((OsuReplayFrame)Frames[Frames.Count - 1]).Position; if (spinCentreOffset.Length > SPIN_RADIUS) { @@ -192,13 +192,13 @@ namespace osu.Game.Rulesets.Osu.Replays private void moveToHitObject(OsuHitObject h, Vector2 targetPos, Easing easing) { - ReplayFrame lastFrame = Frames[Frames.Count - 1]; + OsuReplayFrame lastFrame = (OsuReplayFrame)Frames[Frames.Count - 1]; // Wait until Auto could "see and react" to the next note. double waitTime = h.StartTime - Math.Max(0.0, h.TimePreempt - reactionTime); if (waitTime > lastFrame.Time) { - lastFrame = new ReplayFrame(waitTime, lastFrame.MouseX, lastFrame.MouseY, lastFrame.ButtonState); + lastFrame = new OsuReplayFrame(waitTime, lastFrame.Position) { Actions = lastFrame.Actions }; AddFrameToReplay(lastFrame); } @@ -215,7 +215,7 @@ namespace osu.Game.Rulesets.Osu.Replays for (double time = lastFrame.Time + FrameDelay; time < h.StartTime; time += FrameDelay) { Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPos, lastFrame.Time, h.StartTime, easing); - AddFrameToReplay(new ReplayFrame((int)time, currentPosition.X, currentPosition.Y, lastFrame.ButtonState)); + AddFrameToReplay(new OsuReplayFrame((int)time, new Vector2(currentPosition.X, currentPosition.Y)) { Actions = lastFrame.Actions }); } buttonIndex = 0; @@ -231,14 +231,14 @@ namespace osu.Game.Rulesets.Osu.Replays { // Time to insert the first frame which clicks the object // Here we mainly need to determine which button to use - ReplayButtonState button = buttonIndex % 2 == 0 ? ReplayButtonState.Left1 : ReplayButtonState.Right1; + var action = buttonIndex % 2 == 0 ? OsuAction.LeftButton : OsuAction.RightButton; - ReplayFrame startFrame = new ReplayFrame(h.StartTime, startPosition.X, startPosition.Y, button); + var startFrame = new OsuReplayFrame(h.StartTime, new Vector2(startPosition.X, startPosition.Y), action); // TODO: Why do we delay 1 ms if the object is a spinner? There already is KEY_UP_DELAY from hEndTime. double hEndTime = ((h as IHasEndTime)?.EndTime ?? h.StartTime) + KEY_UP_DELAY; int endDelay = h is Spinner ? 1 : 0; - ReplayFrame endFrame = new ReplayFrame(hEndTime + endDelay, h.StackedEndPosition.X, h.StackedEndPosition.Y, ReplayButtonState.None); + var endFrame = new OsuReplayFrame(hEndTime + endDelay, new Vector2(h.StackedEndPosition.X, h.StackedEndPosition.Y)); // Decrement because we want the previous frame, not the next one int index = FindInsertionIndex(startFrame) - 1; @@ -248,19 +248,18 @@ namespace osu.Game.Rulesets.Osu.Replays // Do we have a previous frame? No need to check for < replay.Count since we decremented! if (index >= 0) { - ReplayFrame previousFrame = Frames[index]; - var previousButton = previousFrame.ButtonState; + var previousFrame = (OsuReplayFrame)Frames[index]; + var previousActions = previousFrame.Actions; // If a button is already held, then we simply alternate - if (previousButton != ReplayButtonState.None) + if (previousActions.Any()) { - Debug.Assert(previousButton != (ReplayButtonState.Left1 | ReplayButtonState.Right1), "Previous button state was not Left1 nor Right1 despite only using those two states."); - // Force alternation if we have the same button. Otherwise we can just keep the naturally to us assigned button. - if (previousButton == button) + if (previousActions.Contains(action)) { - button = (ReplayButtonState.Left1 | ReplayButtonState.Right1) & ~button; - startFrame.ButtonState = button; + action = action == OsuAction.LeftButton ? OsuAction.RightButton : OsuAction.LeftButton; + startFrame.Actions.Clear(); + startFrame.Actions.Add(action); } // We always follow the most recent slider / spinner, so remove any other frames that occur while it exists. @@ -272,9 +271,14 @@ namespace osu.Game.Rulesets.Osu.Replays // After alternating we need to keep holding the other button in the future rather than the previous one. for (int j = index + 1; j < Frames.Count; ++j) { + var frame = (OsuReplayFrame)Frames[j]; + // Don't affect frames which stop pressing a button! - if (j < Frames.Count - 1 || Frames[j].ButtonState == previousButton) - Frames[j].ButtonState = button; + if (j < Frames.Count - 1 || frame.Actions.SequenceEqual(previousActions)) + { + frame.Actions.Clear(); + frame.Actions.Add(action); + } } } } @@ -298,16 +302,15 @@ namespace osu.Game.Rulesets.Osu.Replays t = ApplyModsToTime(j - h.StartTime) * spinnerDirection; Vector2 pos = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS); - AddFrameToReplay(new ReplayFrame((int)j, pos.X, pos.Y, button)); + AddFrameToReplay(new OsuReplayFrame((int)j, new Vector2(pos.X, pos.Y), action)); } t = ApplyModsToTime(s.EndTime - h.StartTime) * spinnerDirection; Vector2 endPosition = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS); - AddFrameToReplay(new ReplayFrame(s.EndTime, endPosition.X, endPosition.Y, button)); + AddFrameToReplay(new OsuReplayFrame(s.EndTime, new Vector2(endPosition.X, endPosition.Y), action)); - endFrame.MouseX = endPosition.X; - endFrame.MouseY = endPosition.Y; + endFrame.Position = endPosition; } else if (h is Slider) { @@ -315,11 +318,11 @@ namespace osu.Game.Rulesets.Osu.Replays for (double j = FrameDelay; j < s.Duration; j += FrameDelay) { - Vector2 pos = s.PositionAt(j / s.Duration); - AddFrameToReplay(new ReplayFrame(h.StartTime + j, pos.X, pos.Y, button)); + Vector2 pos = s.StackedPositionAt(j / s.Duration); + AddFrameToReplay(new OsuReplayFrame(h.StartTime + j, new Vector2(pos.X, pos.Y), action)); } - AddFrameToReplay(new ReplayFrame(s.EndTime, s.EndPosition.X, s.EndPosition.Y, button)); + AddFrameToReplay(new OsuReplayFrame(s.EndTime, new Vector2(s.StackedEndPosition.X, s.StackedEndPosition.Y), action)); } // We only want to let go of our button if we are at the end of the current replay. Otherwise something is still going on after us so we need to keep the button pressed! diff --git a/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs new file mode 100644 index 0000000000..bcdfe07417 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Replays/OsuReplayFrame.cs @@ -0,0 +1,36 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Replays.Legacy; +using osu.Game.Rulesets.Replays.Types; +using OpenTK; + +namespace osu.Game.Rulesets.Osu.Replays +{ + public class OsuReplayFrame : ReplayFrame, IConvertibleReplayFrame + { + public Vector2 Position; + public List Actions = new List(); + + public OsuReplayFrame() + { + } + + public OsuReplayFrame(double time, Vector2 position, params OsuAction[] actions) + : base(time) + { + Position = position; + Actions.AddRange(actions); + } + + public void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap) + { + Position = legacyFrame.Position; + if (legacyFrame.MouseLeft) Actions.Add(OsuAction.LeftButton); + if (legacyFrame.MouseRight) Actions.Add(OsuAction.RightButton); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Replays/OsuReplayInputHandler.cs b/osu.Game.Rulesets.Osu/Replays/OsuReplayInputHandler.cs index 63c9111190..0a61b0f199 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuReplayInputHandler.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuReplayInputHandler.cs @@ -2,32 +2,42 @@ // 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.Framework.MathUtils; using osu.Game.Rulesets.Replays; using OpenTK; namespace osu.Game.Rulesets.Osu.Replays { - public class OsuReplayInputHandler : FramedReplayInputHandler + public class OsuReplayInputHandler : FramedReplayInputHandler { public OsuReplayInputHandler(Replay replay) : base(replay) { } + protected override bool IsImportant(OsuReplayFrame frame) => frame.Actions.Any(); + + protected Vector2? Position + { + get + { + if (!HasFrames) + return null; + + return Interpolation.ValueAt(CurrentTime, CurrentFrame.Position, NextFrame.Position, CurrentFrame.Time, NextFrame.Time); + } + } + public override List GetPendingStates() { - List actions = new List(); - - if (CurrentFrame?.MouseLeft ?? false) actions.Add(OsuAction.LeftButton); - if (CurrentFrame?.MouseRight ?? false) actions.Add(OsuAction.RightButton); - return new List { new ReplayState { Mouse = new ReplayMouseState(ToScreenSpace(Position ?? Vector2.Zero)), - PressedActions = actions + PressedActions = CurrentFrame.Actions } }; } diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/basic-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/basic-expected-conversion.json new file mode 100644 index 0000000000..b82fddbe79 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/basic-expected-conversion.json @@ -0,0 +1,124 @@ +{ + "Mappings": [{ + "StartTime": 500, + "Objects": [{ + "StartTime": 500, + "EndTime": 2500, + "StartX": 96, + "StartY": 192, + "EndX": 96, + "EndY": 192 + }] + }, + { + "StartTime": 3000, + "Objects": [{ + "StartTime": 3000, + "EndTime": 4000, + "StartX": 256, + "StartY": 192, + "EndX": 256, + "EndY": 192 + }] + }, + { + "StartTime": 4500, + "Objects": [{ + "StartTime": 4500, + "EndTime": 5500, + "StartX": 256, + "StartY": 192, + "EndX": 256, + "EndY": 192 + }] + }, + { + "StartTime": 6000, + "Objects": [{ + "StartTime": 6000, + "EndTime": 6500, + "StartX": 256, + "StartY": 192, + "EndX": 256, + "EndY": 192 + }] + }, + { + "StartTime": 7000, + "Objects": [{ + "StartTime": 7000, + "EndTime": 8000, + "StartX": 256, + "StartY": 128, + "EndX": 256, + "EndY": 128 + }] + }, + { + "StartTime": 8500, + "Objects": [{ + "StartTime": 8500, + "EndTime": 10999, + "StartX": 32, + "StartY": 192, + "EndX": 508.166229, + "EndY": 153.299271 + }] + }, + { + "StartTime": 11500, + "Objects": [{ + "StartTime": 11500, + "EndTime": 12000, + "StartX": 256, + "StartY": 192, + "EndX": 256, + "EndY": 192 + }] + }, + { + "StartTime": 12500, + "Objects": [{ + "StartTime": 12500, + "EndTime": 16500, + "StartX": 512, + "StartY": 320, + "EndX": 291.1977, + "EndY": 40.799427 + }] + }, + { + "StartTime": 17000, + "Objects": [{ + "StartTime": 17000, + "EndTime": 18000, + "StartX": 256, + "StartY": 256, + "EndX": 256, + "EndY": 256 + }] + }, + { + "StartTime": 18500, + "Objects": [{ + "StartTime": 18500, + "EndTime": 19450, + "StartX": 256, + "StartY": 192, + "EndX": 256, + "EndY": 192 + }] + }, + { + "StartTime": 19875, + "Objects": [{ + "StartTime": 19875, + "EndTime": 23874, + "StartX": 216, + "StartY": 231, + "EndX": 408.720825, + "EndY": 339.810455 + }] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/basic.osu b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/basic.osu new file mode 100644 index 0000000000..40b4409760 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/basic.osu @@ -0,0 +1,27 @@ +osu file format v14 + +[Difficulty] +HPDrainRate:6 +CircleSize:4 +OverallDifficulty:7 +ApproachRate:8.3 +SliderMultiplier:1.6 +SliderTickRate:1 + +[TimingPoints] +500,500,4,2,1,50,1,0 +13426,-100,4,3,1,45,0,0 +14884,-100,4,2,1,50,0,0 + +[HitObjects] +96,192,500,6,0,L|416:192,2,320 +256,192,3000,12,0,4000,0:0:0:0: +256,192,4500,12,0,5500,0:0:0:0: +256,192,6000,12,0,6500,0:0:0:0: +256,128,7000,6,0,L|352:128,4,80 +32,192,8500,6,0,B|32:384|256:384|256:192|256:192|256:0|512:0|512:192,1,800 +256,192,11500,12,0,12000,0:0:0:0: +512,320,12500,6,0,B|0:256|0:256|512:96|512:96|256:32,1,1280 +256,256,17000,6,0,L|160:256,4,80 +256,192,18500,12,0,19450,0:0:0:0: +216,231,19875,6,0,B|216:135|280:135|344:135|344:199|344:263|248:327|248:327|120:327|120:327|56:39|408:39|408:39|472:150|408:342,1,1280 diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json new file mode 100644 index 0000000000..7fe038658c --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json @@ -0,0 +1,13 @@ +{ + "Mappings": [{ + "StartTime": 118858, + "Objects": [{ + "StartTime": 118858, + "EndTime": 119088, + "StartX": 219, + "StartY": 215, + "EndX": 239.6507, + "EndY": 29.1437378 + }] + }] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu new file mode 100644 index 0000000000..8c3edc9571 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu @@ -0,0 +1,15 @@ +osu file format v14 + +[Difficulty] +HPDrainRate:6 +CircleSize:4.2 +OverallDifficulty:9 +ApproachRate:9.8 +SliderMultiplier:1.87 +SliderTickRate:1 + +[TimingPoints] +49051,230.769230769231,4,2,1,15,1,0 + +[HitObjects] +219,215,118858,2,0,P|224:170|244:-10,1,187,8|2,0:0|0:0,0:0:0:0: diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 67b96f1fd9..d41331e3bd 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -68,6 +68,8 @@ namespace osu.Game.Rulesets.Osu.Scoring score.Statistics[HitResult.Miss] = scoreResultCounts.GetOrDefault(HitResult.Miss); } + private const double harshness = 0.01; + protected override void OnNewJudgement(Judgement judgement) { base.OnNewJudgement(judgement); @@ -83,15 +85,15 @@ namespace osu.Game.Rulesets.Osu.Scoring switch (judgement.Result) { case HitResult.Great: - Health.Value += (10.2 - hpDrainRate) * 0.02; + Health.Value += (10.2 - hpDrainRate) * harshness; break; case HitResult.Good: - Health.Value += (8 - hpDrainRate) * 0.02; + Health.Value += (8 - hpDrainRate) * harshness; break; case HitResult.Meh: - Health.Value += (4 - hpDrainRate) * 0.02; + Health.Value += (4 - hpDrainRate) * harshness; break; /*case HitResult.SliderTick: @@ -99,7 +101,7 @@ namespace osu.Game.Rulesets.Osu.Scoring break;*/ case HitResult.Miss: - Health.Value -= hpDrainRate * 0.04; + Health.Value -= hpDrainRate * (harshness * 2); break; } } diff --git a/osu.Game.Rulesets.Osu/Tests/OsuBeatmapConversionTest.cs b/osu.Game.Rulesets.Osu/Tests/OsuBeatmapConversionTest.cs new file mode 100644 index 0000000000..59c59dc0e3 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Tests/OsuBeatmapConversionTest.cs @@ -0,0 +1,70 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.MathUtils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; +using OpenTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class OsuBeatmapConversionTest : BeatmapConversionTest + { + protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; + + [TestCase("basic")] + [TestCase("colinear-perfect-curve")] + public new void Test(string name) + { + base.Test(name); + } + + protected override IEnumerable CreateConvertValue(HitObject hitObject) + { + var startPosition = (hitObject as IHasPosition)?.Position ?? new Vector2(256, 192); + var endPosition = (hitObject as Slider)?.EndPosition ?? startPosition; + + yield return new ConvertValue + { + StartTime = hitObject.StartTime, + EndTime = (hitObject as IHasEndTime)?.EndTime ?? hitObject.StartTime, + StartX = startPosition.X, + StartY = startPosition.Y, + EndX = endPosition.X, + EndY = endPosition.Y + }; + } + + protected override IBeatmapConverter CreateConverter(Beatmap beatmap) => new OsuBeatmapConverter(); + } + + public struct ConvertValue : IEquatable + { + /// + /// A sane value to account for osu!stable using ints everwhere. + /// + private const double conversion_lenience = 2; + + public double StartTime; + public double EndTime; + public float StartX; + public float StartY; + public float EndX; + public float EndY; + + public bool Equals(ConvertValue other) + => Precision.AlmostEquals(StartTime, other.StartTime) + && Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience) + && Precision.AlmostEquals(StartX, other.StartX) + && Precision.AlmostEquals(StartY, other.StartY, conversion_lenience) + && Precision.AlmostEquals(EndX, other.EndX, conversion_lenience) + && Precision.AlmostEquals(EndY, other.EndY, conversion_lenience); + } +} diff --git a/osu.Game.Rulesets.Osu/Tests/TestCaseEditor.cs b/osu.Game.Rulesets.Osu/Tests/TestCaseEditor.cs new file mode 100644 index 0000000000..a11f32935e --- /dev/null +++ b/osu.Game.Rulesets.Osu/Tests/TestCaseEditor.cs @@ -0,0 +1,17 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [TestFixture] + public class TestCaseEditor : EditorTestCase + { + public TestCaseEditor() + : base(new OsuRuleset()) + { + } + } +} diff --git a/osu.Game.Rulesets.Osu/Tests/TestCaseGameplayCursor.cs b/osu.Game.Rulesets.Osu/Tests/TestCaseGameplayCursor.cs new file mode 100644 index 0000000000..273422f2e9 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Tests/TestCaseGameplayCursor.cs @@ -0,0 +1,33 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; +using osu.Game.Graphics.Cursor; +using osu.Game.Rulesets.Osu.UI.Cursor; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Osu.Tests +{ + [TestFixture] + public class TestCaseGameplayCursor : OsuTestCase, IProvideCursor + { + private GameplayCursor cursor; + + public override IReadOnlyList RequiredTypes => new [] { typeof(CursorTrail) }; + + public CursorContainer Cursor => cursor; + + public bool ProvidingUserCursor => true; + + [BackgroundDependencyLoader] + private void load() + { + Add(cursor = new GameplayCursor { RelativeSizeAxes = Axes.Both }); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Tests/TestCaseHitCircle.cs b/osu.Game.Rulesets.Osu/Tests/TestCaseHitCircle.cs index 77c70c68cd..b0cfa43f15 100644 --- a/osu.Game.Rulesets.Osu/Tests/TestCaseHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Tests/TestCaseHitCircle.cs @@ -1,7 +1,6 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; @@ -11,17 +10,17 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Tests.Visual; using OpenTK; -using OpenTK.Graphics; using osu.Game.Rulesets.Osu.Judgements; using System.Collections.Generic; using System; using osu.Game.Rulesets.Mods; using System.Linq; +using NUnit.Framework; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Tests { - [Ignore("getting CI working")] + [TestFixture] public class TestCaseHitCircle : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] @@ -61,7 +60,6 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + 1000 + timeOffset, Position = positionOffset.Value, - ComboColour = Color4.LightSeaGreen }; circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize }); diff --git a/osu.Game.Rulesets.Osu/Tests/TestCaseHitCircleHidden.cs b/osu.Game.Rulesets.Osu/Tests/TestCaseHitCircleHidden.cs index 1f64de496d..f030c6db60 100644 --- a/osu.Game.Rulesets.Osu/Tests/TestCaseHitCircleHidden.cs +++ b/osu.Game.Rulesets.Osu/Tests/TestCaseHitCircleHidden.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Mods; namespace osu.Game.Rulesets.Osu.Tests { - [Ignore("getting CI working")] + [TestFixture] public class TestCaseHitCircleHidden : TestCaseHitCircle { public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModHidden) }).ToList(); diff --git a/osu.Game.Rulesets.Osu/Tests/TestCasePerformancePoints.cs b/osu.Game.Rulesets.Osu/Tests/TestCasePerformancePoints.cs index 500347c874..b6dca3f1cb 100644 --- a/osu.Game.Rulesets.Osu/Tests/TestCasePerformancePoints.cs +++ b/osu.Game.Rulesets.Osu/Tests/TestCasePerformancePoints.cs @@ -5,7 +5,7 @@ using NUnit.Framework; namespace osu.Game.Rulesets.Osu.Tests { - [Ignore("getting CI working")] + [TestFixture] public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints { public TestCasePerformancePoints() diff --git a/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs b/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs index 93085df975..e819d8fff5 100644 --- a/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs +++ b/osu.Game.Rulesets.Osu/Tests/TestCaseSlider.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Audio; @@ -16,6 +15,7 @@ using OpenTK; using OpenTK.Graphics; using osu.Game.Rulesets.Mods; using System.Linq; +using NUnit.Framework; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; @@ -24,7 +24,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Osu.Tests { - [Ignore("getting CI working")] + [TestFixture] public class TestCaseSlider : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] @@ -90,10 +90,15 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("Catmull Slider", () => testCatmull()); AddStep("Catmull Slider 1 Repeat", () => testCatmull(1)); AddStep("Catmull Slider 2 Repeats", () => testCatmull(2)); + + AddStep("Big Single, Large StackOffset", () => testSimpleBigLargeStackOffset()); + AddStep("Big 1 Repeat, Large StackOffset", () => testSimpleBigLargeStackOffset(1)); } private void testSimpleBig(int repeats = 0) => createSlider(2, repeats: repeats); + private void testSimpleBigLargeStackOffset(int repeats = 0) => createSlider(2, repeats: repeats, stackHeight: 10); + private void testSimpleMedium(int repeats = 0) => createSlider(5, repeats: repeats); private void testSimpleSmall(int repeats = 0) => createSlider(7, repeats: repeats); @@ -106,21 +111,21 @@ namespace osu.Game.Rulesets.Osu.Tests private void testShortHighSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 15); - private void createSlider(float circleSize = 2, float distance = 400, int repeats = 0, double speedMultiplier = 2) + private void createSlider(float circleSize = 2, float distance = 400, int repeats = 0, double speedMultiplier = 2, int stackHeight = 0) { var slider = new Slider { StartTime = Time.Current + 1000, Position = new Vector2(-(distance / 2), 0), - ComboColour = Color4.LightSeaGreen, ControlPoints = new List { - new Vector2(-(distance / 2), 0), - new Vector2(distance / 2, 0), + Vector2.Zero, + new Vector2(distance, 0), }, Distance = distance, RepeatCount = repeats, - RepeatSamples = createEmptySamples(repeats) + RepeatSamples = createEmptySamples(repeats), + StackHeight = stackHeight }; addSlider(slider, circleSize, speedMultiplier); @@ -132,12 +137,11 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + 1000, Position = new Vector2(-200, 0), - ComboColour = Color4.LightSeaGreen, ControlPoints = new List { - new Vector2(-200, 0), - new Vector2(0, 200), - new Vector2(200, 0) + Vector2.Zero, + new Vector2(200, 200), + new Vector2(400, 0) }, Distance = 600, RepeatCount = repeats, @@ -156,15 +160,14 @@ namespace osu.Game.Rulesets.Osu.Tests CurveType = CurveType.Linear, StartTime = Time.Current + 1000, Position = new Vector2(-200, 0), - ComboColour = Color4.LightSeaGreen, ControlPoints = new List { - new Vector2(-200, 0), - new Vector2(-50, 75), - new Vector2(0, 100), - new Vector2(100, -200), + Vector2.Zero, + new Vector2(150, 75), new Vector2(200, 0), - new Vector2(230, 0) + new Vector2(300, -200), + new Vector2(400, 0), + new Vector2(430, 0) }, Distance = 793.4417, RepeatCount = repeats, @@ -183,14 +186,13 @@ namespace osu.Game.Rulesets.Osu.Tests CurveType = CurveType.Bezier, StartTime = Time.Current + 1000, Position = new Vector2(-200, 0), - ComboColour = Color4.LightSeaGreen, ControlPoints = new List { - new Vector2(-200, 0), - new Vector2(-50, 75), - new Vector2(0, 100), - new Vector2(100, -200), - new Vector2(230, 0) + Vector2.Zero, + new Vector2(150, 75), + new Vector2(200, 100), + new Vector2(300, -200), + new Vector2(430, 0) }, Distance = 480, RepeatCount = repeats, @@ -209,10 +211,9 @@ namespace osu.Game.Rulesets.Osu.Tests CurveType = CurveType.Linear, StartTime = Time.Current + 1000, Position = new Vector2(0, 0), - ComboColour = Color4.LightSeaGreen, ControlPoints = new List { - new Vector2(0, 0), + Vector2.Zero, new Vector2(-200, 0), new Vector2(0, 0), new Vector2(0, -200), @@ -239,14 +240,13 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = Time.Current + 1000, Position = new Vector2(-100, 0), - ComboColour = Color4.LightSeaGreen, CurveType = CurveType.Catmull, ControlPoints = new List { - new Vector2(-100, 0), - new Vector2(-50, -50), - new Vector2(50, 50), - new Vector2(100, 0) + Vector2.Zero, + new Vector2(50, -50), + new Vector2(150, 50), + new Vector2(200, 0) }, Distance = 300, RepeatCount = repeats, diff --git a/osu.Game.Rulesets.Osu/Tests/TestCaseSliderHidden.cs b/osu.Game.Rulesets.Osu/Tests/TestCaseSliderHidden.cs index bd4be1675b..57b719464f 100644 --- a/osu.Game.Rulesets.Osu/Tests/TestCaseSliderHidden.cs +++ b/osu.Game.Rulesets.Osu/Tests/TestCaseSliderHidden.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Mods; namespace osu.Game.Rulesets.Osu.Tests { - [Ignore("getting CI working")] + [TestFixture] public class TestCaseSliderHidden : TestCaseSlider { public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModHidden) }).ToList(); diff --git a/osu.Game.Rulesets.Osu/Tests/TestCaseSpinner.cs b/osu.Game.Rulesets.Osu/Tests/TestCaseSpinner.cs index f2d031633b..d3620bcbda 100644 --- a/osu.Game.Rulesets.Osu/Tests/TestCaseSpinner.cs +++ b/osu.Game.Rulesets.Osu/Tests/TestCaseSpinner.cs @@ -17,7 +17,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests { - [Ignore("getting CI working")] + [TestFixture] public class TestCaseSpinner : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] diff --git a/osu.Game.Rulesets.Osu/Tests/TestCaseSpinnerHidden.cs b/osu.Game.Rulesets.Osu/Tests/TestCaseSpinnerHidden.cs index 7764f3d4af..75b3b4c763 100644 --- a/osu.Game.Rulesets.Osu/Tests/TestCaseSpinnerHidden.cs +++ b/osu.Game.Rulesets.Osu/Tests/TestCaseSpinnerHidden.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Mods; namespace osu.Game.Rulesets.Osu.Tests { - [Ignore("getting CI working")] + [TestFixture] public class TestCaseSpinnerHidden : TestCaseSpinner { public override IReadOnlyList RequiredTypes => base.RequiredTypes.Concat(new[] { typeof(OsuModHidden) }).ToList(); diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 8be55f7b4b..3203df16e2 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -3,9 +3,9 @@ using System; using System.Diagnostics; +using System.Runtime.InteropServices; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.OpenGL.Buffers; using osu.Framework.Graphics.OpenGL.Vertices; using osu.Framework.Graphics.Primitives; @@ -14,11 +14,12 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Input; using osu.Framework.Timing; using OpenTK; +using OpenTK.Graphics; using OpenTK.Graphics.ES30; namespace osu.Game.Rulesets.Osu.UI.Cursor { - internal class CursorTrail : Drawable + internal class CursorTrail : Drawable, IRequireHighFrequencyMousePosition { private int currentIndex; @@ -31,6 +32,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private float time; + public override bool IsPresent => true; + private readonly TrailDrawNodeSharedData trailDrawNodeSharedData = new TrailDrawNodeSharedData(); private const int max_sprites = 2048; @@ -96,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor const int fade_clock_reset_threshold = 1000000; - time = (float)(Time.Current - timeOffset) / 500f; + time = (float)(Time.Current - timeOffset) / 300f; if (time > fade_clock_reset_threshold) resetTime(); } @@ -115,14 +118,16 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor protected override bool OnMouseMove(InputState state) { + Vector2 pos = state.Mouse.NativeState.Position; + if (lastPosition == null) { - lastPosition = state.Mouse.NativeState.Position; + lastPosition = pos; resampler.AddPosition(lastPosition.Value); return base.OnMouseMove(state); } - foreach (Vector2 pos2 in resampler.AddPosition(state.Mouse.NativeState.Position)) + foreach (Vector2 pos2 in resampler.AddPosition(pos)) { Trace.Assert(lastPosition.HasValue); @@ -163,7 +168,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private class TrailDrawNodeSharedData { - public VertexBuffer VertexBuffer; + public VertexBuffer VertexBuffer; } private class TrailDrawNode : DrawNode @@ -189,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor public override void Draw(Action vertexAction) { if (Shared.VertexBuffer == null) - Shared.VertexBuffer = new QuadVertexBuffer(max_sprites, BufferUsageHint.DynamicDraw); + Shared.VertexBuffer = new QuadVertexBuffer(max_sprites, BufferUsageHint.DynamicDraw); Shader.GetUniform("g_FadeClock").Value = Time; @@ -206,17 +211,19 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor int end = start; Vector2 pos = Parts[i].Position; - ColourInfo colour = DrawInfo.Colour; - colour.TopLeft.Linear.A = Parts[i].Time + colour.TopLeft.Linear.A; - colour.TopRight.Linear.A = Parts[i].Time + colour.TopRight.Linear.A; - colour.BottomLeft.Linear.A = Parts[i].Time + colour.BottomLeft.Linear.A; - colour.BottomRight.Linear.A = Parts[i].Time + colour.BottomRight.Linear.A; + float time = Parts[i].Time; Texture.DrawQuad( new Quad(pos.X - Size.X / 2, pos.Y - Size.Y / 2, Size.X, Size.Y), - colour, + DrawInfo.Colour, null, - v => Shared.VertexBuffer.Vertices[end++] = v); + v => Shared.VertexBuffer.Vertices[end++] = new TexturedTrailVertex + { + Position = v.Position, + TexturePosition = v.TexturePosition, + Time = time + 1, + Colour = v.Colour, + }); Parts[i].WasUpdated = false; } @@ -241,5 +248,26 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor Shader.Unbind(); } } + + [StructLayout(LayoutKind.Sequential)] + public struct TexturedTrailVertex : IEquatable, IVertex + { + [VertexMember(2, VertexAttribPointerType.Float)] + public Vector2 Position; + [VertexMember(4, VertexAttribPointerType.Float)] + public Color4 Colour; + [VertexMember(2, VertexAttribPointerType.Float)] + public Vector2 TexturePosition; + [VertexMember(1, VertexAttribPointerType.Float)] + public float Time; + + public bool Equals(TexturedTrailVertex other) + { + return Position.Equals(other.Position) + && TexturePosition.Equals(other.TexturePosition) + && Colour.Equals(other.Colour) + && Time.Equals(other.Time); + } + } } } diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursor.cs b/osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursor.cs index 0aeb14514d..ac81d93309 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursor.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/GameplayCursor.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Skinning; using OpenTK; using OpenTK.Graphics; @@ -20,118 +21,24 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor { protected override Drawable CreateCursor() => new OsuCursor(); + protected override Container Content => fadeContainer; + + private readonly Container fadeContainer; + public GameplayCursor() { - Add(new CursorTrail { Depth = 1 }); + InternalChild = fadeContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new CursorTrail { Depth = 1 } + } + }; } private int downCount; - public class OsuCursor : Container - { - private Container cursorContainer; - - private Bindable cursorScale; - private Bindable autoCursorScale; - private Bindable beatmap; - - public OsuCursor() - { - Origin = Anchor.Centre; - Size = new Vector2(42); - } - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config, OsuGameBase game) - { - Children = new Drawable[] - { - cursorContainer = new CircularContainer - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = Size.X / 6, - BorderColour = Color4.White, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Colour = Color4.Pink.Opacity(0.5f), - Radius = 5, - }, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true, - }, - new CircularContainer - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = Size.X / 3, - BorderColour = Color4.White.Opacity(0.5f), - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true, - }, - }, - }, - new CircularContainer - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Scale = new Vector2(0.1f), - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.White, - }, - }, - }, - } - }, - }; - - beatmap = game.Beatmap.GetBoundCopy(); - beatmap.ValueChanged += v => calculateScale(); - - cursorScale = config.GetBindable(OsuSetting.GameplayCursorSize); - cursorScale.ValueChanged += v => calculateScale(); - - autoCursorScale = config.GetBindable(OsuSetting.AutoCursorSize); - autoCursorScale.ValueChanged += v => calculateScale(); - - calculateScale(); - } - - private void calculateScale() - { - float scale = (float)cursorScale.Value; - - if (autoCursorScale && beatmap.Value != null) - { - // if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier. - scale *= (float)(1 - 0.7 * (1 + beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY); - } - - cursorContainer.Scale = new Vector2(scale); - } - } - public bool OnPressed(OsuAction action) { switch (action) @@ -160,16 +67,123 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor return false; } + public override bool HandleMouseInput => true; // OverlayContainer will set this false when we go hidden, but we always want to receive input. + protected override void PopIn() { - ActiveCursor.FadeTo(1, 250, Easing.OutQuint); + fadeContainer.FadeTo(1, 300, Easing.OutQuint); ActiveCursor.ScaleTo(1, 400, Easing.OutQuint); } protected override void PopOut() { - ActiveCursor.FadeTo(0, 250, Easing.OutQuint); - ActiveCursor.ScaleTo(0.6f, 250, Easing.In); + fadeContainer.FadeTo(0.05f, 450, Easing.OutQuint); + ActiveCursor.ScaleTo(0.8f, 450, Easing.OutQuint); + } + + public class OsuCursor : Container + { + private Drawable cursorContainer; + + private Bindable cursorScale; + private Bindable autoCursorScale; + private Bindable beatmap; + + public OsuCursor() + { + Origin = Anchor.Centre; + Size = new Vector2(42); + } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config, OsuGameBase game) + { + Child = cursorContainer = new SkinnableDrawable("cursor", _ => new CircularContainer + { + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = Size.X / 6, + BorderColour = Color4.White, + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Pink.Opacity(0.5f), + Radius = 5, + }, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true, + }, + new CircularContainer + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = Size.X / 3, + BorderColour = Color4.White.Opacity(0.5f), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true, + }, + }, + }, + new CircularContainer + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Scale = new Vector2(0.1f), + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + }, + }, + }, + } + }, restrictSize: false) + { + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }; + + beatmap = game.Beatmap.GetBoundCopy(); + beatmap.ValueChanged += v => calculateScale(); + + cursorScale = config.GetBindable(OsuSetting.GameplayCursorSize); + cursorScale.ValueChanged += v => calculateScale(); + + autoCursorScale = config.GetBindable(OsuSetting.AutoCursorSize); + autoCursorScale.ValueChanged += v => calculateScale(); + + calculateScale(); + } + + private void calculateScale() + { + float scale = (float)cursorScale.Value; + + if (autoCursorScale && beatmap.Value != null) + { + // if we have a beatmap available, let's get its circle size to figure out an automatic cursor scale modifier. + scale *= (float)(1 - 0.7 * (1 + beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY); + } + + cursorContainer.Scale = new Vector2(scale); + } } } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 17521f8992..98a8096678 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.UI public class OsuPlayfield : Playfield { private readonly Container approachCircles; - private readonly Container judgementLayer; + private readonly JudgementContainer judgementLayer; private readonly ConnectionRenderer connectionLayer; // Todo: This should not be a thing, but is currently required for the editor @@ -27,21 +27,8 @@ namespace osu.Game.Rulesets.Osu.UI public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); - public override Vector2 Size - { - get - { - if (Parent == null) - return Vector2.Zero; - - var parentSize = Parent.DrawSize; - var aspectSize = parentSize.X * 0.75f < parentSize.Y ? new Vector2(parentSize.X, parentSize.X * 0.75f) : new Vector2(parentSize.Y * 4f / 3f, parentSize.Y); - - return new Vector2(aspectSize.X / parentSize.X, aspectSize.Y / parentSize.Y) * base.Size; - } - } - - public OsuPlayfield() : base(BASE_SIZE.X) + public OsuPlayfield() + : base(BASE_SIZE.X) { Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -53,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.UI RelativeSizeAxes = Axes.Both, Depth = 2, }, - judgementLayer = new Container + judgementLayer = new JudgementContainer { RelativeSizeAxes = Axes.Both, Depth = 1, @@ -88,16 +75,13 @@ namespace osu.Game.Rulesets.Osu.UI private void onJudgement(DrawableHitObject judgedObject, Judgement judgement) { - var osuJudgement = (OsuJudgement)judgement; - var osuObject = (OsuHitObject)judgedObject.HitObject; - if (!judgedObject.DisplayJudgement) return; - DrawableOsuJudgement explosion = new DrawableOsuJudgement(osuJudgement) + DrawableOsuJudgement explosion = new DrawableOsuJudgement(judgement, judgedObject) { Origin = Anchor.Centre, - Position = osuObject.StackedEndPosition + osuJudgement.PositionOffset + Position = ((OsuHitObject)judgedObject.HitObject).StackedEndPosition + ((OsuJudgement)judgement).PositionOffset }; judgementLayer.Add(explosion); diff --git a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs index 526348062f..b825ba73b7 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs @@ -5,6 +5,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Input; using OpenTK; using osu.Game.Beatmaps; +using osu.Game.Input.Handlers; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; @@ -48,9 +49,13 @@ namespace osu.Game.Rulesets.Osu.UI return null; } - protected override FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => new OsuReplayInputHandler(replay); + protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new OsuReplayInputHandler(replay); - protected override Vector2 GetPlayfieldAspectAdjust() => new Vector2(0.75f); + protected override Vector2 GetAspectAdjustedSize() + { + var aspectSize = DrawSize.X * 0.75f < DrawSize.Y ? new Vector2(DrawSize.X, DrawSize.X * 0.75f) : new Vector2(DrawSize.Y * 4f / 3f, DrawSize.Y); + return new Vector2(aspectSize.X / DrawSize.X, aspectSize.Y / DrawSize.Y); + } protected override CursorContainer CreateCursor() => new GameplayCursor(); } diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs index 5493a5029b..afa3d162f4 100644 --- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs +++ b/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs @@ -2,10 +2,9 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Audio { @@ -14,7 +13,9 @@ namespace osu.Game.Rulesets.Taiko.Audio private readonly ControlPointInfo controlPoints; private readonly Dictionary mappings = new Dictionary(); - public DrumSampleMapping(ControlPointInfo controlPoints, AudioManager audio) + public readonly List Sounds = new List(); + + public DrumSampleMapping(ControlPointInfo controlPoints) { this.controlPoints = controlPoints; @@ -27,20 +28,34 @@ namespace osu.Game.Rulesets.Taiko.Audio foreach (var s in samplePoints) { + var centre = s.GetSampleInfo(); + var rim = s.GetSampleInfo(SampleInfo.HIT_CLAP); + + // todo: this is ugly + centre.Namespace = "taiko"; + rim.Namespace = "taiko"; + mappings[s.Time] = new DrumSample { - Centre = s.GetSampleInfo().GetChannel(audio.Sample, "Taiko"), - Rim = s.GetSampleInfo(SampleInfo.HIT_CLAP).GetChannel(audio.Sample, "Taiko") + Centre = addSound(centre), + Rim = addSound(rim) }; } } + private SkinnableSound addSound(SampleInfo sampleInfo) + { + var drawable = new SkinnableSound(sampleInfo); + Sounds.Add(drawable); + return drawable; + } + public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time]; public class DrumSample { - public SampleChannel Centre; - public SampleChannel Rim; + public SkinnableSound Centre; + public SkinnableSound Rim; } } } diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index e5fe288f20..9d6b5b5ce4 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -101,16 +101,16 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps // The duration of the taiko hit object double taikoDuration = distance / taikoVelocity; - // For some reason, old osu! always uses speedAdjustment to determine the taiko velocity, but - // only uses it to determine osu! velocity if beatmap version < 8. Let's account for that here. - if (beatmap.BeatmapInfo.BeatmapVersion >= 8) - speedAdjustedBeatLength *= speedAdjustment; - // The velocity of the osu! hit object - calculated as the velocity of a slider double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * legacy_velocity_multiplier / speedAdjustedBeatLength; // The duration of the osu! hit object double osuDuration = distance / osuVelocity; + // osu-stable always uses the speed-adjusted beatlength to determine the velocities, but + // only uses it for tick rate if beatmap version < 8 + if (beatmap.BeatmapInfo.BeatmapVersion >= 8) + speedAdjustedBeatLength *= speedAdjustment; + // If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat double tickSpacing = Math.Min(speedAdjustedBeatLength / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate, taikoDuration / spans); diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs index c50878c6a3..703e6b4f1c 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDaycore.cs @@ -7,6 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModDaycore : ModDaycore { - public override double ScoreMultiplier => 0.5; + public override double ScoreMultiplier => 0.3; } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs index 1c5e43f411..be6510459e 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs @@ -7,5 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModEasy : ModEasy { + public override string Description => @"Beats move slower, less accuracy required, and three lives!"; } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs index 9813f8b78e..6542b5a844 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHalfTime.cs @@ -7,6 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModHalfTime : ModHalfTime { - public override double ScoreMultiplier => 0.5; + public override double ScoreMultiplier => 0.3; } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs index ba304c41d8..435a0c1613 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs @@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Taiko.Mods public class TaikoModHardRock : ModHardRock { public override double ScoreMultiplier => 1.06; - public override bool Ranked => true; } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index b0ad43b851..be987a1773 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -7,7 +7,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModHidden : ModHidden { - public override string Description => @"The notes fade out before you hit them!"; + public override string Description => @"Beats fade out before you hit them!"; public override double ScoreMultiplier => 1.06; } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs index ec2385bfba..d5ad04f595 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModRelax.cs @@ -7,6 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModRelax : ModRelax { - public override string Description => @"Relax! You will no longer get dizzyfied by ninja-like spinners, demanding drumrolls or unexpected katu's."; + public override string Description => @"No ninja-like spinners, demanding drumrolls or unexpected katu's."; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs index cf6aa7d895..d3a38289a8 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables RelativeSizeAxes = Axes.Y; Width = tracker_width; - Children = new[] + InternalChildren = new[] { Tracker = new Box { diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs index 23c34e9863..19a6e4eac2 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public DrawableBarLineMajor(BarLine barLine) : base(barLine) { - Add(triangleContainer = new Container + AddInternal(triangleContainer = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 29d464f614..2bb2d478c3 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -20,11 +20,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public class DrawableDrumRoll : DrawableTaikoHitObject { /// - /// Number of rolling hits required to reach the dark/final accent colour. + /// Number of rolling hits required to reach the dark/final colour. /// - private const int rolling_hits_for_dark_accent = 5; - - private Color4 accentDarkColour; + private const int rolling_hits_for_engaged_colour = 5; /// /// Rolling number of tick hits. This increases for hits and decreases for misses. @@ -53,11 +51,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public override bool OnPressed(TaikoAction action) => false; + private Color4 colourIdle; + private Color4 colourEngaged; + [BackgroundDependencyLoader] private void load(OsuColour colours) { - MainPiece.AccentColour = AccentColour = colours.YellowDark; - accentDarkColour = colours.YellowDarker; + MainPiece.AccentColour = colourIdle = colours.YellowDark; + colourEngaged = colours.YellowDarker; } private void onTickJudgement(DrawableHitObject obj, Judgement judgement) @@ -67,10 +68,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables else rollingHits--; - rollingHits = MathHelper.Clamp(rollingHits, 0, rolling_hits_for_dark_accent); + rollingHits = MathHelper.Clamp(rollingHits, 0, rolling_hits_for_engaged_colour); - Color4 newAccent = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_dark_accent, AccentColour, accentDarkColour, 0, 1); - MainPiece.FadeAccent(newAccent, 100); + Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1); + MainPiece.FadeAccent(newColour, 100); } protected override void CheckForJudgements(bool userTriggered, double timeOffset) @@ -82,8 +83,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return; int countHit = NestedHitObjects.Count(o => o.IsHit); - - if (countHit > HitObject.RequiredGoodHits) + if (countHit >= HitObject.RequiredGoodHits) { AddJudgement(new TaikoJudgement { Result = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Good }); if (HitObject.IsStrong) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index bc5abce245..65a4e7bd95 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables switch (state) { case ArmedState.Hit: - Content.ScaleTo(0, 100, Easing.OutQuint).Expire(); + this.ScaleTo(0, 100, Easing.OutQuint).Expire(); break; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 63e6cfb297..75e1e2a247 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables const float gravity_time = 300; const float gravity_travel_height = 200; - Content.ScaleTo(0.8f, gravity_time * 2, Easing.OutQuad); + this.ScaleTo(0.8f, gravity_time * 2, Easing.OutQuad); this.MoveToY(-gravity_travel_height, gravity_time, Easing.Out) .Then() diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index c9e488764c..37f1300d47 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { FillMode = FillMode.Fit; - Add(bodyContainer = new Container + AddInternal(bodyContainer = new Container { RelativeSizeAxes = Axes.Both, Depth = 1, diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index e57c2f9944..f20ad5b4aa 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables RelativeSizeAxes = Axes.Both; Size = BaseSize = new Vector2(HitObject.IsStrong ? TaikoHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE); - Add(MainPiece = CreateMainPiece()); + InternalChild = MainPiece = CreateMainPiece(); MainPiece.KiaiMode = HitObject.Kiai; } diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs index 002159439d..1a556fe01d 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs @@ -35,15 +35,13 @@ namespace osu.Game.Rulesets.Taiko.Replays { bool hitButton = true; - Frames.Add(new TaikoReplayFrame(-100000, ReplayButtonState.None)); - Frames.Add(new TaikoReplayFrame(Beatmap.HitObjects[0].StartTime - 1000, ReplayButtonState.None)); + Frames.Add(new TaikoReplayFrame(-100000)); + Frames.Add(new TaikoReplayFrame(Beatmap.HitObjects[0].StartTime - 1000)); for (int i = 0; i < Beatmap.HitObjects.Count; i++) { TaikoHitObject h = Beatmap.HitObjects[i]; - ReplayButtonState button; - IHasEndTime endTimeData = h as IHasEndTime; double endTime = endTimeData?.EndTime ?? h.StartTime; @@ -59,24 +57,26 @@ namespace osu.Game.Rulesets.Taiko.Replays double hitRate = Math.Min(swell_hit_speed, swell.Duration / req); for (double j = h.StartTime; j < endTime; j += hitRate) { + TaikoAction action; + switch (d) { default: case 0: - button = ReplayButtonState.Left1; + action = TaikoAction.LeftCentre; break; case 1: - button = ReplayButtonState.Right1; + action = TaikoAction.LeftRim; break; case 2: - button = ReplayButtonState.Left2; + action = TaikoAction.RightCentre; break; case 3: - button = ReplayButtonState.Right2; + action = TaikoAction.RightRim; break; } - Frames.Add(new TaikoReplayFrame(j, button)); + Frames.Add(new TaikoReplayFrame(j, action)); d = (d + 1) % 4; if (++count == req) break; @@ -86,39 +86,39 @@ namespace osu.Game.Rulesets.Taiko.Replays { foreach (var tick in drumRoll.NestedHitObjects.OfType()) { - Frames.Add(new TaikoReplayFrame(tick.StartTime, hitButton ? ReplayButtonState.Right1 : ReplayButtonState.Right2)); + Frames.Add(new TaikoReplayFrame(tick.StartTime, hitButton ? TaikoAction.LeftCentre : TaikoAction.RightCentre)); hitButton = !hitButton; } } else if (hit != null) { + TaikoAction[] actions; + if (hit is CentreHit) { - if (h.IsStrong) - button = ReplayButtonState.Right1 | ReplayButtonState.Right2; - else - button = hitButton ? ReplayButtonState.Right1 : ReplayButtonState.Right2; + actions = h.IsStrong + ? new[] { TaikoAction.LeftCentre, TaikoAction.RightCentre } + : new[] { hitButton ? TaikoAction.LeftCentre : TaikoAction.RightCentre }; } else { - if (h.IsStrong) - button = ReplayButtonState.Left1 | ReplayButtonState.Left2; - else - button = hitButton ? ReplayButtonState.Left1 : ReplayButtonState.Left2; + actions = h.IsStrong + ? new[] { TaikoAction.LeftRim, TaikoAction.RightRim } + : new[] { hitButton ? TaikoAction.LeftRim : TaikoAction.RightRim }; } - Frames.Add(new TaikoReplayFrame(h.StartTime, button)); + Frames.Add(new TaikoReplayFrame(h.StartTime, actions)); } else throw new InvalidOperationException("Unknown hit object type."); - Frames.Add(new TaikoReplayFrame(endTime + KEY_UP_DELAY, ReplayButtonState.None)); + Frames.Add(new TaikoReplayFrame(endTime + KEY_UP_DELAY)); if (i < Beatmap.HitObjects.Count - 1) { double waitTime = Beatmap.HitObjects[i + 1].StartTime - 1000; if (waitTime > endTime) - Frames.Add(new TaikoReplayFrame(waitTime, ReplayButtonState.None)); + Frames.Add(new TaikoReplayFrame(waitTime)); } hitButton = !hitButton; diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs index 05e10b6fce..c80bddc304 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs @@ -3,31 +3,20 @@ using osu.Game.Rulesets.Replays; using System.Collections.Generic; +using System.Linq; using osu.Framework.Input; namespace osu.Game.Rulesets.Taiko.Replays { - internal class TaikoFramedReplayInputHandler : FramedReplayInputHandler + internal class TaikoFramedReplayInputHandler : FramedReplayInputHandler { public TaikoFramedReplayInputHandler(Replay replay) : base(replay) { } - public override List GetPendingStates() - { - var actions = new List(); + protected override bool IsImportant(TaikoReplayFrame frame) => frame.Actions.Any(); - if (CurrentFrame?.MouseRight1 == true) - actions.Add(TaikoAction.LeftCentre); - if (CurrentFrame?.MouseRight2 == true) - actions.Add(TaikoAction.RightCentre); - if (CurrentFrame?.MouseLeft1 == true) - actions.Add(TaikoAction.LeftRim); - if (CurrentFrame?.MouseLeft2 == true) - actions.Add(TaikoAction.RightRim); - - return new List { new ReplayState { PressedActions = actions } }; - } + public override List GetPendingStates() => new List { new ReplayState { PressedActions = CurrentFrame.Actions } }; } } diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs index 0c60cdc109..6cd63f6c70 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoReplayFrame.cs @@ -1,17 +1,34 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Collections.Generic; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Replays.Legacy; +using osu.Game.Rulesets.Replays.Types; namespace osu.Game.Rulesets.Taiko.Replays { - public class TaikoReplayFrame : ReplayFrame + public class TaikoReplayFrame : ReplayFrame, IConvertibleReplayFrame { - public override bool IsImportant => MouseLeft || MouseRight; + public List Actions = new List(); - public TaikoReplayFrame(double time, ReplayButtonState buttons) - : base(time, null, null, buttons) + public TaikoReplayFrame() { } + + public TaikoReplayFrame(double time, params TaikoAction[] actions) + : base(time) + { + Actions.AddRange(actions); + } + + public void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap) + { + if (legacyFrame.MouseRight1) Actions.Add(TaikoAction.LeftRim); + if (legacyFrame.MouseRight2) Actions.Add(TaikoAction.RightRim); + if (legacyFrame.MouseLeft1) Actions.Add(TaikoAction.LeftCentre); + if (legacyFrame.MouseLeft2) Actions.Add(TaikoAction.RightCentre); + } } } diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/basic-expected-conversion.json b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/basic-expected-conversion.json new file mode 100644 index 0000000000..5c9310fec7 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/basic-expected-conversion.json @@ -0,0 +1,209 @@ +{ + "Mappings": [{ + "StartTime": 500, + "Objects": [{ + "StartTime": 500, + "EndTime": 2499, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": true, + "IsSwell": false, + "IsStrong": false + }] + }, + { + "StartTime": 3000, + "Objects": [{ + "StartTime": 3000, + "EndTime": 4000, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": true, + "IsStrong": false + }] + }, + { + "StartTime": 4500, + "Objects": [{ + "StartTime": 4500, + "EndTime": 5500, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": true, + "IsStrong": false + }] + }, + { + "StartTime": 6000, + "Objects": [{ + "StartTime": 6000, + "EndTime": 6500, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": true, + "IsStrong": false + }] + }, + { + "StartTime": 7000, + "Objects": [{ + "StartTime": 7000, + "EndTime": 7000, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 7249, + "EndTime": 7249, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 7499, + "EndTime": 7499, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 7749, + "EndTime": 7749, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 7999, + "EndTime": 7999, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + } + ] + }, + { + "StartTime": 8500, + "Objects": [{ + "StartTime": 8500, + "EndTime": 10999, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": true, + "IsSwell": false, + "IsStrong": false + }] + }, + { + "StartTime": 11500, + "Objects": [{ + "StartTime": 11500, + "EndTime": 12000, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": true, + "IsStrong": false + }] + }, + { + "StartTime": 12500, + "Objects": [{ + "StartTime": 12500, + "EndTime": 16499, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": true, + "IsSwell": false, + "IsStrong": false + }] + }, + { + "StartTime": 17000, + "Objects": [{ + "StartTime": 17000, + "EndTime": 17000, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 17249, + "EndTime": 17249, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 17499, + "EndTime": 17499, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 17749, + "EndTime": 17749, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 17999, + "EndTime": 17999, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + } + ] + }, + { + "StartTime": 18500, + "Objects": [{ + "StartTime": 18500, + "EndTime": 19450, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": true, + "IsStrong": false + }] + }, + { + "StartTime": 19875, + "Objects": [{ + "StartTime": 19875, + "EndTime": 23874, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": true, + "IsSwell": false, + "IsStrong": false + }] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/basic.osu b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/basic.osu new file mode 100644 index 0000000000..40b4409760 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/basic.osu @@ -0,0 +1,27 @@ +osu file format v14 + +[Difficulty] +HPDrainRate:6 +CircleSize:4 +OverallDifficulty:7 +ApproachRate:8.3 +SliderMultiplier:1.6 +SliderTickRate:1 + +[TimingPoints] +500,500,4,2,1,50,1,0 +13426,-100,4,3,1,45,0,0 +14884,-100,4,2,1,50,0,0 + +[HitObjects] +96,192,500,6,0,L|416:192,2,320 +256,192,3000,12,0,4000,0:0:0:0: +256,192,4500,12,0,5500,0:0:0:0: +256,192,6000,12,0,6500,0:0:0:0: +256,128,7000,6,0,L|352:128,4,80 +32,192,8500,6,0,B|32:384|256:384|256:192|256:192|256:0|512:0|512:192,1,800 +256,192,11500,12,0,12000,0:0:0:0: +512,320,12500,6,0,B|0:256|0:256|512:96|512:96|256:32,1,1280 +256,256,17000,6,0,L|160:256,4,80 +256,192,18500,12,0,19450,0:0:0:0: +216,231,19875,6,0,B|216:135|280:135|344:135|344:199|344:263|248:327|248:327|120:327|120:327|56:39|408:39|408:39|472:150|408:342,1,1280 diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-generating-drumroll-expected-conversion.json b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-generating-drumroll-expected-conversion.json new file mode 100644 index 0000000000..fc7d466c1b --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-generating-drumroll-expected-conversion.json @@ -0,0 +1,87 @@ +{ + "Mappings": [{ + "StartTime": 6590, + "Objects": [{ + "StartTime": 6590, + "EndTime": 8320, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": true, + "IsSwell": false, + "IsStrong": false + }] + }, + { + "StartTime": 8436, + "Objects": [{ + "StartTime": 8436, + "EndTime": 10166, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": true, + "IsSwell": false, + "IsStrong": false + }] + }, + { + "StartTime": 10282, + "Objects": [{ + "StartTime": 10282, + "EndTime": 12012, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": true, + "IsSwell": false, + "IsStrong": false + }] + }, + { + "StartTime": 12128, + "Objects": [{ + "StartTime": 12128, + "EndTime": 13858, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": true, + "IsSwell": false, + "IsStrong": false + }] + }, + { + "StartTime": 41666, + "Objects": [{ + "StartTime": 41666, + "EndTime": 42589, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": true, + "IsSwell": false, + "IsStrong": false + }] + }, + { + "StartTime": 62666, + "Objects": [{ + "StartTime": 62666, + "EndTime": 63127, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": true, + "IsSwell": false, + "IsStrong": false + }] + }, + { + "StartTime": 208743, + "Objects": [{ + "StartTime": 208743, + "EndTime": 209204, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": true, + "IsSwell": false, + "IsStrong": false + }] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-generating-drumroll.osu b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-generating-drumroll.osu new file mode 100644 index 0000000000..4c493b47d4 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-generating-drumroll.osu @@ -0,0 +1,25 @@ +osu file format v14 + +[Difficulty] +HPDrainRate:6 +CircleSize:4.2 +OverallDifficulty:9 +ApproachRate:9.8 +SliderMultiplier:1.87 +SliderTickRate:1 + +[TimingPoints] +6590,461.538461538462,4,2,2,15,1,0 +6590,-200,4,2,2,15,0,0 +49051,230.769230769231,4,2,1,15,1,0 +62666,-200,4,2,1,60,0,0 +197666,-100,4,2,1,85,0,1 + +[HitObjects] +88,104,6590,6,0,B|176:156|256:108|256:108|336:60|423:112,1,350.625,6|0,0:0|0:0,0:0:0:0: +396,213,8436,2,0,P|277:247|376:172,1,350.625,6|0,0:0|0:0,0:0:0:0: +472,220,10282,2,0,P|456:288|220:300,1,350.625,6|0,0:0|0:0,0:0:0:0: +277,200,12128,2,0,P|398:225|276:244,1,350.625,6|0,0:0|0:0,0:0:0:0: +268,229,41666,2,0,L|473:210,1,187,2|2,0:0|0:0,0:0:0:0: +133,342,62666,2,0,B|132:316|132:316|128:316|128:316|130:295|130:295|126:296|126:296|129:275|129:275|125:275|125:275|127:254|127:254|123:255|123:255|125:234|125:234|121:234|121:234|123:213|123:213|119:214|119:214|121:193|121:193|118:193|118:193|118:172,1,187,8|8,0:0|0:0,0:0:0:0: +481,338,208743,6,0,P|492:262|383:195,2,187,2|8|2,0:0|0:0|0:0,0:0:0:0: diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 50cc80db50..0a9719f27b 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -10,6 +10,8 @@ using osu.Game.Rulesets.UI; using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.Replays.Types; +using osu.Game.Rulesets.Taiko.Replays; namespace osu.Game.Rulesets.Taiko { @@ -101,7 +103,9 @@ namespace osu.Game.Rulesets.Taiko public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new TaikoDifficultyCalculator(beatmap); - public override int LegacyID => 1; + public override int? LegacyID => 1; + + public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); public TaikoRuleset(RulesetInfo rulesetInfo = null) : base(rulesetInfo) diff --git a/osu.Game.Rulesets.Taiko/Tests/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko/Tests/TaikoBeatmapConversionTest.cs new file mode 100644 index 0000000000..385e041ace --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Tests/TaikoBeatmapConversionTest.cs @@ -0,0 +1,73 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.MathUtils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Taiko.Beatmaps; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + public class TaikoBeatmapConversionTest : BeatmapConversionTest + { + protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; + + private bool isForCurrentRuleset; + + [NonParallelizable] + [TestCase("basic", false), Ignore("See: https://github.com/ppy/osu/issues/2152")] + [TestCase("slider-generating-drumroll", false)] + public void Test(string name, bool isForCurrentRuleset) + { + this.isForCurrentRuleset = isForCurrentRuleset; + base.Test(name); + } + + protected override IEnumerable CreateConvertValue(HitObject hitObject) + { + yield return new ConvertValue + { + StartTime = hitObject.StartTime, + EndTime = (hitObject as IHasEndTime)?.EndTime ?? hitObject.StartTime, + IsRim = hitObject is RimHit, + IsCentre = hitObject is CentreHit, + IsDrumRoll = hitObject is DrumRoll, + IsSwell = hitObject is Swell, + IsStrong = ((TaikoHitObject)hitObject).IsStrong + }; + } + + protected override IBeatmapConverter CreateConverter(Beatmap beatmap) => new TaikoBeatmapConverter(isForCurrentRuleset); + } + + public struct ConvertValue : IEquatable + { + /// + /// A sane value to account for osu!stable using ints everwhere. + /// + private const float conversion_lenience = 2; + + public double StartTime; + public double EndTime; + public bool IsRim; + public bool IsCentre; + public bool IsDrumRoll; + public bool IsSwell; + public bool IsStrong; + + public bool Equals(ConvertValue other) + => Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience) + && Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience) + && IsRim == other.IsRim + && IsCentre == other.IsCentre + && IsDrumRoll == other.IsDrumRoll + && IsSwell == other.IsSwell + && IsStrong == other.IsStrong; + } +} diff --git a/osu.Game.Rulesets.Taiko/Tests/TestCaseInputDrum.cs b/osu.Game.Rulesets.Taiko/Tests/TestCaseInputDrum.cs index c7201150e9..80721271d6 100644 --- a/osu.Game.Rulesets.Taiko/Tests/TestCaseInputDrum.cs +++ b/osu.Game.Rulesets.Taiko/Tests/TestCaseInputDrum.cs @@ -15,7 +15,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Taiko.Tests { - [Ignore("getting CI working")] + [TestFixture] public class TestCaseInputDrum : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] diff --git a/osu.Game.Rulesets.Taiko/Tests/TestCasePerformancePoints.cs b/osu.Game.Rulesets.Taiko/Tests/TestCasePerformancePoints.cs index 3d2d97b6d3..f6b0ceb7bd 100644 --- a/osu.Game.Rulesets.Taiko/Tests/TestCasePerformancePoints.cs +++ b/osu.Game.Rulesets.Taiko/Tests/TestCasePerformancePoints.cs @@ -5,7 +5,7 @@ using NUnit.Framework; namespace osu.Game.Rulesets.Taiko.Tests { - [Ignore("getting CI working")] + [TestFixture] public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints { public TestCasePerformancePoints() diff --git a/osu.Game.Rulesets.Taiko/Tests/TestCaseTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/Tests/TestCaseTaikoPlayfield.cs index fd396c201d..3fd16ed1b5 100644 --- a/osu.Game.Rulesets.Taiko/Tests/TestCaseTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/Tests/TestCaseTaikoPlayfield.cs @@ -25,7 +25,6 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Taiko.Tests { [TestFixture] - [Ignore("getting CI working")] public class TestCaseTaikoPlayfield : OsuTestCase { private const double default_duration = 1000; diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs index c0e8bd1b5a..6274232ffd 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs @@ -15,17 +15,14 @@ namespace osu.Game.Rulesets.Taiko.UI /// public class DrawableTaikoJudgement : DrawableJudgement { - public readonly DrawableHitObject JudgedObject; - /// /// Creates a new judgement text. /// /// The object which is being judged. /// The judgement to visualise. - public DrawableTaikoJudgement(DrawableHitObject judgedObject, Judgement judgement) - : base(judgement) + public DrawableTaikoJudgement(Judgement judgement, DrawableHitObject judgedObject) + : base(judgement, judgedObject) { - JudgedObject = judgedObject; } [BackgroundDependencyLoader] diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs index 98f20fd558..b918f495fc 100644 --- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs +++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs @@ -4,7 +4,6 @@ using System; using OpenTK; using osu.Framework.Allocation; -using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -34,9 +33,9 @@ namespace osu.Game.Rulesets.Taiko.UI } [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load() { - var sampleMappings = new DrumSampleMapping(controlPoints, audio); + var sampleMappings = new DrumSampleMapping(controlPoints); Children = new Drawable[] { @@ -63,6 +62,8 @@ namespace osu.Game.Rulesets.Taiko.UI CentreAction = TaikoAction.RightCentre } }; + + AddRangeInternal(sampleMappings.Sounds); } /// diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 49c87f7480..75aaceaecb 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -16,6 +16,7 @@ using System.Linq; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Taiko.UI @@ -41,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.UI private readonly Container hitExplosionContainer; private readonly Container kiaiExplosionContainer; - private readonly Container judgementContainer; + private readonly JudgementContainer judgementContainer; protected override Container Content => content; private readonly Container content; @@ -131,7 +132,7 @@ namespace osu.Game.Rulesets.Taiko.UI Margin = new MarginPadding { Left = HIT_TARGET_OFFSET }, Blending = BlendingMode.Additive }, - judgementContainer = new Container + judgementContainer = new JudgementContainer { Name = "Judgements", RelativeSizeAxes = Axes.Y, @@ -227,7 +228,7 @@ namespace osu.Game.Rulesets.Taiko.UI { if (judgedObject.DisplayJudgement && judgementContainer.FirstOrDefault(j => j.JudgedObject == judgedObject) == null) { - judgementContainer.Add(new DrawableTaikoJudgement(judgedObject, judgement) + judgementContainer.Add(new DrawableTaikoJudgement(judgement, judgedObject) { Anchor = judgement.IsHit ? Anchor.TopLeft : Anchor.CentreLeft, Origin = judgement.IsHit ? Anchor.BottomCentre : Anchor.Centre, diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs index 1b9821d698..eb282c53ca 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs @@ -17,6 +17,7 @@ using osu.Game.Rulesets.Taiko.Replays; using OpenTK; using System.Linq; using osu.Framework.Input; +using osu.Game.Input.Handlers; using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Rulesets.Taiko.UI @@ -78,7 +79,7 @@ namespace osu.Game.Rulesets.Taiko.UI } } - protected override Vector2 GetPlayfieldAspectAdjust() + protected override Vector2 GetAspectAdjustedSize() { const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768; const float default_aspect = 16f / 9f; @@ -88,6 +89,8 @@ namespace osu.Game.Rulesets.Taiko.UI return new Vector2(1, default_relative_height * aspectAdjust); } + protected override Vector2 PlayfieldArea => Vector2.One; + public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(this); protected override BeatmapConverter CreateBeatmapConverter() => new TaikoBeatmapConverter(IsForCurrentRuleset); @@ -131,6 +134,6 @@ namespace osu.Game.Rulesets.Taiko.UI return null; } - protected override FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => new TaikoFramedReplayInputHandler(replay); + protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new TaikoFramedReplayInputHandler(replay); } } diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 21bbc4993c..bc878b599b 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -11,6 +11,7 @@ using osu.Game.Audio; using osu.Game.Rulesets.Objects.Types; using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Timing; +using osu.Game.Skinning; namespace osu.Game.Tests.Beatmaps.Formats { @@ -20,11 +21,11 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestDecodeBeatmapGeneral() { - var decoder = new LegacyBeatmapDecoder(); + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; using (var resStream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) using (var stream = new StreamReader(resStream)) { - var beatmap = decoder.DecodeBeatmap(stream); + var beatmap = decoder.Decode(stream); var beatmapInfo = beatmap.BeatmapInfo; var metadata = beatmap.Metadata; @@ -47,7 +48,7 @@ namespace osu.Game.Tests.Beatmaps.Formats using (var resStream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) using (var stream = new StreamReader(resStream)) { - var beatmapInfo = decoder.DecodeBeatmap(stream).BeatmapInfo; + var beatmapInfo = decoder.Decode(stream).BeatmapInfo; int[] expectedBookmarks = { @@ -72,7 +73,7 @@ namespace osu.Game.Tests.Beatmaps.Formats using (var resStream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) using (var stream = new StreamReader(resStream)) { - var beatmap = decoder.DecodeBeatmap(stream); + var beatmap = decoder.Decode(stream); var beatmapInfo = beatmap.BeatmapInfo; var metadata = beatmap.Metadata; @@ -96,13 +97,13 @@ namespace osu.Game.Tests.Beatmaps.Formats using (var resStream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) using (var stream = new StreamReader(resStream)) { - var difficulty = decoder.DecodeBeatmap(stream).BeatmapInfo.BaseDifficulty; + var difficulty = decoder.Decode(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(1.8, difficulty.SliderMultiplier); Assert.AreEqual(2, difficulty.SliderTickRate); } } @@ -110,11 +111,11 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestDecodeBeatmapEvents() { - var decoder = new LegacyBeatmapDecoder(); + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; using (var resStream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) using (var stream = new StreamReader(resStream)) { - var beatmap = decoder.DecodeBeatmap(stream); + var beatmap = decoder.Decode(stream); var metadata = beatmap.Metadata; var breakPoint = beatmap.Breaks[0]; @@ -128,11 +129,11 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestDecodeBeatmapTimingPoints() { - var decoder = new LegacyBeatmapDecoder(); + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; using (var resStream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) using (var stream = new StreamReader(resStream)) { - var beatmap = decoder.DecodeBeatmap(stream); + var beatmap = decoder.Decode(stream); var controlPoints = beatmap.ControlPointInfo; Assert.AreEqual(4, controlPoints.TimingPoints.Count); @@ -163,11 +164,11 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestDecodeBeatmapColors() { - var decoder = new LegacyBeatmapDecoder(); + var decoder = new LegacySkinDecoder(); using (var resStream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) using (var stream = new StreamReader(resStream)) { - var comboColors = decoder.DecodeBeatmap(stream).ComboColors; + var comboColors = decoder.Decode(stream).ComboColours; Color4[] expectedColors = { @@ -187,11 +188,11 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestDecodeBeatmapHitObjects() { - var decoder = new LegacyBeatmapDecoder(); + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; using (var resStream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) using (var stream = new StreamReader(resStream)) { - var hitObjects = decoder.DecodeBeatmap(stream).HitObjects; + var hitObjects = decoder.Decode(stream).HitObjects; var curveData = hitObjects[0] as IHasCurve; var positionData = hitObjects[0] as IHasPosition; diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index dce6c0f55b..1c0801c634 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -18,11 +18,11 @@ namespace osu.Game.Tests.Beatmaps.Formats [Test] public void TestDecodeStoryboardEvents() { - var decoder = new LegacyBeatmapDecoder(); + var decoder = new LegacyStoryboardDecoder(); 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); + var storyboard = decoder.Decode(stream); Assert.IsTrue(storyboard.HasDrawable); Assert.AreEqual(4, storyboard.Layers.Count()); diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index 186bd44640..f37672b5cc 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -12,7 +12,6 @@ using osu.Game.IO.Serialization; using osu.Game.Rulesets.Objects.Types; using osu.Game.Tests.Resources; using OpenTK; -using OpenTK.Graphics; namespace osu.Game.Tests.Beatmaps.Formats { @@ -85,28 +84,10 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(4, difficulty.CircleSize); Assert.AreEqual(8, difficulty.OverallDifficulty); Assert.AreEqual(9, difficulty.ApproachRate); - Assert.AreEqual(1.8f, difficulty.SliderMultiplier); + Assert.AreEqual(1.8, difficulty.SliderMultiplier); Assert.AreEqual(2, difficulty.SliderTickRate); } - [Test] - public void TestDecodeColors() - { - var beatmap = decodeAsJson(normal); - 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() { @@ -159,7 +140,7 @@ namespace osu.Game.Tests.Beatmaps.Formats using (var sr = new StreamReader(stream)) { - var legacyDecoded = new LegacyBeatmapDecoder().DecodeBeatmap(sr); + var legacyDecoded = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr); using (var ms = new MemoryStream()) using (var sw = new StreamWriter(ms)) using (var sr2 = new StreamReader(ms)) @@ -168,7 +149,7 @@ namespace osu.Game.Tests.Beatmaps.Formats sw.Flush(); ms.Position = 0; - return (legacyDecoded, new JsonBeatmapDecoder().DecodeBeatmap(sr2)); + return (legacyDecoded, new JsonBeatmapDecoder().Decode(sr2)); } } } diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 0b49bc8bb9..6428881b54 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -26,19 +26,124 @@ namespace osu.Game.Tests.Beatmaps.IO //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWhenClosed")) { - var osu = loadOsu(host); + try + { + loadOszIntoOsu(loadOsu(host)); + } + finally + { + host.Exit(); + } + } + } - var temp = prepareTempCopy(osz_path); + [Test] + public void TestImportThenDelete() + { + //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenDelete")) + { + try + { + var osu = loadOsu(host); - Assert.IsTrue(File.Exists(temp)); + var imported = loadOszIntoOsu(osu); - osu.Dependencies.Get().Import(temp); + deleteBeatmapSet(imported, osu); + } + finally + { + host.Exit(); + } + } + } - ensureLoaded(osu); + [Test] + public void TestImportThenImport() + { + //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenImport")) + { + try + { + var osu = loadOsu(host); - waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); + var imported = loadOszIntoOsu(osu); + var importedSecondTime = loadOszIntoOsu(osu); - host.Exit(); + // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. + Assert.IsTrue(imported.ID == importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); + + var manager = osu.Dependencies.Get(); + + Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 1); + Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count == 1); + } + finally + { + host.Exit(); + } + } + } + + [Test] + public void TestImportThenImportDifferentHash() + { + //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenImportDifferentHash")) + { + try + { + var osu = loadOsu(host); + var manager = osu.Dependencies.Get(); + + var imported = loadOszIntoOsu(osu); + + //var change = manager.QueryBeatmapSets(_ => true).First(); + imported.Hash += "-changed"; + manager.Update(imported); + + var importedSecondTime = loadOszIntoOsu(osu); + + // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. + Assert.IsTrue(imported.ID != importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID < importedSecondTime.Beatmaps.First().ID); + + Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 1); + Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count == 1); + } + finally + { + host.Exit(); + } + } + } + + [Test] + public void TestImportThenDeleteThenImport() + { + //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenDeleteThenImport")) + { + try + { + var osu = loadOsu(host); + + var imported = loadOszIntoOsu(osu); + + deleteBeatmapSet(imported, osu); + + var importedSecondTime = loadOszIntoOsu(osu); + + // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. + Assert.IsTrue(imported.ID == importedSecondTime.ID); + Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); + } + finally + { + host.Exit(); + } } } @@ -50,24 +155,28 @@ namespace osu.Game.Tests.Beatmaps.IO using (HeadlessGameHost host = new CleanRunHeadlessGameHost("host", true)) using (HeadlessGameHost client = new CleanRunHeadlessGameHost("client", true)) { - Assert.IsTrue(host.IsPrimaryInstance); - Assert.IsFalse(client.IsPrimaryInstance); + try + { + Assert.IsTrue(host.IsPrimaryInstance); + Assert.IsFalse(client.IsPrimaryInstance); - var osu = loadOsu(host); + var osu = loadOsu(host); - var temp = prepareTempCopy(osz_path); + var temp = prepareTempCopy(osz_path); + Assert.IsTrue(File.Exists(temp)); - Assert.IsTrue(File.Exists(temp)); + var importer = new ArchiveImportIPCChannel(client); + if (!importer.ImportAsync(temp).Wait(10000)) + Assert.Fail(@"IPC took too long to send"); - var importer = new BeatmapIPCChannel(client); - if (!importer.ImportAsync(temp).Wait(10000)) - Assert.Fail(@"IPC took too long to send"); + ensureLoaded(osu); - ensureLoaded(osu); - - waitForOrAssert(() => !File.Exists(temp), "Temporary still exists after IPC import", 5000); - - host.Exit(); + waitForOrAssert(() => !File.Exists(temp), "Temporary still exists after IPC import", 5000); + } + finally + { + host.Exit(); + } } } @@ -76,25 +185,53 @@ namespace osu.Game.Tests.Beatmaps.IO { using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWhenFileOpen")) { - var osu = loadOsu(host); - - var temp = prepareTempCopy(osz_path); - - Assert.IsTrue(File.Exists(temp), "Temporary file copy never substantiated"); - - using (File.OpenRead(temp)) - osu.Dependencies.Get().Import(temp); - - ensureLoaded(osu); - - File.Delete(temp); - - Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't"); - - host.Exit(); + try + { + var osu = loadOsu(host); + var temp = prepareTempCopy(osz_path); + Assert.IsTrue(File.Exists(temp), "Temporary file copy never substantiated"); + using (File.OpenRead(temp)) + osu.Dependencies.Get().Import(temp); + ensureLoaded(osu); + File.Delete(temp); + Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't"); + } + finally + { + host.Exit(); + } } } + private BeatmapSetInfo loadOszIntoOsu(OsuGameBase osu) + { + var temp = prepareTempCopy(osz_path); + + Assert.IsTrue(File.Exists(temp)); + + var manager = osu.Dependencies.Get(); + + manager.Import(temp); + + var imported = manager.GetAllUsableBeatmapSets(); + + ensureLoaded(osu); + + waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); + + return imported.FirstOrDefault(); + } + + private void deleteBeatmapSet(BeatmapSetInfo imported, OsuGameBase osu) + { + var manager = osu.Dependencies.Get(); + manager.Delete(imported); + + Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 0); + Assert.IsTrue(manager.QueryBeatmapSets(_ => true).ToList().Count == 1); + Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); + } + private string prepareTempCopy(string path) { var temp = Path.GetTempFileName(); @@ -105,65 +242,55 @@ namespace osu.Game.Tests.Beatmaps.IO { var osu = new OsuGameBase(); Task.Run(() => host.Run(osu)); - waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); - return osu; } private void ensureLoaded(OsuGameBase osu, int timeout = 60000) { IEnumerable resultSets = null; - var store = osu.Dependencies.Get(); - waitForOrAssert(() => (resultSets = store.QueryBeatmapSets(s => s.OnlineBeatmapSetID == 241526)).Any(), @"BeatmapSet did not import to the database in allocated time.", timeout); //ensure we were stored to beatmap database backing... Assert.IsTrue(resultSets.Count() == 1, $@"Incorrect result count found ({resultSets.Count()} but should be 1)."); - IEnumerable queryBeatmaps() => store.QueryBeatmaps(s => s.BeatmapSet.OnlineBeatmapSetID == 241526 && s.BaseDifficultyID > 0); IEnumerable queryBeatmapSets() => store.QueryBeatmapSets(s => s.OnlineBeatmapSetID == 241526); //if we don't re-check here, the set will be inserted but the beatmaps won't be present yet. waitForOrAssert(() => queryBeatmaps().Count() == 12, @"Beatmaps did not import to the database in allocated time", timeout); - waitForOrAssert(() => queryBeatmapSets().Count() == 1, @"BeatmapSet did not import to the database in allocated time", timeout); - int countBeatmapSetBeatmaps = 0; int countBeatmaps = 0; - waitForOrAssert(() => - (countBeatmapSetBeatmaps = queryBeatmapSets().First().Beatmaps.Count) == - (countBeatmaps = queryBeatmaps().Count()), + (countBeatmapSetBeatmaps = queryBeatmapSets().First().Beatmaps.Count) == + (countBeatmaps = queryBeatmaps().Count()), $@"Incorrect database beatmap count post-import ({countBeatmaps} but should be {countBeatmapSetBeatmaps}).", timeout); var set = queryBeatmapSets().First(); - foreach (BeatmapInfo b in set.Beatmaps) Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineBeatmapID == b.OnlineBeatmapID)); - Assert.IsTrue(set.Beatmaps.Count > 0); - var beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 0))?.Beatmap; Assert.IsTrue(beatmap?.HitObjects.Count > 0); - beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 1))?.Beatmap; Assert.IsTrue(beatmap?.HitObjects.Count > 0); - beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 2))?.Beatmap; Assert.IsTrue(beatmap?.HitObjects.Count > 0); - beatmap = store.GetWorkingBeatmap(set.Beatmaps.First(b => b.RulesetID == 3))?.Beatmap; Assert.IsTrue(beatmap?.HitObjects.Count > 0); } private void waitForOrAssert(Func result, string failureMessage, int timeout = 60000) { - Action waitAction = () => { while (!result()) Thread.Sleep(200); }; + Action waitAction = () => + { + while (!result()) Thread.Sleep(200); + }; + Assert.IsTrue(waitAction.BeginInvoke(null, null).AsyncWaitHandle.WaitOne(timeout), failureMessage); } } diff --git a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs index 44eb385e22..29d25accbb 100644 --- a/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/OszArchiveReaderTest.cs @@ -5,9 +5,9 @@ using System.IO; using System.Linq; using NUnit.Framework; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.IO; using osu.Game.Tests.Resources; using osu.Game.Beatmaps.Formats; +using osu.Game.IO.Archives; namespace osu.Game.Tests.Beatmaps.IO { @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Beatmaps.IO { using (var osz = Resource.OpenResource("Beatmaps.241526 Soleily - Renatus.osz")) { - var reader = new OszArchiveReader(osz); + var reader = new ZipArchiveReader(osz); string[] expected = { "Soleily - Renatus (Deif) [Platter].osu", @@ -46,11 +46,11 @@ namespace osu.Game.Tests.Beatmaps.IO { using (var osz = Resource.OpenResource("Beatmaps.241526 Soleily - Renatus.osz")) { - var reader = new OszArchiveReader(osz); + var reader = new ZipArchiveReader(osz); BeatmapMetadata meta; using (var stream = new StreamReader(reader.GetStream("Soleily - Renatus (Deif) [Platter].osu"))) - meta = Decoder.GetDecoder(stream).DecodeBeatmap(stream).Metadata; + meta = Decoder.GetDecoder(stream).Decode(stream).Metadata; Assert.AreEqual(241526, meta.OnlineBeatmapSetID); Assert.AreEqual("Soleily", meta.Artist); @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Beatmaps.IO Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", meta.AudioFile); Assert.AreEqual("Deif", meta.AuthorString); Assert.AreEqual("machinetop_background.jpg", meta.BackgroundFile); - Assert.AreEqual(164471, meta.PreviewTime); + Assert.AreEqual(164471 + LegacyBeatmapDecoder.UniversalOffset, meta.PreviewTime); Assert.AreEqual(string.Empty, meta.Source); Assert.AreEqual("MBC7 Unisphere 地球ヤバイEP Chikyu Yabai", meta.Tags); Assert.AreEqual("Renatus", meta.Title); @@ -71,7 +71,7 @@ namespace osu.Game.Tests.Beatmaps.IO { using (var osz = Resource.OpenResource("Beatmaps.241526 Soleily - Renatus.osz")) { - var reader = new OszArchiveReader(osz); + var reader = new ZipArchiveReader(osz); using (var stream = new StreamReader( reader.GetStream("Soleily - Renatus (Deif) [Platter].osu"))) { diff --git a/osu.Game.Tests/Visual/TestCaseAllPlayers.cs b/osu.Game.Tests/Visual/TestCaseAllPlayers.cs index 912dbc4056..e633d121ca 100644 --- a/osu.Game.Tests/Visual/TestCaseAllPlayers.cs +++ b/osu.Game.Tests/Visual/TestCaseAllPlayers.cs @@ -1,8 +1,11 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; + namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseAllPlayers : TestCasePlayer { } diff --git a/osu.Game.Tests/Visual/TestCaseBeatDivisorControl.cs b/osu.Game.Tests/Visual/TestCaseBeatDivisorControl.cs new file mode 100644 index 0000000000..04a662426f --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseBeatDivisorControl.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Screens.Edit.Screens.Compose; +using OpenTK; + +namespace osu.Game.Tests.Visual +{ + public class TestCaseBeatDivisorControl : OsuTestCase + { + public override IReadOnlyList RequiredTypes => new[] { typeof(BindableBeatDivisor) }; + + [BackgroundDependencyLoader] + private void load() + { + Child = new BeatDivisorControl(new BindableBeatDivisor()) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(90, 90) + }; + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseBeatSyncedContainer.cs b/osu.Game.Tests/Visual/TestCaseBeatSyncedContainer.cs index f081d090c8..66cee634f5 100644 --- a/osu.Game.Tests/Visual/TestCaseBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/TestCaseBeatSyncedContainer.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using NUnit.Framework; using osu.Framework.Audio.Track; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -17,6 +18,7 @@ using osu.Framework.Lists; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseBeatSyncedContainer : OsuTestCase { private readonly MusicController mc; diff --git a/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs b/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs index 4a65d12977..c68e548f44 100644 --- a/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/TestCaseBeatmapCarousel.cs @@ -6,20 +6,24 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Rulesets; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Filter; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseBeatmapCarousel : OsuTestCase { private TestBeatmapCarousel carousel; + private RulesetStore rulesets; public override IReadOnlyList RequiredTypes => new[] { @@ -44,8 +48,10 @@ namespace osu.Game.Tests.Visual private const int set_count = 5; [BackgroundDependencyLoader] - private void load() + private void load(RulesetStore rulesets) { + this.rulesets = rulesets; + Add(carousel = new TestBeatmapCarousel { RelativeSizeAxes = Axes.Both, @@ -60,7 +66,9 @@ namespace osu.Game.Tests.Visual AddStep("Load Beatmaps", () => { carousel.BeatmapSets = beatmapSets; }); - AddUntilStep(() => carousel.BeatmapSets.Any(), "Wait for load"); + bool changed = false; + carousel.BeatmapSetsChanged = () => changed = true; + AddUntilStep(() => changed, "Wait for load"); testTraversal(); testFiltering(); @@ -71,6 +79,7 @@ namespace osu.Game.Tests.Visual testRemoveAll(); testEmptyTraversal(); testHiding(); + testSelectingFilteredRuleset(); } private void ensureRandomFetchSuccess() => @@ -205,6 +214,12 @@ namespace osu.Game.Tests.Visual checkVisibleItemCount(true, 0); AddAssert("Selection is null", () => currentSelection == null); + advanceSelection(true); + AddAssert("Selection is null", () => currentSelection == null); + + advanceSelection(false); + AddAssert("Selection is null", () => currentSelection == null); + AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false)); AddAssert("Selection is non-null", () => currentSelection != null); @@ -353,6 +368,41 @@ namespace osu.Game.Tests.Visual } } + private void testSelectingFilteredRuleset() + { + var testMixed = createTestBeatmapSet(set_count + 1); + AddStep("add mixed ruleset beatmapset", () => + { + for (int i = 0; i <= 2; i++) + { + testMixed.Beatmaps[i].Ruleset = rulesets.AvailableRulesets.ElementAt(i); + testMixed.Beatmaps[i].RulesetID = i; + } + + carousel.UpdateBeatmapSet(testMixed); + }); + AddStep("filter to ruleset 0", () => + carousel.Filter(new FilterCriteria { Ruleset = rulesets.AvailableRulesets.ElementAt(0) }, false)); + AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testMixed.Beatmaps[1], false)); + AddAssert("unfiltered beatmap selected", () => carousel.SelectedBeatmap.Equals(testMixed.Beatmaps[0])); + + AddStep("remove mixed set", () => + { + carousel.RemoveBeatmapSet(testMixed); + testMixed = null; + }); + var testSingle = createTestBeatmapSet(set_count + 2); + testSingle.Beatmaps.ForEach(b => + { + b.Ruleset = rulesets.AvailableRulesets.ElementAt(1); + b.RulesetID = b.Ruleset.ID ?? 1; + }); + AddStep("add single ruleset beatmapset", () => carousel.UpdateBeatmapSet(testSingle)); + AddStep("select filtered map skipping filtered", () => carousel.SelectBeatmap(testSingle.Beatmaps[0], false)); + checkNoSelection(); + AddStep("remove single ruleset set", () => carousel.RemoveBeatmapSet(testSingle)); + } + private BeatmapSetInfo createTestBeatmapSet(int id) { return new BeatmapSetInfo diff --git a/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs index bde071c4a3..3ccdaa90d9 100644 --- a/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/TestCaseBeatmapInfoWedge.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using NUnit.Framework; using OpenTK; using osu.Framework.Allocation; using osu.Framework.Configuration; @@ -18,6 +19,7 @@ using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseBeatmapInfoWedge : OsuTestCase { private RulesetStore rulesets; diff --git a/osu.Game.Tests/Visual/TestCaseBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/TestCaseBeatmapSetOverlay.cs index ad85b3ed52..d9aedb7a5f 100644 --- a/osu.Game.Tests/Visual/TestCaseBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/TestCaseBeatmapSetOverlay.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Overlays; @@ -12,6 +13,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseBeatmapSetOverlay : OsuTestCase { private readonly BeatmapSetOverlay overlay; diff --git a/osu.Game.Tests/Visual/TestCaseBreadcrumbs.cs b/osu.Game.Tests/Visual/TestCaseBreadcrumbs.cs index 34abef7d76..20bdd6736c 100644 --- a/osu.Game.Tests/Visual/TestCaseBreadcrumbs.cs +++ b/osu.Game.Tests/Visual/TestCaseBreadcrumbs.cs @@ -1,11 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseBreadcrumbs : OsuTestCase { public TestCaseBreadcrumbs() diff --git a/osu.Game.Tests/Visual/TestCaseBreakOverlay.cs b/osu.Game.Tests/Visual/TestCaseBreakOverlay.cs index f9ed606080..51b8c61963 100644 --- a/osu.Game.Tests/Visual/TestCaseBreakOverlay.cs +++ b/osu.Game.Tests/Visual/TestCaseBreakOverlay.cs @@ -3,11 +3,13 @@ using osu.Framework.Timing; using osu.Game.Beatmaps.Timing; -using osu.Game.Screens.Play.BreaksOverlay; using System.Collections.Generic; +using NUnit.Framework; +using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseBreakOverlay : OsuTestCase { private readonly BreakOverlay breakOverlay; diff --git a/osu.Game.Tests/Visual/TestCaseButtonSystem.cs b/osu.Game.Tests/Visual/TestCaseButtonSystem.cs index 61da76970e..93740593cb 100644 --- a/osu.Game.Tests/Visual/TestCaseButtonSystem.cs +++ b/osu.Game.Tests/Visual/TestCaseButtonSystem.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Shapes; @@ -9,6 +10,7 @@ using OpenTK.Graphics; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseButtonSystem : OsuTestCase { public TestCaseButtonSystem() diff --git a/osu.Game.Tests/Visual/TestCaseChatLink.cs b/osu.Game.Tests/Visual/TestCaseChatLink.cs index 3a7be686e1..786fcb64ab 100644 --- a/osu.Game.Tests/Visual/TestCaseChatLink.cs +++ b/osu.Game.Tests/Visual/TestCaseChatLink.cs @@ -12,12 +12,14 @@ using osu.Game.Users; using System; using System.Collections.Generic; using System.Linq; +using NUnit.Framework; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseChatLink : OsuTestCase { private readonly TestChatLineContainer textContainer; diff --git a/osu.Game.Tests/Visual/TestCaseContextMenu.cs b/osu.Game.Tests/Visual/TestCaseContextMenu.cs index 6098187dd6..45c12cf4af 100644 --- a/osu.Game.Tests/Visual/TestCaseContextMenu.cs +++ b/osu.Game.Tests/Visual/TestCaseContextMenu.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -13,6 +14,7 @@ using osu.Game.Graphics.Cursor; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseContextMenu : OsuTestCase { private const int start_time = 0; diff --git a/osu.Game.Tests/Visual/TestCaseCursors.cs b/osu.Game.Tests/Visual/TestCaseCursors.cs index 363f6b53f0..72e699c54b 100644 --- a/osu.Game.Tests/Visual/TestCaseCursors.cs +++ b/osu.Game.Tests/Visual/TestCaseCursors.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -16,6 +17,7 @@ using OpenTK.Graphics; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseCursors : OsuTestCase { private readonly ManualInputManager inputManager; diff --git a/osu.Game.Tests/Visual/TestCaseDialogOverlay.cs b/osu.Game.Tests/Visual/TestCaseDialogOverlay.cs index d7fbf64664..e9512b29f7 100644 --- a/osu.Game.Tests/Visual/TestCaseDialogOverlay.cs +++ b/osu.Game.Tests/Visual/TestCaseDialogOverlay.cs @@ -1,12 +1,14 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Game.Graphics; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseDialogOverlay : OsuTestCase { public TestCaseDialogOverlay() diff --git a/osu.Game.Tests/Visual/TestCaseDirect.cs b/osu.Game.Tests/Visual/TestCaseDirect.cs index 8fa576135e..3f3dbb0bca 100644 --- a/osu.Game.Tests/Visual/TestCaseDirect.cs +++ b/osu.Game.Tests/Visual/TestCaseDirect.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Overlays; @@ -9,6 +10,7 @@ using osu.Game.Rulesets; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseDirect : OsuTestCase { private DirectOverlay direct; diff --git a/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs b/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs index ec70253118..4268fd305e 100644 --- a/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs +++ b/osu.Game.Tests/Visual/TestCaseDrawableRoom.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -12,6 +13,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseDrawableRoom : OsuTestCase { private RulesetStore rulesets; diff --git a/osu.Game.Tests/Visual/TestCaseEditor.cs b/osu.Game.Tests/Visual/TestCaseEditor.cs deleted file mode 100644 index 37da41c228..0000000000 --- a/osu.Game.Tests/Visual/TestCaseEditor.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using osu.Framework.Allocation; -using osu.Game.Beatmaps; -using osu.Game.Screens.Edit; -using osu.Game.Screens.Edit.Screens; - -namespace osu.Game.Tests.Visual -{ - public class TestCaseEditor : OsuTestCase - { - public override IReadOnlyList RequiredTypes => new[] { typeof(Editor), typeof(EditorScreen) }; - - private readonly Random rng; - - private BeatmapManager beatmaps; - private OsuGameBase osuGame; - - public TestCaseEditor() - { - rng = new Random(1337); - - Add(new Editor()); - AddStep("Next beatmap", nextBeatmap); - } - - [BackgroundDependencyLoader] - private void load(OsuGameBase osuGame, BeatmapManager beatmaps) - { - this.osuGame = osuGame; - this.beatmaps = beatmaps; - } - - private void nextBeatmap() - { - var sets = beatmaps.GetAllUsableBeatmapSets(); - if (sets.Count == 0) - return; - - BeatmapInfo info = sets[rng.Next(0, sets.Count)].Beatmaps[0]; - osuGame.Beatmap.Value = beatmaps.GetWorkingBeatmap(info); - } - } -} diff --git a/osu.Game.Tests/Visual/TestCaseEditorCompose.cs b/osu.Game.Tests/Visual/TestCaseEditorCompose.cs index 76771ecf82..cd25bc1683 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorCompose.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorCompose.cs @@ -2,45 +2,39 @@ // 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.Game.Beatmaps; +using osu.Framework.Timing; +using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit.Screens.Compose; +using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseEditorCompose : OsuTestCase { - private readonly Random random; - private readonly Compose compose; + public override IReadOnlyList RequiredTypes => new[] { typeof(Compose) }; - public TestCaseEditorCompose() - { - random = new Random(1337); + private DependencyContainer dependencies; - Add(compose = new Compose()); - AddStep("Next beatmap", nextBeatmap); - } - - private OsuGameBase osuGame; - private BeatmapManager beatmaps; + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) + => dependencies = new DependencyContainer(parent); [BackgroundDependencyLoader] - private void load(OsuGameBase osuGame, BeatmapManager beatmaps) + private void load(OsuGameBase osuGame) { - this.osuGame = osuGame; - this.beatmaps = beatmaps; + osuGame.Beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo); + var clock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; + dependencies.CacheAs(clock); + dependencies.CacheAs(clock); + + var compose = new Compose(); 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); + Child = compose; } } } diff --git a/osu.Game.Tests/Visual/TestCaseEditorComposeRadioButtons.cs b/osu.Game.Tests/Visual/TestCaseEditorComposeRadioButtons.cs index 8717f15311..d9850139cd 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorComposeRadioButtons.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorComposeRadioButtons.cs @@ -3,11 +3,13 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Screens.Edit.Screens.Compose.RadioButtons; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseEditorComposeRadioButtons : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] { typeof(DrawableRadioButton) }; diff --git a/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs b/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs index 6a47933a3c..d15ee32d8d 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorComposeTimeline.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using OpenTK; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -12,6 +13,7 @@ using osu.Game.Screens.Edit.Screens.Compose.Timeline; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseEditorComposeTimeline : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] { typeof(ScrollableTimeline), typeof(ScrollingTimelineContainer), typeof(BeatmapWaveformGraph), typeof(TimelineButton) }; diff --git a/osu.Game.Tests/Visual/TestCaseEditorMenuBar.cs b/osu.Game.Tests/Visual/TestCaseEditorMenuBar.cs index edfcde22b3..ee98fa087a 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorMenuBar.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorMenuBar.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; @@ -11,6 +12,7 @@ using osu.Game.Screens.Edit.Menus; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseEditorMenuBar : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] { typeof(EditorMenuBar), typeof(ScreenSelectionTabControl) }; diff --git a/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs b/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs new file mode 100644 index 0000000000..e9e966a826 --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseEditorSeekSnapping.cs @@ -0,0 +1,468 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit.Screens.Compose; +using osu.Game.Tests.Beatmaps; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Tests.Visual +{ + public class TestCaseEditorSeekSnapping : OsuTestCase + { + public override IReadOnlyList RequiredTypes => new[] { typeof(HitObjectComposer) }; + + private Track track; + private HitObjectComposer composer; + + private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(4); + + private DecoupleableInterpolatingFramedClock clock; + + private DependencyContainer dependencies; + + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) + => dependencies = new DependencyContainer(parent); + + [BackgroundDependencyLoader] + private void load(OsuGameBase osuGame) + { + clock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; + dependencies.CacheAs(clock); + dependencies.CacheAs(clock); + dependencies.Cache(beatDivisor); + + var testBeatmap = new Beatmap + { + ControlPointInfo = new ControlPointInfo + { + TimingPoints = + { + new TimingControlPoint { Time = 0, BeatLength = 200}, + new TimingControlPoint { Time = 100, BeatLength = 400 }, + new TimingControlPoint { Time = 175, BeatLength = 800 }, + new TimingControlPoint { Time = 350, BeatLength = 200 }, + new TimingControlPoint { Time = 450, BeatLength = 100 }, + new TimingControlPoint { Time = 500, BeatLength = 307.69230769230802 } + } + }, + HitObjects = + { + new HitCircle { StartTime = 0 }, + new HitCircle { StartTime = 5000 } + } + }; + + osuGame.Beatmap.Value = new TestWorkingBeatmap(testBeatmap); + track = osuGame.Beatmap.Value.Track; + + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] { composer = new TestHitObjectComposer(new OsuRuleset()) }, + new Drawable[] { new TimingPointVisualiser(testBeatmap, track) { Clock = clock } }, + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Distributed), + new Dimension(GridSizeMode.AutoSize), + } + }; + + testSeekNoSnapping(); + testSeekSnappingOnBeat(); + testSeekSnappingInBetweenBeat(); + testSeekForwardNoSnapping(); + testSeekForwardSnappingOnBeat(); + testSeekForwardSnappingFromInBetweenBeat(); + testSeekBackwardSnappingOnBeat(); + testSeekBackwardSnappingFromInBetweenBeat(); + testSeekingWithFloatingPointBeatLength(); + } + + /// + /// Tests whether time is correctly seeked without snapping. + /// + private void testSeekNoSnapping() + { + reset(); + + // Forwards + AddStep("Seek(0)", () => composer.SeekTo(0)); + AddAssert("Time = 0", () => clock.CurrentTime == 0); + AddStep("Seek(33)", () => composer.SeekTo(33)); + AddAssert("Time = 33", () => clock.CurrentTime == 33); + AddStep("Seek(89)", () => composer.SeekTo(89)); + AddAssert("Time = 89", () => clock.CurrentTime == 89); + + // Backwards + AddStep("Seek(25)", () => composer.SeekTo(25)); + AddAssert("Time = 25", () => clock.CurrentTime == 25); + AddStep("Seek(0)", () => composer.SeekTo(0)); + AddAssert("Time = 0", () => clock.CurrentTime == 0); + } + + /// + /// Tests whether seeking to exact beat times puts us on the beat time. + /// These are the white/yellow ticks on the graph. + /// + private void testSeekSnappingOnBeat() + { + reset(); + + AddStep("Seek(0), Snap", () => composer.SeekTo(0, true)); + AddAssert("Time = 0", () => clock.CurrentTime == 0); + AddStep("Seek(50), Snap", () => composer.SeekTo(50, true)); + AddAssert("Time = 50", () => clock.CurrentTime == 50); + AddStep("Seek(100), Snap", () => composer.SeekTo(100, true)); + AddAssert("Time = 100", () => clock.CurrentTime == 100); + AddStep("Seek(175), Snap", () => composer.SeekTo(175, true)); + AddAssert("Time = 175", () => clock.CurrentTime == 175); + AddStep("Seek(350), Snap", () => composer.SeekTo(350, true)); + AddAssert("Time = 350", () => clock.CurrentTime == 350); + AddStep("Seek(400), Snap", () => composer.SeekTo(400, true)); + AddAssert("Time = 400", () => clock.CurrentTime == 400); + AddStep("Seek(450), Snap", () => composer.SeekTo(450, true)); + AddAssert("Time = 450", () => clock.CurrentTime == 450); + } + + /// + /// Tests whether seeking to somewhere in the middle between beats puts us on the expected beats. + /// For example, snapping between a white/yellow beat should put us on either the yellow or white, depending on which one we're closer too. + /// If + /// + private void testSeekSnappingInBetweenBeat() + { + reset(); + + AddStep("Seek(24), Snap", () => composer.SeekTo(24, true)); + AddAssert("Time = 0", () => clock.CurrentTime == 0); + AddStep("Seek(26), Snap", () => composer.SeekTo(26, true)); + AddAssert("Time = 50", () => clock.CurrentTime == 50); + AddStep("Seek(150), Snap", () => composer.SeekTo(150, true)); + AddAssert("Time = 100", () => clock.CurrentTime == 100); + AddStep("Seek(170), Snap", () => composer.SeekTo(170, true)); + AddAssert("Time = 175", () => clock.CurrentTime == 175); + AddStep("Seek(274), Snap", () => composer.SeekTo(274, true)); + AddAssert("Time = 175", () => clock.CurrentTime == 175); + AddStep("Seek(276), Snap", () => composer.SeekTo(276, true)); + AddAssert("Time = 350", () => clock.CurrentTime == 350); + } + + /// + /// Tests that when seeking forward with no beat snapping, beats are never explicitly snapped to, nor the next timing point (if we've skipped it). + /// + private void testSeekForwardNoSnapping() + { + reset(); + + AddStep("SeekForward", () => composer.SeekForward()); + AddAssert("Time = 50", () => clock.CurrentTime == 50); + AddStep("SeekForward", () => composer.SeekForward()); + AddAssert("Time = 100", () => clock.CurrentTime == 100); + AddStep("SeekForward", () => composer.SeekForward()); + AddAssert("Time = 200", () => clock.CurrentTime == 200); + AddStep("SeekForward", () => composer.SeekForward()); + AddAssert("Time = 400", () => clock.CurrentTime == 400); + AddStep("SeekForward", () => composer.SeekForward()); + AddAssert("Time = 450", () => clock.CurrentTime == 450); + } + + /// + /// Tests that when seeking forward with beat snapping, all beats are snapped to and timing points are never skipped. + /// + private void testSeekForwardSnappingOnBeat() + { + reset(); + + AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddAssert("Time = 50", () => clock.CurrentTime == 50); + AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddAssert("Time = 100", () => clock.CurrentTime == 100); + AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddAssert("Time = 175", () => clock.CurrentTime == 175); + AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddAssert("Time = 350", () => clock.CurrentTime == 350); + AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddAssert("Time = 400", () => clock.CurrentTime == 400); + AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddAssert("Time = 450", () => clock.CurrentTime == 450); + } + + /// + /// Tests that when seeking forward from in-between two beats, the next beat or timing point is snapped to, and no beats are skipped. + /// This will also test being extremely close to the next beat/timing point, to ensure rounding is not an issue. + /// + private void testSeekForwardSnappingFromInBetweenBeat() + { + reset(); + + AddStep("Seek(49)", () => composer.SeekTo(49)); + AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddAssert("Time = 50", () => clock.CurrentTime == 50); + AddStep("Seek(49.999)", () => composer.SeekTo(49.999)); + AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddAssert("Time = 50", () => clock.CurrentTime == 50); + AddStep("Seek(99)", () => composer.SeekTo(99)); + AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddAssert("Time = 100", () => clock.CurrentTime == 100); + AddStep("Seek(99.999)", () => composer.SeekTo(99.999)); + AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddAssert("Time = 100", () => clock.CurrentTime == 100); + AddStep("Seek(174)", () => composer.SeekTo(174)); + AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddAssert("Time = 175", () => clock.CurrentTime == 175); + AddStep("Seek(349)", () => composer.SeekTo(349)); + AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddAssert("Time = 350", () => clock.CurrentTime == 350); + AddStep("Seek(399)", () => composer.SeekTo(399)); + AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddAssert("Time = 400", () => clock.CurrentTime == 400); + AddStep("Seek(449)", () => composer.SeekTo(449)); + AddStep("SeekForward, Snap", () => composer.SeekForward(true)); + AddAssert("Time = 450", () => clock.CurrentTime == 450); + } + + /// + /// Tests that when seeking backward with no beat snapping, beats are never explicitly snapped to, nor the next timing point (if we've skipped it). + /// + private void testSeekBackwardNoSnapping() + { + reset(); + + AddStep("Seek(450)", () => composer.SeekTo(450)); + AddStep("SeekBackward", () => composer.SeekBackward()); + AddAssert("Time = 425", () => clock.CurrentTime == 425); + AddStep("SeekBackward", () => composer.SeekBackward()); + AddAssert("Time = 375", () => clock.CurrentTime == 375); + AddStep("SeekBackward", () => composer.SeekBackward()); + AddAssert("Time = 325", () => clock.CurrentTime == 325); + AddStep("SeekBackward", () => composer.SeekBackward()); + AddAssert("Time = 125", () => clock.CurrentTime == 125); + AddStep("SeekBackward", () => composer.SeekBackward()); + AddAssert("Time = 25", () => clock.CurrentTime == 25); + AddStep("SeekBackward", () => composer.SeekBackward()); + AddAssert("Time = 0", () => clock.CurrentTime == 0); + } + + /// + /// Tests that when seeking backward with beat snapping, all beats are snapped to and timing points are never skipped. + /// + private void testSeekBackwardSnappingOnBeat() + { + reset(); + + AddStep("Seek(450)", () => composer.SeekTo(450)); + AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); + AddAssert("Time = 400", () => clock.CurrentTime == 400); + AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); + AddAssert("Time = 350", () => clock.CurrentTime == 350); + AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); + AddAssert("Time = 175", () => clock.CurrentTime == 175); + AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); + AddAssert("Time = 100", () => clock.CurrentTime == 100); + AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); + AddAssert("Time = 50", () => clock.CurrentTime == 50); + AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); + AddAssert("Time = 0", () => clock.CurrentTime == 0); + } + + /// + /// Tests that when seeking backward from in-between two beats, the previous beat or timing point is snapped to, and no beats are skipped. + /// This will also test being extremely close to the previous beat/timing point, to ensure rounding is not an issue. + /// + private void testSeekBackwardSnappingFromInBetweenBeat() + { + reset(); + + AddStep("Seek(451)", () => composer.SeekTo(451)); + AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); + AddAssert("Time = 450", () => clock.CurrentTime == 450); + AddStep("Seek(450.999)", () => composer.SeekTo(450.999)); + AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); + AddAssert("Time = 450", () => clock.CurrentTime == 450); + AddStep("Seek(401)", () => composer.SeekTo(401)); + AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); + AddAssert("Time = 400", () => clock.CurrentTime == 400); + AddStep("Seek(401.999)", () => composer.SeekTo(401.999)); + AddStep("SeekBackward, Snap", () => composer.SeekBackward(true)); + AddAssert("Time = 400", () => clock.CurrentTime == 400); + } + + /// + /// Tests that there are no rounding issues when snapping to beats within a timing point with a floating-point beatlength. + /// + private void testSeekingWithFloatingPointBeatLength() + { + reset(); + + double lastTime = 0; + + AddStep("Seek(0)", () => composer.SeekTo(0)); + + for (int i = 0; i < 20; i++) + { + AddStep("SeekForward, Snap", () => + { + lastTime = clock.CurrentTime; + composer.SeekForward(true); + }); + AddAssert("Time > lastTime", () => clock.CurrentTime > lastTime); + } + + for (int i = 0; i < 20; i++) + { + AddStep("SeekBackward, Snap", () => + { + lastTime = clock.CurrentTime; + composer.SeekBackward(true); + }); + AddAssert("Time < lastTime", () => clock.CurrentTime < lastTime); + } + + AddAssert("Time = 0", () => clock.CurrentTime == 0); + } + + private void reset() + { + AddStep("Reset", () => composer.SeekTo(0)); + } + + private class TestHitObjectComposer : HitObjectComposer + { + public TestHitObjectComposer(Ruleset ruleset) + : base(ruleset) + { + } + + protected override IReadOnlyList CompositionTools => new ICompositionTool[0]; + } + + private class TimingPointVisualiser : CompositeDrawable + { + private readonly Track track; + + private readonly Drawable tracker; + + public TimingPointVisualiser(Beatmap beatmap, Track track) + { + this.track = track; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Width = 0.75f; + + FillFlowContainer timelineContainer; + + InternalChildren = new Drawable[] + { + new Box + { + Name = "Background", + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(85f) + }, + new Container + { + Name = "Tracks", + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(15), + Children = new[] + { + tracker = new Box + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + RelativePositionAxes = Axes.X, + Width = 2, + Colour = Color4.Red, + }, + timelineContainer = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0, 5) + }, + } + } + }; + + var timingPoints = beatmap.ControlPointInfo.TimingPoints; + + for (int i = 0; i < timingPoints.Count; i++) + { + TimingControlPoint next = i == timingPoints.Count - 1 ? null : timingPoints[i + 1]; + timelineContainer.Add(new TimingPointTimeline(timingPoints[i], next?.Time ?? beatmap.HitObjects.Last().StartTime, track.Length)); + } + } + + protected override void Update() + { + base.Update(); + + tracker.X = (float)(Time.Current / track.Length); + } + + private class TimingPointTimeline : CompositeDrawable + { + public TimingPointTimeline(TimingControlPoint timingPoint, double endTime, double fullDuration) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Box createMainTick(double time) => new Box + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomCentre, + RelativePositionAxes = Axes.X, + X = (float)(time / fullDuration), + Height = 10, + Width = 2 + }; + + Box createBeatTick(double time) => new Box + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomCentre, + RelativePositionAxes = Axes.X, + X = (float)(time / fullDuration), + Height = 5, + Width = 2, + Colour = time > endTime ? Color4.Gray : Color4.Yellow + }; + + AddInternal(createMainTick(timingPoint.Time)); + AddInternal(createMainTick(endTime)); + + for (double t = timingPoint.Time + timingPoint.BeatLength / 4; t < fullDuration; t += timingPoint.BeatLength / 4) + AddInternal(createBeatTick(t)); + } + } + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseEditorSelectionLayer.cs b/osu.Game.Tests/Visual/TestCaseEditorSelectionLayer.cs index 755800c4e1..bbbfef477a 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorSelectionLayer.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorSelectionLayer.cs @@ -3,63 +3,73 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Allocation; -using OpenTK; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Timing; +using OpenTK; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets.Edit.Layers.Selection; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Edit; +using osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Screens.Edit.Screens.Compose.Layers; +using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseEditorSelectionLayer : OsuTestCase { - public override IReadOnlyList RequiredTypes => new[] { typeof(SelectionLayer) }; + public override IReadOnlyList RequiredTypes => new[] + { + typeof(SelectionLayer), + typeof(SelectionBox), + typeof(HitObjectComposer), + typeof(OsuHitObjectComposer), + typeof(HitObjectMaskLayer), + typeof(HitObjectMask), + typeof(HitCircleMask), + typeof(SliderMask), + typeof(SliderCircleMask) + }; + + private DependencyContainer dependencies; + + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) + => dependencies = new DependencyContainer(parent); [BackgroundDependencyLoader] - private void load() + private void load(OsuGameBase osuGame) { - var playfield = new OsuEditPlayfield(); - - Children = new Drawable[] + osuGame.Beatmap.Value = new TestWorkingBeatmap(new Beatmap { - new Container + HitObjects = new List { - RelativeSizeAxes = Axes.Both, - Clock = new FramedClock(new StopwatchClock()), - Child = playfield + new HitCircle { Position = new Vector2(256, 192), Scale = 0.5f }, + new HitCircle { Position = new Vector2(344, 148), Scale = 0.5f }, + new Slider + { + Position = new Vector2(128, 256), + ControlPoints = new List + { + Vector2.Zero, + new Vector2(216, 0), + }, + Distance = 400, + Velocity = 1, + TickDistance = 100, + Scale = 0.5f, + } }, - new SelectionLayer(playfield) - }; + }); - var hitCircle1 = new HitCircle { Position = new Vector2(256, 192), Scale = 0.5f }; - var hitCircle2 = new HitCircle { Position = new Vector2(344, 148), Scale = 0.5f }; - var slider = new Slider - { - ControlPoints = new List - { - new Vector2(128, 256), - new Vector2(344, 256), - }, - Distance = 400, - Position = new Vector2(128, 256), - Velocity = 1, - TickDistance = 100, - Scale = 0.5f, - }; + var clock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; + dependencies.CacheAs(clock); + dependencies.CacheAs(clock); - hitCircle1.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - hitCircle2.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - - playfield.Add(new DrawableHitCircle(hitCircle1)); - playfield.Add(new DrawableHitCircle(hitCircle2)); - playfield.Add(new DrawableSlider(slider)); + Child = new OsuHitObjectComposer(new OsuRuleset()); } } } diff --git a/osu.Game.Tests/Visual/TestCaseEditorSummaryTimeline.cs b/osu.Game.Tests/Visual/TestCaseEditorSummaryTimeline.cs index 8c8699fffa..bbe2956c5d 100644 --- a/osu.Game.Tests/Visual/TestCaseEditorSummaryTimeline.cs +++ b/osu.Game.Tests/Visual/TestCaseEditorSummaryTimeline.cs @@ -3,29 +3,39 @@ using System; using System.Collections.Generic; -using osu.Framework.Audio.Track; -using osu.Framework.Graphics.Textures; +using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using OpenTK; using osu.Game.Screens.Edit.Components.Timelines.Summary; using osu.Framework.Configuration; +using osu.Framework.Timing; +using osu.Game.Rulesets.Osu; +using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseEditorSummaryTimeline : OsuTestCase { - private const int length = 60000; - private readonly Random random; - public override IReadOnlyList RequiredTypes => new[] { typeof(SummaryTimeline) }; private readonly Bindable beatmap = new Bindable(); - public TestCaseEditorSummaryTimeline() + private DependencyContainer dependencies; + + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) + => dependencies = new DependencyContainer(parent); + + [BackgroundDependencyLoader] + private void load() { - random = new Random(1337); + beatmap.Value = new TestWorkingBeatmap(new OsuRuleset().RulesetInfo); + + var clock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; + dependencies.CacheAs(clock); + dependencies.CacheAs(clock); SummaryTimeline summaryTimeline; Add(summaryTimeline = new SummaryTimeline @@ -36,58 +46,6 @@ namespace osu.Game.Tests.Visual }); summaryTimeline.Beatmap.BindTo(beatmap); - - AddStep("New beatmap", newBeatmap); - - newBeatmap(); - } - - private void newBeatmap() - { - var b = new Beatmap(); - - for (int i = 0; i < random.Next(1, 10); i++) - b.ControlPointInfo.TimingPoints.Add(new TimingControlPoint { Time = random.Next(0, length) }); - - for (int i = 0; i < random.Next(1, 5); i++) - b.ControlPointInfo.DifficultyPoints.Add(new DifficultyControlPoint { Time = random.Next(0, length) }); - - for (int i = 0; i < random.Next(1, 5); i++) - b.ControlPointInfo.EffectPoints.Add(new EffectControlPoint { Time = random.Next(0, length) }); - - for (int i = 0; i < random.Next(1, 5); i++) - b.ControlPointInfo.SamplePoints.Add(new SampleControlPoint { Time = random.Next(0, length) }); - - b.BeatmapInfo.Bookmarks = new int[random.Next(10, 30)]; - for (int i = 0; i < b.BeatmapInfo.Bookmarks.Length; i++) - b.BeatmapInfo.Bookmarks[i] = random.Next(0, length); - - beatmap.Value = new TestWorkingBeatmap(b); - } - - private class TestWorkingBeatmap : WorkingBeatmap - { - private readonly Beatmap beatmap; - - public TestWorkingBeatmap(Beatmap beatmap) - : base(beatmap.BeatmapInfo) - { - this.beatmap = beatmap; - } - - protected override Texture GetBackground() => null; - - protected override Beatmap GetBeatmap() => beatmap; - - protected override Track GetTrack() => new TestTrack(); - - private class TestTrack : TrackVirtual - { - public TestTrack() - { - Length = length; - } - } } } } diff --git a/osu.Game.Tests/Visual/TestCaseGamefield.cs b/osu.Game.Tests/Visual/TestCaseGamefield.cs index 44f46dea18..80b3f9eb40 100644 --- a/osu.Game.Tests/Visual/TestCaseGamefield.cs +++ b/osu.Game.Tests/Visual/TestCaseGamefield.cs @@ -1,10 +1,12 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseGamefield : OsuTestCase { protected override void LoadComplete() diff --git a/osu.Game.Tests/Visual/TestCaseGraph.cs b/osu.Game.Tests/Visual/TestCaseGraph.cs index 99184d4689..285a43707a 100644 --- a/osu.Game.Tests/Visual/TestCaseGraph.cs +++ b/osu.Game.Tests/Visual/TestCaseGraph.cs @@ -2,12 +2,14 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Linq; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; using OpenTK; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseGraph : OsuTestCase { public TestCaseGraph() diff --git a/osu.Game.Tests/Visual/TestCaseHistoricalSection.cs b/osu.Game.Tests/Visual/TestCaseHistoricalSection.cs index a7fc58f2b5..2e94baa9fc 100644 --- a/osu.Game.Tests/Visual/TestCaseHistoricalSection.cs +++ b/osu.Game.Tests/Visual/TestCaseHistoricalSection.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -13,6 +14,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseHistoricalSection : OsuTestCase { public override IReadOnlyList RequiredTypes => diff --git a/osu.Game.Tests/Visual/TestCaseIconButton.cs b/osu.Game.Tests/Visual/TestCaseIconButton.cs index 525e867c56..fae79e25bd 100644 --- a/osu.Game.Tests/Visual/TestCaseIconButton.cs +++ b/osu.Game.Tests/Visual/TestCaseIconButton.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using OpenTK; using OpenTK.Graphics; using osu.Framework.Graphics; @@ -12,6 +13,7 @@ using osu.Game.Graphics.UserInterface; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseIconButton : OsuTestCase { public TestCaseIconButton() diff --git a/osu.Game.Tests/Visual/TestCaseIntroSequence.cs b/osu.Game.Tests/Visual/TestCaseIntroSequence.cs index 97116e7746..4af6255b48 100644 --- a/osu.Game.Tests/Visual/TestCaseIntroSequence.cs +++ b/osu.Game.Tests/Visual/TestCaseIntroSequence.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using OpenTK.Graphics; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -12,6 +13,7 @@ using osu.Game.Screens.Menu; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseIntroSequence : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] diff --git a/osu.Game.Tests/Visual/TestCaseKeyConfiguration.cs b/osu.Game.Tests/Visual/TestCaseKeyConfiguration.cs index 57bb36d144..e39b9f6683 100644 --- a/osu.Game.Tests/Visual/TestCaseKeyConfiguration.cs +++ b/osu.Game.Tests/Visual/TestCaseKeyConfiguration.cs @@ -1,10 +1,12 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Game.Overlays; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseKeyConfiguration : OsuTestCase { private readonly KeyBindingOverlay overlay; diff --git a/osu.Game.Tests/Visual/TestCaseKeyCounter.cs b/osu.Game.Tests/Visual/TestCaseKeyCounter.cs index ff1b320b5a..bf73c6899b 100644 --- a/osu.Game.Tests/Visual/TestCaseKeyCounter.cs +++ b/osu.Game.Tests/Visual/TestCaseKeyCounter.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.MathUtils; using osu.Game.Screens.Play; @@ -8,6 +9,7 @@ using OpenTK.Input; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseKeyCounter : OsuTestCase { public TestCaseKeyCounter() diff --git a/osu.Game.Tests/Visual/TestCaseMedalOverlay.cs b/osu.Game.Tests/Visual/TestCaseMedalOverlay.cs index f11c37f5b2..8d91a0f0dd 100644 --- a/osu.Game.Tests/Visual/TestCaseMedalOverlay.cs +++ b/osu.Game.Tests/Visual/TestCaseMedalOverlay.cs @@ -3,12 +3,14 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using osu.Game.Overlays; using osu.Game.Overlays.MedalSplash; using osu.Game.Users; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseMedalOverlay : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] diff --git a/osu.Game.Tests/Visual/TestCaseMusicController.cs b/osu.Game.Tests/Visual/TestCaseMusicController.cs index 9424a3fee7..2ddc57d7b4 100644 --- a/osu.Game.Tests/Visual/TestCaseMusicController.cs +++ b/osu.Game.Tests/Visual/TestCaseMusicController.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; @@ -11,6 +12,7 @@ using osu.Game.Overlays; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseMusicController : OsuTestCase { private readonly Bindable beatmapBacking = new Bindable(); diff --git a/osu.Game.Tests/Visual/TestCaseNotificationOverlay.cs b/osu.Game.Tests/Visual/TestCaseNotificationOverlay.cs index b2d3ac8c4d..2ba57f2bd2 100644 --- a/osu.Game.Tests/Visual/TestCaseNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/TestCaseNotificationOverlay.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -13,6 +14,7 @@ using osu.Game.Overlays.Notifications; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseNotificationOverlay : OsuTestCase { private readonly NotificationOverlay manager; diff --git a/osu.Game.Tests/Visual/TestCaseOnScreenDisplay.cs b/osu.Game.Tests/Visual/TestCaseOnScreenDisplay.cs index 9c6c50858f..6fe8bc5a8a 100644 --- a/osu.Game.Tests/Visual/TestCaseOnScreenDisplay.cs +++ b/osu.Game.Tests/Visual/TestCaseOnScreenDisplay.cs @@ -1,12 +1,14 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Game.Overlays; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseOnScreenDisplay : OsuTestCase { private FrameworkConfigManager config; diff --git a/osu.Game.Tests/Visual/TestCaseOsuGame.cs b/osu.Game.Tests/Visual/TestCaseOsuGame.cs index 9e6776800e..a802db6a10 100644 --- a/osu.Game.Tests/Visual/TestCaseOsuGame.cs +++ b/osu.Game.Tests/Visual/TestCaseOsuGame.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Timing; @@ -12,6 +13,7 @@ using OpenTK.Graphics; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseOsuGame : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] diff --git a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs index 809de2b8db..5fd8fcc9c3 100644 --- a/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/TestCasePlaySongSelect.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.MathUtils; @@ -19,6 +20,7 @@ using osu.Game.Tests.Platform; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCasePlaySongSelect : OsuTestCase { private BeatmapManager manager; @@ -63,12 +65,10 @@ namespace osu.Game.Tests.Visual var storage = new TestStorage(@"TestCasePlaySongSelect"); // this is by no means clean. should be replacing inside of OsuGameBase somehow. - var context = new OsuDbContext(); + IDatabaseContextFactory factory = new SingletonContextFactory(new OsuDbContext()); - OsuDbContext contextFactory() => context; - - dependencies.Cache(rulesets = new RulesetStore(contextFactory)); - dependencies.Cache(manager = new BeatmapManager(storage, contextFactory, rulesets, null) + dependencies.Cache(rulesets = new RulesetStore(factory)); + dependencies.Cache(manager = new BeatmapManager(storage, factory, rulesets, null, null) { DefaultBeatmap = defaultBeatmap = game.Beatmap.Default }); @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual { if (deleteMaps) { - manager.DeleteAll(); + manager.Delete(manager.GetAllUsableBeatmapSets()); game.Beatmap.SetDefault(); } diff --git a/osu.Game.Tests/Visual/TestCasePlaybackControl.cs b/osu.Game.Tests/Visual/TestCasePlaybackControl.cs index 82c0b8f4fd..9cdb3e36e3 100644 --- a/osu.Game.Tests/Visual/TestCasePlaybackControl.cs +++ b/osu.Game.Tests/Visual/TestCasePlaybackControl.cs @@ -1,7 +1,10 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Screens.Edit.Components; using osu.Game.Tests.Beatmaps; @@ -9,19 +12,31 @@ using OpenTK; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCasePlaybackControl : OsuTestCase { - public TestCasePlaybackControl() + private DependencyContainer dependencies; + + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) + => dependencies = new DependencyContainer(parent); + + [BackgroundDependencyLoader] + private void load() { + var clock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; + dependencies.CacheAs(clock); + dependencies.CacheAs(clock); + var playback = new PlaybackControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(200,100) }; + playback.Beatmap.Value = new TestWorkingBeatmap(new Beatmap()); - Add(playback); + Child = playback; } } } diff --git a/osu.Game.Tests/Visual/TestCasePopupDialog.cs b/osu.Game.Tests/Visual/TestCasePopupDialog.cs index e3bae3955a..8d830672b7 100644 --- a/osu.Game.Tests/Visual/TestCasePopupDialog.cs +++ b/osu.Game.Tests/Visual/TestCasePopupDialog.cs @@ -1,12 +1,14 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Overlays.Dialog; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCasePopupDialog : OsuTestCase { public TestCasePopupDialog() diff --git a/osu.Game.Tests/Visual/TestCaseRankGraph.cs b/osu.Game.Tests/Visual/TestCaseRankGraph.cs index 88631aa982..ad53238e76 100644 --- a/osu.Game.Tests/Visual/TestCaseRankGraph.cs +++ b/osu.Game.Tests/Visual/TestCaseRankGraph.cs @@ -9,11 +9,13 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using System.Collections.Generic; using System; +using NUnit.Framework; using osu.Game.Graphics.UserInterface; using osu.Game.Users; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseRankGraph : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] diff --git a/osu.Game.Tests/Visual/TestCaseReplay.cs b/osu.Game.Tests/Visual/TestCaseReplay.cs index 237687458d..115ac11919 100644 --- a/osu.Game.Tests/Visual/TestCaseReplay.cs +++ b/osu.Game.Tests/Visual/TestCaseReplay.cs @@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual // We create a dummy RulesetContainer just to get the replay - we don't want to use mods here // to simulate setting a replay rather than having the replay already set for us beatmap.Mods.Value = beatmap.Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() }); - var dummyRulesetContainer = ruleset.CreateRulesetContainerWith(beatmap, false); + var dummyRulesetContainer = ruleset.CreateRulesetContainerWith(beatmap, beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo)); // We have the replay var replay = dummyRulesetContainer.Replay; diff --git a/osu.Game.Tests/Visual/TestCaseReplaySettingsOverlay.cs b/osu.Game.Tests/Visual/TestCaseReplaySettingsOverlay.cs index 595a93b194..a1b683b64c 100644 --- a/osu.Game.Tests/Visual/TestCaseReplaySettingsOverlay.cs +++ b/osu.Game.Tests/Visual/TestCaseReplaySettingsOverlay.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Play.HUD; @@ -8,6 +9,7 @@ using osu.Game.Screens.Play.PlayerSettings; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseReplaySettingsOverlay : OsuTestCase { public TestCaseReplaySettingsOverlay() diff --git a/osu.Game.Tests/Visual/TestCaseResults.cs b/osu.Game.Tests/Visual/TestCaseResults.cs index 012d31e75a..06bdfdb7e1 100644 --- a/osu.Game.Tests/Visual/TestCaseResults.cs +++ b/osu.Game.Tests/Visual/TestCaseResults.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Rulesets.Scoring; @@ -11,6 +12,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseResults : OsuTestCase { private BeatmapManager beatmaps; diff --git a/osu.Game.Tests/Visual/TestCaseRoomInspector.cs b/osu.Game.Tests/Visual/TestCaseRoomInspector.cs index 8c4aa02a68..c45312392f 100644 --- a/osu.Game.Tests/Visual/TestCaseRoomInspector.cs +++ b/osu.Game.Tests/Visual/TestCaseRoomInspector.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; @@ -11,6 +12,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseRoomInspector : OsuTestCase { private RulesetStore rulesets; diff --git a/osu.Game.Tests/Visual/TestCaseScoreCounter.cs b/osu.Game.Tests/Visual/TestCaseScoreCounter.cs index a8dc96ad72..e657035355 100644 --- a/osu.Game.Tests/Visual/TestCaseScoreCounter.cs +++ b/osu.Game.Tests/Visual/TestCaseScoreCounter.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.MathUtils; @@ -10,6 +11,7 @@ using OpenTK; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseScoreCounter : OsuTestCase { public TestCaseScoreCounter() diff --git a/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs b/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs index 21d967c3e3..745ae9ad9d 100644 --- a/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs +++ b/osu.Game.Tests/Visual/TestCaseScrollingHitObjects.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Extensions.IEnumerableExtensions; using OpenTK; using osu.Framework.Graphics; @@ -16,6 +17,7 @@ using osu.Game.Rulesets.UI.Scrolling; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseScrollingHitObjects : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] { typeof(Playfield) }; @@ -138,12 +140,12 @@ namespace osu.Game.Tests.Visual { Origin = Anchor.Centre; - Add(new Box + InternalChild = new Box { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both - }); + }; switch (direction) { @@ -173,7 +175,7 @@ namespace osu.Game.Tests.Visual Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; - Add(new Box { Size = new Vector2(75) }); + InternalChild = new Box { Size = new Vector2(75) }; } protected override void UpdateState(ArmedState state) diff --git a/osu.Game.Tests/Visual/TestCaseSettings.cs b/osu.Game.Tests/Visual/TestCaseSettings.cs index 923ae540db..3f42f2e863 100644 --- a/osu.Game.Tests/Visual/TestCaseSettings.cs +++ b/osu.Game.Tests/Visual/TestCaseSettings.cs @@ -1,12 +1,14 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Game.Overlays; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseSettings : OsuTestCase { private readonly SettingsOverlay settings; diff --git a/osu.Game.Tests/Visual/TestCaseSkipButton.cs b/osu.Game.Tests/Visual/TestCaseSkipButton.cs index 3fd66f8be3..df94d5147f 100644 --- a/osu.Game.Tests/Visual/TestCaseSkipButton.cs +++ b/osu.Game.Tests/Visual/TestCaseSkipButton.cs @@ -1,17 +1,19 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseSkipButton : OsuTestCase { protected override void LoadComplete() { base.LoadComplete(); - Add(new SkipButton(Clock.CurrentTime + 5000)); + Add(new SkipOverlay(Clock.CurrentTime + 5000)); } } } diff --git a/osu.Game.Tests/Visual/TestCaseSocial.cs b/osu.Game.Tests/Visual/TestCaseSocial.cs index d3ff18b37f..4003d834d5 100644 --- a/osu.Game.Tests/Visual/TestCaseSocial.cs +++ b/osu.Game.Tests/Visual/TestCaseSocial.cs @@ -3,12 +3,14 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using osu.Game.Overlays; using osu.Game.Overlays.Social; using osu.Game.Users; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseSocial : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] diff --git a/osu.Game.Tests/Visual/TestCaseSongProgress.cs b/osu.Game.Tests/Visual/TestCaseSongProgress.cs index 2320e8d8db..857fd6c902 100644 --- a/osu.Game.Tests/Visual/TestCaseSongProgress.cs +++ b/osu.Game.Tests/Visual/TestCaseSongProgress.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.MathUtils; using osu.Framework.Timing; @@ -10,6 +11,7 @@ using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseSongProgress : OsuTestCase { private readonly SongProgress progress; diff --git a/osu.Game.Tests/Visual/TestCaseStoryboard.cs b/osu.Game.Tests/Visual/TestCaseStoryboard.cs index 089733c57e..d34a0e0e5f 100644 --- a/osu.Game.Tests/Visual/TestCaseStoryboard.cs +++ b/osu.Game.Tests/Visual/TestCaseStoryboard.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; @@ -14,6 +15,7 @@ using OpenTK.Graphics; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseStoryboard : OsuTestCase { private readonly Bindable beatmapBacking = new Bindable(); diff --git a/osu.Game.Tests/Visual/TestCaseTextAwesome.cs b/osu.Game.Tests/Visual/TestCaseTextAwesome.cs index 830dea406a..bf7609ff8d 100644 --- a/osu.Game.Tests/Visual/TestCaseTextAwesome.cs +++ b/osu.Game.Tests/Visual/TestCaseTextAwesome.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -10,6 +11,7 @@ using OpenTK; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseTextAwesome : OsuTestCase { public TestCaseTextAwesome() diff --git a/osu.Game.Tests/Visual/TestCaseToolbar.cs b/osu.Game.Tests/Visual/TestCaseToolbar.cs index b596c4d5e0..94e45fe0c2 100644 --- a/osu.Game.Tests/Visual/TestCaseToolbar.cs +++ b/osu.Game.Tests/Visual/TestCaseToolbar.cs @@ -4,11 +4,13 @@ using System; using System.Collections.Generic; using System.Linq; +using NUnit.Framework; using osu.Framework.Graphics.Containers; using osu.Game.Overlays.Toolbar; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseToolbar : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] diff --git a/osu.Game.Tests/Visual/TestCaseUserPanel.cs b/osu.Game.Tests/Visual/TestCaseUserPanel.cs index b18edf0ccb..ed377dc160 100644 --- a/osu.Game.Tests/Visual/TestCaseUserPanel.cs +++ b/osu.Game.Tests/Visual/TestCaseUserPanel.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Users; @@ -8,6 +9,7 @@ using OpenTK; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseUserPanel : OsuTestCase { public TestCaseUserPanel() diff --git a/osu.Game.Tests/Visual/TestCaseUserProfile.cs b/osu.Game.Tests/Visual/TestCaseUserProfile.cs index 8acc8d1b5b..1fc6c6f224 100644 --- a/osu.Game.Tests/Visual/TestCaseUserProfile.cs +++ b/osu.Game.Tests/Visual/TestCaseUserProfile.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using NUnit.Framework; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Profile; @@ -11,6 +12,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseUserProfile : OsuTestCase { private readonly TestUserProfileOverlay profile; @@ -56,6 +58,12 @@ namespace osu.Game.Tests.Visual checkSupporterTag(false); + AddStep("Show null dummy", () => profile.ShowUser(new User + { + Username = @"Null", + Id = 1, + }, false)); + AddStep("Show ppy", () => profile.ShowUser(new User { Username = @"peppy", diff --git a/osu.Game.Tests/Visual/TestCaseUserProfileRecentSection.cs b/osu.Game.Tests/Visual/TestCaseUserProfileRecentSection.cs new file mode 100644 index 0000000000..1f7a7e7165 --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseUserProfileRecentSection.cs @@ -0,0 +1,161 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Online.API.Requests; +using osu.Game.Overlays.Profile.Sections; +using osu.Game.Overlays.Profile.Sections.Recent; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace osu.Game.Tests.Visual +{ + [TestFixture] + public class TestCaseUserProfileRecentSection : OsuTestCase + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(RecentSection), + typeof(DrawableRecentActivity), + typeof(PaginatedRecentActivityContainer), + typeof(MedalIcon) + }; + + public TestCaseUserProfileRecentSection() + { + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.Gray(0.2f) + }, + new ScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + ChildrenEnumerable = createDummyActivities().Select(a => new DrawableRecentActivity(a)) + }, + } + }; + } + + private IEnumerable createDummyActivities() + { + var dummyBeatmap = new RecentActivity.RecentActivityBeatmap + { + Title = @"Dummy beatmap", + Url = "/b/1337", + }; + + var dummyUser = new RecentActivity.RecentActivityUser + { + Username = "DummyReborn", + Url = "/u/666", + PreviousUsername = "Dummy", + }; + + return new[] + { + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.Achievement, + Achievement = new RecentActivity.RecentActivityAchievement + { + Name = @"Feelin' It", + Slug = @"all-secret-feelinit", + }, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapPlaycount, + Count = 1337, + Beatmap = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapsetApprove, + Approval = BeatmapApproval.Qualified, + Beatmapset = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapsetDelete, + Beatmapset = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapsetRevive, + Beatmapset = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapsetRevive, + Beatmapset = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapsetUpdate, + Beatmapset = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.BeatmapsetUpload, + Beatmapset = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.Rank, + Rank = 1, + Mode = "osu!", + Beatmap = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.RankLost, + Mode = "osu!", + Beatmap = dummyBeatmap, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.UsernameChange, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.UserSupportAgain, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.UserSupportFirst, + }, + new RecentActivity + { + User = dummyUser, + Type = RecentActivityType.UserSupportGift, + }, + }; + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseUserRanks.cs b/osu.Game.Tests/Visual/TestCaseUserRanks.cs index 1926585f07..effc98c381 100644 --- a/osu.Game.Tests/Visual/TestCaseUserRanks.cs +++ b/osu.Game.Tests/Visual/TestCaseUserRanks.cs @@ -10,9 +10,11 @@ using osu.Game.Overlays.Profile.Sections.Ranks; using osu.Game.Users; using System; using System.Collections.Generic; +using NUnit.Framework; namespace osu.Game.Tests.Visual { + [TestFixture] public class TestCaseUserRanks : OsuTestCase { public override IReadOnlyList RequiredTypes => new[] { typeof(DrawableProfileScore), typeof(RanksSection) }; diff --git a/osu.Game.Tests/Visual/TestCaseVolumePieces.cs b/osu.Game.Tests/Visual/TestCaseVolumePieces.cs new file mode 100644 index 0000000000..cfbf7fdb4d --- /dev/null +++ b/osu.Game.Tests/Visual/TestCaseVolumePieces.cs @@ -0,0 +1,30 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using osu.Framework.Graphics; +using osu.Game.Overlays.Volume; +using OpenTK.Graphics; + +namespace osu.Game.Tests.Visual +{ + public class TestCaseVolumePieces : OsuTestCase + { + public override IReadOnlyList RequiredTypes => new[] { typeof(VolumeMeter), typeof(MuteButton) }; + + protected override void LoadComplete() + { + VolumeMeter meter; + MuteButton mute; + Add(meter = new VolumeMeter("MASTER", 125, Color4.Blue)); + Add(mute = new MuteButton + { + Margin = new MarginPadding { Top = 200 } + }); + + AddSliderStep("master volume", 0, 10, 0, i => meter.Bindable.Value = i * 0.1); + AddToggleStep("mute", b => mute.Current.Value = b); + } + } +} diff --git a/osu.Game.Tests/Visual/TestCaseWaveform.cs b/osu.Game.Tests/Visual/TestCaseWaveform.cs index dd5420400f..7d4a9d663b 100644 --- a/osu.Game.Tests/Visual/TestCaseWaveform.cs +++ b/osu.Game.Tests/Visual/TestCaseWaveform.cs @@ -16,7 +16,7 @@ using osu.Game.Screens.Edit.Screens.Compose.Timeline; namespace osu.Game.Tests.Visual { - [Ignore("CI regularly hangs on this TestCase...")] + [TestFixture] public class TestCaseWaveform : OsuTestCase { private readonly Bindable beatmapBacking = new Bindable(); diff --git a/osu.Game/Audio/SampleInfo.cs b/osu.Game/Audio/SampleInfo.cs index e6f4a0b8d1..2014db6c61 100644 --- a/osu.Game/Audio/SampleInfo.cs +++ b/osu.Game/Audio/SampleInfo.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using osu.Framework.Audio.Sample; namespace osu.Game.Audio { @@ -14,22 +13,10 @@ namespace osu.Game.Audio public const string HIT_NORMAL = @"hitnormal"; public const string HIT_CLAP = @"hitclap"; - public SampleChannel GetChannel(SampleManager manager, string resourceNamespace = null) - { - SampleChannel channel = null; - - if (resourceNamespace != null) - channel = manager.Get($"Gameplay/{resourceNamespace}/{Bank}-{Name}"); - - // try without namespace as a fallback. - if (channel == null) - channel = manager.Get($"Gameplay/{Bank}-{Name}"); - - if (channel != null) - channel.Volume.Value = Volume / 100.0; - - return channel; - } + /// + /// An optional ruleset namespace. + /// + public string Namespace; /// /// The bank to load the sample from. diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 4fd54e4364..60cf93fd91 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -1,7 +1,6 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK.Graphics; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Objects; using System.Collections.Generic; @@ -22,13 +21,6 @@ namespace osu.Game.Beatmaps public BeatmapInfo BeatmapInfo = new BeatmapInfo(); public ControlPointInfo ControlPointInfo = new ControlPointInfo(); public List Breaks = new List(); - public List ComboColors = new List - { - new Color4(17, 136, 170, 255), - new Color4(102, 136, 0, 255), - new Color4(204, 102, 0, 255), - new Color4(121, 9, 13, 255) - }; [JsonIgnore] public BeatmapMetadata Metadata => BeatmapInfo?.Metadata ?? BeatmapInfo?.BeatmapSet?.Metadata; @@ -54,7 +46,6 @@ namespace osu.Game.Beatmaps BeatmapInfo = original?.BeatmapInfo.DeepClone() ?? BeatmapInfo; ControlPointInfo = original?.ControlPointInfo ?? ControlPointInfo; Breaks = original?.Breaks ?? Breaks; - ComboColors = original?.ComboColors ?? ComboColors; HitObjects = original?.HitObjects ?? HitObjects; if (original == null && Metadata == null) @@ -85,9 +76,13 @@ namespace osu.Game.Beatmaps /// Constructs a new beatmap. /// /// The original beatmap to use the parameters of. - public Beatmap(Beatmap original = null) + public Beatmap(Beatmap original) : base(original) { } + + public Beatmap() + { + } } } diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index 20de4e9680..2003b845d9 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -12,8 +12,16 @@ namespace osu.Game.Beatmaps /// Converts a Beatmap for another mode. /// /// The type of HitObject stored in the Beatmap. - public abstract class BeatmapConverter where T : HitObject + public abstract class BeatmapConverter : IBeatmapConverter + where T : HitObject { + private event Action> ObjectConverted; + event Action> IBeatmapConverter.ObjectConverted + { + add => ObjectConverted += value; + remove => ObjectConverted -= value; + } + /// /// Checks if a Beatmap can be converted using this Beatmap Converter. /// @@ -32,6 +40,8 @@ namespace osu.Game.Beatmaps return ConvertBeatmap(new Beatmap(original)); } + void IBeatmapConverter.Convert(Beatmap original) => Convert(original); + /// /// Performs the conversion of a Beatmap using this Beatmap Converter. /// @@ -40,9 +50,13 @@ namespace osu.Game.Beatmaps protected virtual Beatmap ConvertBeatmap(Beatmap original) { var beatmap = CreateBeatmap(); + + // todo: this *must* share logic (or directly use) Beatmap's constructor. + // right now this isn't easily possible due to generic entanglement. beatmap.BeatmapInfo = original.BeatmapInfo; beatmap.ControlPointInfo = original.ControlPointInfo; beatmap.HitObjects = original.HitObjects.SelectMany(h => convert(h, original)).ToList(); + beatmap.Breaks = original.Breaks; return beatmap; } @@ -63,8 +77,11 @@ namespace osu.Game.Beatmaps yield break; } + var converted = ConvertHitObject(original, beatmap).ToList(); + ObjectConverted?.Invoke(original, converted); + // Convert the hit object - foreach (var obj in ConvertHitObject(original, beatmap)) + foreach (var obj in converted) { if (obj == null) continue; diff --git a/osu.Game/Beatmaps/BeatmapDifficulty.cs b/osu.Game/Beatmaps/BeatmapDifficulty.cs index 3bfa70711b..38b84b4b03 100644 --- a/osu.Game/Beatmaps/BeatmapDifficulty.cs +++ b/osu.Game/Beatmaps/BeatmapDifficulty.cs @@ -20,9 +20,17 @@ namespace osu.Game.Beatmaps public float DrainRate { get; set; } = DEFAULT_DIFFICULTY; public float CircleSize { get; set; } = DEFAULT_DIFFICULTY; public float OverallDifficulty { get; set; } = DEFAULT_DIFFICULTY; - public float ApproachRate { get; set; } = DEFAULT_DIFFICULTY; - public float SliderMultiplier { get; set; } = 1; - public float SliderTickRate { get; set; } = 1; + + private float? approachRate; + + public float ApproachRate + { + get => approachRate ?? OverallDifficulty; + set => approachRate = value; + } + + public double SliderMultiplier { get; set; } = 1; + public double SliderTickRate { get; set; } = 1; /// /// Maps a difficulty value [0, 10] to a two-piece linear range of values. diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index a27c3a2a88..46fef78c4e 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -8,19 +8,14 @@ using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; -using osu.Framework.Audio.Track; +using osu.Framework.Audio; using osu.Framework.Extensions; -using osu.Framework.Graphics.Textures; -using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps.Formats; -using osu.Game.Beatmaps.IO; using osu.Game.Database; using osu.Game.Graphics; -using osu.Game.Graphics.Textures; -using osu.Game.IO; -using osu.Game.IPC; +using osu.Game.IO.Archives; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Overlays.Notifications; @@ -33,23 +28,13 @@ namespace osu.Game.Beatmaps /// /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. /// - public class BeatmapManager + public partial class BeatmapManager : ArchiveModelManager { - /// - /// Fired when a new becomes available in the database. - /// - public event Action BeatmapSetAdded; - /// /// Fired when a single difficulty has been hidden. /// public event Action BeatmapHidden; - /// - /// Fired when a is removed from the database. - /// - public event Action BeatmapSetRemoved; - /// /// Fired when a single difficulty has been restored. /// @@ -65,21 +50,7 @@ namespace osu.Game.Beatmaps /// public WorkingBeatmap DefaultBeatmap { private get; set; } - private readonly Storage storage; - - private BeatmapStore createBeatmapStore(Func context) - { - var store = new BeatmapStore(context); - store.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s); - store.BeatmapSetRemoved += s => BeatmapSetRemoved?.Invoke(s); - store.BeatmapHidden += b => BeatmapHidden?.Invoke(b); - store.BeatmapRestored += b => BeatmapRestored?.Invoke(b); - return store; - } - - private readonly Func createContext; - - private readonly FileStore files; + public override string[] HandledExtensions => new[] { ".osz" }; private readonly RulesetStore rulesets; @@ -87,161 +58,64 @@ namespace osu.Game.Beatmaps private readonly APIAccess api; + private readonly AudioManager audioManager; + private readonly List currentDownloads = new List(); - // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) - private BeatmapIPCChannel ipc; - - /// - /// Set an endpoint for notifications to be posted to. - /// - public Action PostNotification { private get; set; } - /// /// Set a storage with access to an osu-stable install for import purposes. /// public Func GetStableStorage { private get; set; } - private void refreshImportContext() + public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, APIAccess api, AudioManager audioManager, IIpcHost importHost = null) + : base(storage, contextFactory, new BeatmapStore(contextFactory), importHost) { - lock (importContextLock) - { - importContext?.Value?.Dispose(); + beatmaps = (BeatmapStore)ModelStore; + beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b); + beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b); - importContext = new Lazy(() => - { - var c = createContext(); - c.Database.AutoTransactionsEnabled = false; - return c; - }); - } - } - - public BeatmapManager(Storage storage, Func context, RulesetStore rulesets, APIAccess api, IIpcHost importHost = null) - { - createContext = context; - - refreshImportContext(); - - beatmaps = createBeatmapStore(context); - files = new FileStore(context, storage); - - this.storage = files.Storage; this.rulesets = rulesets; this.api = api; - - if (importHost != null) - ipc = new BeatmapIPCChannel(importHost, this); - - beatmaps.Cleanup(); + this.audioManager = audioManager; } - /// - /// Import one or more from filesystem . - /// This will post a notification tracking import progress. - /// - /// One or more beatmap locations on disk. - public void Import(params string[] paths) + protected override void Populate(BeatmapSetInfo model, ArchiveReader archive) { - var notification = new ProgressNotification + model.Beatmaps = createBeatmapDifficulties(archive); + + // remove metadata from difficulties where it matches the set + foreach (BeatmapInfo b in model.Beatmaps) + if (model.Metadata.Equals(b.Metadata)) + b.Metadata = null; + } + + protected override BeatmapSetInfo CheckForExisting(BeatmapSetInfo model) + { + // check if this beatmap has already been imported and exit early if so + var existingHashMatch = beatmaps.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash); + if (existingHashMatch != null) { - Text = "Beatmap import is initialising...", - CompletionText = "Import successful!", - Progress = 0, - State = ProgressNotificationState.Active, - }; + Undelete(existingHashMatch); + return existingHashMatch; + } - PostNotification?.Invoke(notification); - - int i = 0; - foreach (string path in paths) + // check if a set already exists with the same online id + if (model.OnlineBeatmapSetID != null) { - if (notification.State == ProgressNotificationState.Cancelled) - // user requested abort - return; - - try + var existingOnlineId = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == model.OnlineBeatmapSetID); + if (existingOnlineId != null) { - notification.Text = $"Importing ({i} of {paths.Length})\n{Path.GetFileName(path)}"; - using (ArchiveReader reader = getReaderFrom(path)) - Import(reader); - - notification.Progress = (float)++i / paths.Length; - - // We may or may not want to delete the file depending on where it is stored. - // e.g. reconstructing/repairing database with beatmaps from default storage. - // Also, not always a single file, i.e. for LegacyFilesystemReader - // TODO: Add a check to prevent files from storage to be deleted. - try - { - if (File.Exists(path)) - File.Delete(path); - } - catch (Exception e) - { - Logger.Error(e, $@"Could not delete original file after import ({Path.GetFileName(path)})"); - } - } - catch (Exception e) - { - e = e.InnerException ?? e; - Logger.Error(e, $@"Could not import beatmap set ({Path.GetFileName(path)})"); - refreshImportContext(); + Delete(existingOnlineId); + beatmaps.PurgeDeletable(s => s.ID == existingOnlineId.ID); } } - notification.State = ProgressNotificationState.Completed; - } - - private readonly object importContextLock = new object(); - - private Lazy importContext; - - /// - /// Import a beatmap from an . - /// - /// The beatmap to be imported. - public BeatmapSetInfo Import(ArchiveReader archiveReader) - { - // let's only allow one concurrent import at a time for now. - lock (importContextLock) - { - var context = importContext.Value; - - using (var transaction = context.BeginTransaction()) - { - // create local stores so we can isolate and thread safely, and share a context/transaction. - var iFiles = new FileStore(() => context, storage); - var iBeatmaps = createBeatmapStore(() => context); - - BeatmapSetInfo set = importToStorage(iFiles, iBeatmaps, archiveReader); - - if (set.ID == 0) - { - iBeatmaps.Add(set); - context.SaveChanges(); - } - - context.SaveChanges(transaction); - return set; - } - } - } - - /// - /// Import a beatmap from a . - /// - /// The beatmap to be imported. - public void Import(BeatmapSetInfo beatmapSetInfo) - { - // If we have an ID then we already exist in the database. - if (beatmapSetInfo.ID != 0) return; - - createBeatmapStore(createContext).Add(beatmapSetInfo); + return null; } /// /// Downloads a beatmap. + /// This will post notifications tracking progress. /// /// The to be downloaded. /// Whether the beatmap should be downloaded without video. Defaults to false. @@ -283,7 +157,7 @@ namespace osu.Game.Beatmaps { // This gets scheduled back to the update thread, but we want the import to run in the background. using (var stream = new MemoryStream(data)) - using (var archive = new OszArchiveReader(stream)) + using (var archive = new ZipArchiveReader(stream, beatmapSetInfo.ToString())) Import(archive); downloadNotification.State = ProgressNotificationState.Completed; @@ -425,21 +299,6 @@ namespace osu.Game.Beatmaps /// The beatmap difficulty to restore. public void Restore(BeatmapInfo beatmap) => beatmaps.Restore(beatmap); - /// - /// Returns a to a usable state if it has previously been deleted but not yet purged. - /// Is a no-op for already usable beatmaps. - /// - /// The store to restore beatmaps from. - /// The store to restore beatmap files from. - /// The beatmap to restore. - private void undelete(BeatmapStore beatmaps, FileStore files, BeatmapSetInfo beatmapSet) - { - if (!beatmaps.Undelete(beatmapSet)) return; - - if (!beatmapSet.Protected) - files.Reference(beatmapSet.Files.Select(f => f.FileInfo).ToArray()); - } - /// /// Retrieve a instance for the provided /// @@ -454,7 +313,7 @@ namespace osu.Game.Beatmaps if (beatmapInfo.Metadata == null) beatmapInfo.Metadata = beatmapInfo.BeatmapSet.Metadata; - WorkingBeatmap working = new BeatmapManagerWorkingBeatmap(files.Store, beatmapInfo); + WorkingBeatmap working = new BeatmapManagerWorkingBeatmap(Files.Store, beatmapInfo, audioManager); previous?.TransferTo(working); @@ -466,21 +325,20 @@ namespace osu.Game.Beatmaps /// /// The query. /// The first result for the provided query, or null if no results were found. - public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmaps.BeatmapSets.AsNoTracking().FirstOrDefault(query); + public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query); /// - /// Refresh an existing instance of a from the store. + /// Returns a list of all usable s. /// - /// A stale instance. - /// A fresh instance. - public BeatmapSetInfo Refresh(BeatmapSetInfo beatmapSet) => QueryBeatmapSet(s => s.ID == beatmapSet.ID); + /// A list of available . + public List GetAllUsableBeatmapSets() => beatmaps.ConsumableItems.Where(s => !s.DeletePending && !s.Protected).ToList(); /// /// Perform a lookup query on available s. /// /// The query. /// Results from the provided query. - public IEnumerable QueryBeatmapSets(Expression> query) => beatmaps.BeatmapSets.AsNoTracking().Where(query); + public IEnumerable QueryBeatmapSets(Expression> query) => beatmaps.ConsumableItems.AsNoTracking().Where(query); /// /// Perform a lookup query on available s. @@ -497,220 +355,8 @@ namespace osu.Game.Beatmaps public IEnumerable QueryBeatmaps(Expression> query) => beatmaps.Beatmaps.AsNoTracking().Where(query); /// - /// Creates an from a valid storage path. + /// Denotes whether an osu-stable installation is present to perform automated imports from. /// - /// A file or folder path resolving the beatmap content. - /// A reader giving access to the beatmap's content. - private ArchiveReader getReaderFrom(string path) - { - if (ZipUtils.IsZipArchive(path)) - // ReSharper disable once InconsistentlySynchronizedField - return new OszArchiveReader(storage.GetStream(path)); - return new LegacyFilesystemReader(path); - } - - /// - /// Import a beamap into our local storage. - /// If the beatmap is already imported, the existing instance will be returned. - /// - /// The store to import beatmap files to. - /// The store to import beatmaps to. - /// The beatmap archive to be read. - /// The imported beatmap, or an existing instance if it is already present. - private BeatmapSetInfo importToStorage(FileStore files, BeatmapStore beatmaps, ArchiveReader reader) - { - // let's make sure there are actually .osu files to import. - string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu")); - if (string.IsNullOrEmpty(mapName)) - throw new InvalidOperationException("No beatmap files found in the map folder."); - - // for now, concatenate all .osu files in the set to create a unique hash. - MemoryStream hashable = new MemoryStream(); - foreach (string file in reader.Filenames.Where(f => f.EndsWith(".osu"))) - using (Stream s = reader.GetStream(file)) - s.CopyTo(hashable); - - var hash = hashable.ComputeSHA2Hash(); - - // check if this beatmap has already been imported and exit early if so. - var beatmapSet = beatmaps.BeatmapSets.FirstOrDefault(b => b.Hash == hash); - - if (beatmapSet != null) - { - undelete(beatmaps, files, beatmapSet); - - // ensure all files are present and accessible - foreach (var f in beatmapSet.Files) - { - if (!storage.Exists(f.FileInfo.StoragePath)) - using (Stream s = reader.GetStream(f.Filename)) - files.Add(s, false); - } - - // todo: delete any files which shouldn't exist any more. - - return beatmapSet; - } - - List fileInfos = new List(); - - // import files to manager - foreach (string file in reader.Filenames) - using (Stream s = reader.GetStream(file)) - fileInfos.Add(new BeatmapSetFileInfo - { - Filename = file, - FileInfo = files.Add(s) - }); - - BeatmapMetadata metadata; - - using (var stream = new StreamReader(reader.GetStream(mapName))) - metadata = Decoder.GetDecoder(stream).DecodeBeatmap(stream).Metadata; - - // check if a set already exists with the same online id. - if (metadata.OnlineBeatmapSetID != null) - beatmapSet = beatmaps.BeatmapSets.FirstOrDefault(b => b.OnlineBeatmapSetID == metadata.OnlineBeatmapSetID); - - if (beatmapSet == null) - beatmapSet = new BeatmapSetInfo - { - OnlineBeatmapSetID = metadata.OnlineBeatmapSetID, - Beatmaps = new List(), - Hash = hash, - Files = fileInfos, - Metadata = metadata - }; - - var mapNames = reader.Filenames.Where(f => f.EndsWith(".osu")); - - foreach (var name in mapNames) - { - using (var raw = reader.GetStream(name)) - using (var ms = new MemoryStream()) //we need a memory stream so we can seek and shit - using (var sr = new StreamReader(ms)) - { - raw.CopyTo(ms); - ms.Position = 0; - - var decoder = Decoder.GetDecoder(sr); - Beatmap beatmap = decoder.DecodeBeatmap(sr); - - beatmap.BeatmapInfo.Path = name; - beatmap.BeatmapInfo.Hash = ms.ComputeSHA2Hash(); - beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash(); - - var existing = beatmaps.Beatmaps.FirstOrDefault(b => b.Hash == beatmap.BeatmapInfo.Hash || beatmap.BeatmapInfo.OnlineBeatmapID != null && b.OnlineBeatmapID == beatmap.BeatmapInfo.OnlineBeatmapID); - - if (existing == null) - { - // Exclude beatmap-metadata if it's equal to beatmapset-metadata - if (metadata.Equals(beatmap.Metadata)) - beatmap.BeatmapInfo.Metadata = null; - - RulesetInfo ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID); - - // TODO: this should be done in a better place once we actually need to dynamically update it. - beatmap.BeatmapInfo.Ruleset = ruleset; - beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance()?.CreateDifficultyCalculator(beatmap).Calculate() ?? 0; - - beatmapSet.Beatmaps.Add(beatmap.BeatmapInfo); - } - } - } - - return beatmapSet; - } - - /// - /// Returns a list of all usable s. - /// - /// A list of available . - public List GetAllUsableBeatmapSets() - { - return beatmaps.BeatmapSets.Where(s => !s.DeletePending).ToList(); - } - - protected class BeatmapManagerWorkingBeatmap : WorkingBeatmap - { - private readonly IResourceStore store; - - public BeatmapManagerWorkingBeatmap(IResourceStore store, BeatmapInfo beatmapInfo) - : base(beatmapInfo) - { - this.store = store; - } - - protected override Beatmap GetBeatmap() - { - try - { - using (var stream = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo.Path)))) - { - Decoder decoder = Decoder.GetDecoder(stream); - return decoder.DecodeBeatmap(stream); - } - } - catch - { - return null; - } - } - - private string getPathForFile(string filename) => BeatmapSetInfo.Files.First(f => string.Equals(f.Filename, filename, StringComparison.InvariantCultureIgnoreCase)).FileInfo.StoragePath; - - protected override Texture GetBackground() - { - if (Metadata?.BackgroundFile == null) - return null; - - try - { - return new LargeTextureStore(new RawTextureLoaderStore(store)).Get(getPathForFile(Metadata.BackgroundFile)); - } - catch - { - return null; - } - } - - protected override Track GetTrack() - { - try - { - var trackData = store.GetStream(getPathForFile(Metadata.AudioFile)); - return trackData == null ? null : new TrackBass(trackData); - } - catch - { - return new TrackVirtual(); - } - } - - protected override Waveform GetWaveform() => new Waveform(store.GetStream(getPathForFile(Metadata.AudioFile))); - - protected override Storyboard GetStoryboard() - { - try - { - using (var beatmap = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo.Path)))) - { - Decoder decoder = Decoder.GetDecoder(beatmap); - - if (BeatmapSetInfo?.StoryboardFile == null) - return decoder.GetStoryboardDecoder().DecodeStoryboard(beatmap); - - using (var storyboard = new StreamReader(store.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile)))) - return decoder.GetStoryboardDecoder().DecodeStoryboard(beatmap, storyboard); - } - } - catch - { - return new Storyboard(); - } - } - } - public bool StableInstallationAvailable => GetStableStorage?.Invoke() != null; /// @@ -729,35 +375,73 @@ namespace osu.Game.Beatmaps await Task.Factory.StartNew(() => Import(stable.GetDirectories("Songs")), TaskCreationOptions.LongRunning); } - public void DeleteAll() + /// + /// Create a SHA-2 hash from the provided archive based on contained beatmap (.osu) file content. + /// + private string computeBeatmapSetHash(ArchiveReader reader) { - var maps = GetAllUsableBeatmapSets(); + // for now, concatenate all .osu files in the set to create a unique hash. + MemoryStream hashable = new MemoryStream(); + foreach (string file in reader.Filenames.Where(f => f.EndsWith(".osu"))) + using (Stream s = reader.GetStream(file)) + s.CopyTo(hashable); - if (maps.Count == 0) return; + return hashable.ComputeSHA2Hash(); + } - var notification = new ProgressNotification + protected override BeatmapSetInfo CreateModel(ArchiveReader reader) + { + // let's make sure there are actually .osu files to import. + string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu")); + if (string.IsNullOrEmpty(mapName)) throw new InvalidOperationException("No beatmap files found in the map folder."); + + BeatmapMetadata metadata; + using (var stream = new StreamReader(reader.GetStream(mapName))) + metadata = Decoder.GetDecoder(stream).Decode(stream).Metadata; + + return new BeatmapSetInfo { - Progress = 0, - CompletionText = "Deleted all beatmaps!", - State = ProgressNotificationState.Active, + OnlineBeatmapSetID = metadata.OnlineBeatmapSetID, + Beatmaps = new List(), + Hash = computeBeatmapSetHash(reader), + Metadata = metadata }; + } - PostNotification?.Invoke(notification); + /// + /// Create all required s for the provided archive. + /// + private List createBeatmapDifficulties(ArchiveReader reader) + { + var beatmapInfos = new List(); - int i = 0; - - foreach (var b in maps) + foreach (var name in reader.Filenames.Where(f => f.EndsWith(".osu"))) { - if (notification.State == ProgressNotificationState.Cancelled) - // user requested abort - return; + using (var raw = reader.GetStream(name)) + using (var ms = new MemoryStream()) //we need a memory stream so we can seek and shit + using (var sr = new StreamReader(ms)) + { + raw.CopyTo(ms); + ms.Position = 0; - notification.Text = $"Deleting ({i} of {maps.Count})"; - notification.Progress = (float)++i / maps.Count; - Delete(b); + var decoder = Decoder.GetDecoder(sr); + Beatmap beatmap = decoder.Decode(sr); + + beatmap.BeatmapInfo.Path = name; + beatmap.BeatmapInfo.Hash = ms.ComputeSHA2Hash(); + beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash(); + + RulesetInfo ruleset = rulesets.GetRuleset(beatmap.BeatmapInfo.RulesetID); + + // TODO: this should be done in a better place once we actually need to dynamically update it. + beatmap.BeatmapInfo.Ruleset = ruleset; + beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance()?.CreateDifficultyCalculator(beatmap).Calculate() ?? 0; + + beatmapInfos.Add(beatmap.BeatmapInfo); + } } - notification.State = ProgressNotificationState.Completed; + return beatmapInfos; } } } diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs new file mode 100644 index 0000000000..5874314f75 --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -0,0 +1,125 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.IO; +using System.Linq; +using osu.Framework.Audio; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; +using osu.Framework.Logging; +using osu.Game.Beatmaps.Formats; +using osu.Game.Graphics.Textures; +using osu.Game.Skinning; +using osu.Game.Storyboards; + +namespace osu.Game.Beatmaps +{ + public partial class BeatmapManager + { + protected class BeatmapManagerWorkingBeatmap : WorkingBeatmap + { + private readonly IResourceStore store; + private readonly AudioManager audioManager; + + public BeatmapManagerWorkingBeatmap(IResourceStore store, BeatmapInfo beatmapInfo, AudioManager audioManager) + : base(beatmapInfo) + { + this.store = store; + this.audioManager = audioManager; + } + + protected override Beatmap GetBeatmap() + { + try + { + using (var stream = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo.Path)))) + return Decoder.GetDecoder(stream).Decode(stream); + } + catch + { + return null; + } + } + + private string getPathForFile(string filename) => BeatmapSetInfo.Files.First(f => string.Equals(f.Filename, filename, StringComparison.InvariantCultureIgnoreCase)).FileInfo.StoragePath; + + protected override Texture GetBackground() + { + if (Metadata?.BackgroundFile == null) + return null; + + try + { + return new LargeTextureStore(new RawTextureLoaderStore(store)).Get(getPathForFile(Metadata.BackgroundFile)); + } + catch + { + return null; + } + } + + protected override Track GetTrack() + { + try + { + var trackData = store.GetStream(getPathForFile(Metadata.AudioFile)); + return trackData == null ? null : new TrackBass(trackData); + } + catch + { + return new TrackVirtual(); + } + } + + protected override Waveform GetWaveform() => new Waveform(store.GetStream(getPathForFile(Metadata.AudioFile))); + + protected override Storyboard GetStoryboard() + { + Storyboard storyboard; + try + { + using (var stream = new StreamReader(store.GetStream(getPathForFile(BeatmapInfo.Path)))) + { + var decoder = Decoder.GetDecoder(stream); + + // todo: support loading from both set-wide storyboard *and* beatmap specific. + if (BeatmapSetInfo?.StoryboardFile == null) + storyboard = decoder.Decode(stream); + else + { + using (var secondaryStream = new StreamReader(store.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile)))) + storyboard = decoder.Decode(stream, secondaryStream); + } + } + } + catch (Exception e) + { + Logger.Error(e, "Storyboard failed to load"); + storyboard = new Storyboard(); + } + + storyboard.BeatmapInfo = BeatmapInfo; + + return storyboard; + } + + protected override Skin GetSkin() + { + Skin skin; + try + { + skin = new LegacyBeatmapSkin(BeatmapInfo, store, audioManager); + } + catch (Exception e) + { + Logger.Error(e, "Skin failed to load"); + skin = new DefaultSkin(); + } + + return skin; + } + } + } +} diff --git a/osu.Game/Beatmaps/BeatmapProcessor.cs b/osu.Game/Beatmaps/BeatmapProcessor.cs index 9b528699ef..f2cc419043 100644 --- a/osu.Game/Beatmaps/BeatmapProcessor.cs +++ b/osu.Game/Beatmaps/BeatmapProcessor.cs @@ -1,7 +1,9 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Linq; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Beatmaps { @@ -19,6 +21,29 @@ namespace osu.Game.Beatmaps /// /// /// The Beatmap to process. - public virtual void PostProcess(Beatmap beatmap) { } + public virtual void PostProcess(Beatmap beatmap) + { + IHasComboInformation lastObj = null; + + foreach (var obj in beatmap.HitObjects.OfType()) + { + if (obj.NewCombo) + { + obj.IndexInCurrentCombo = 0; + if (lastObj != null) + { + lastObj.LastInCombo = true; + obj.ComboIndex = lastObj.ComboIndex + 1; + } + } + else if (lastObj != null) + { + obj.IndexInCurrentCombo = lastObj.IndexInCurrentCombo + 1; + obj.ComboIndex = lastObj.ComboIndex; + } + + lastObj = obj; + } + } } } diff --git a/osu.Game/Beatmaps/BeatmapSetFileInfo.cs b/osu.Game/Beatmaps/BeatmapSetFileInfo.cs index ae4a6772a2..e88af6ed30 100644 --- a/osu.Game/Beatmaps/BeatmapSetFileInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetFileInfo.cs @@ -3,11 +3,12 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using osu.Game.Database; using osu.Game.IO; namespace osu.Game.Beatmaps { - public class BeatmapSetFileInfo + public class BeatmapSetFileInfo : INamedFileInfo { [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int ID { get; set; } diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index 982e41c92c..1736e3fa90 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -8,7 +8,7 @@ using osu.Game.Database; namespace osu.Game.Beatmaps { - public class BeatmapSetInfo : IHasPrimaryKey + public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles, ISoftDelete { [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int ID { get; set; } diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs index df71c5c0d0..93ad1badd2 100644 --- a/osu.Game/Beatmaps/BeatmapStore.cs +++ b/osu.Game/Beatmaps/BeatmapStore.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Collections.Generic; using System.Linq; using Microsoft.EntityFrameworkCore; using osu.Game.Database; @@ -11,83 +12,16 @@ namespace osu.Game.Beatmaps /// /// Handles the storage and retrieval of Beatmaps/BeatmapSets to the database backing /// - public class BeatmapStore : DatabaseBackedStore + public class BeatmapStore : MutableDatabaseBackedStore { - public event Action BeatmapSetAdded; - public event Action BeatmapSetRemoved; - public event Action BeatmapHidden; public event Action BeatmapRestored; - public BeatmapStore(Func factory) + public BeatmapStore(IDatabaseContextFactory factory) : base(factory) { } - /// - /// Add a to the database. - /// - /// The beatmap to add. - public void Add(BeatmapSetInfo beatmapSet) - { - var context = GetContext(); - - foreach (var beatmap in beatmapSet.Beatmaps.Where(b => b.Metadata != null)) - { - // If we detect a new metadata object it'll be attached to the current context so it can be reused - // to prevent duplicate entries when persisting. To accomplish this we look in the cache (.Local) - // of the corresponding table (.Set()) for matching entries to our criteria. - var contextMetadata = context.Set().Local.SingleOrDefault(e => e.Equals(beatmap.Metadata)); - if (contextMetadata != null) - beatmap.Metadata = contextMetadata; - else - context.BeatmapMetadata.Attach(beatmap.Metadata); - } - - context.BeatmapSetInfo.Attach(beatmapSet); - context.SaveChanges(); - - BeatmapSetAdded?.Invoke(beatmapSet); - } - - /// - /// Delete a from the database. - /// - /// The beatmap to delete. - /// Whether the beatmap's was changed. - public bool Delete(BeatmapSetInfo beatmapSet) - { - var context = GetContext(); - - Refresh(ref beatmapSet, BeatmapSets); - - if (beatmapSet.DeletePending) return false; - beatmapSet.DeletePending = true; - context.SaveChanges(); - - BeatmapSetRemoved?.Invoke(beatmapSet); - return true; - } - - /// - /// Restore a previously deleted . - /// - /// The beatmap to restore. - /// Whether the beatmap's was changed. - public bool Undelete(BeatmapSetInfo beatmapSet) - { - var context = GetContext(); - - Refresh(ref beatmapSet, BeatmapSets); - - if (!beatmapSet.DeletePending) return false; - beatmapSet.DeletePending = false; - context.SaveChanges(); - - BeatmapSetAdded?.Invoke(beatmapSet); - return true; - } - /// /// Hide a in the database. /// @@ -95,13 +29,13 @@ namespace osu.Game.Beatmaps /// Whether the beatmap's was changed. public bool Hide(BeatmapInfo beatmap) { - var context = GetContext(); + using (ContextFactory.GetForWrite()) + { + Refresh(ref beatmap, Beatmaps); - Refresh(ref beatmap, Beatmaps); - - if (beatmap.Hidden) return false; - beatmap.Hidden = true; - context.SaveChanges(); + if (beatmap.Hidden) return false; + beatmap.Hidden = true; + } BeatmapHidden?.Invoke(beatmap); return true; @@ -114,51 +48,50 @@ namespace osu.Game.Beatmaps /// Whether the beatmap's was changed. public bool Restore(BeatmapInfo beatmap) { - var context = GetContext(); + using (ContextFactory.GetForWrite()) + { + Refresh(ref beatmap, Beatmaps); - Refresh(ref beatmap, Beatmaps); - - if (!beatmap.Hidden) return false; - beatmap.Hidden = false; - context.SaveChanges(); + if (!beatmap.Hidden) return false; + beatmap.Hidden = false; + } BeatmapRestored?.Invoke(beatmap); return true; } - public override void Cleanup() + protected override IQueryable AddIncludesForDeletion(IQueryable query) => + base.AddIncludesForDeletion(query) + .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) + .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) + .Include(s => s.Metadata); + + protected override IQueryable AddIncludesForConsumption(IQueryable query) => + base.AddIncludesForConsumption(query) + .Include(s => s.Metadata) + .Include(s => s.Beatmaps).ThenInclude(s => s.Ruleset) + .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) + .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) + .Include(s => s.Files).ThenInclude(f => f.FileInfo); + + protected override void Purge(List items, OsuDbContext context) { - var context = GetContext(); - - var purgeable = context.BeatmapSetInfo.Where(s => s.DeletePending && !s.Protected) - .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) - .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) - .Include(s => s.Metadata); - // metadata is M-N so we can't rely on cascades - context.BeatmapMetadata.RemoveRange(purgeable.Select(s => s.Metadata)); - context.BeatmapMetadata.RemoveRange(purgeable.SelectMany(s => s.Beatmaps.Select(b => b.Metadata).Where(m => m != null))); + context.BeatmapMetadata.RemoveRange(items.Select(s => s.Metadata)); + context.BeatmapMetadata.RemoveRange(items.SelectMany(s => s.Beatmaps.Select(b => b.Metadata).Where(m => m != null))); // todo: we can probably make cascades work here with a FK in BeatmapDifficulty. just make to make it work correctly. - context.BeatmapDifficulty.RemoveRange(purgeable.SelectMany(s => s.Beatmaps.Select(b => b.BaseDifficulty))); + context.BeatmapDifficulty.RemoveRange(items.SelectMany(s => s.Beatmaps.Select(b => b.BaseDifficulty))); - // cascades down to beatmaps. - context.BeatmapSetInfo.RemoveRange(purgeable); - context.SaveChanges(); + base.Purge(items, context); } - public IQueryable BeatmapSets => GetContext().BeatmapSetInfo - .Include(s => s.Metadata) - .Include(s => s.Beatmaps).ThenInclude(s => s.Ruleset) - .Include(s => s.Beatmaps).ThenInclude(b => b.BaseDifficulty) - .Include(s => s.Beatmaps).ThenInclude(b => b.Metadata) - .Include(s => s.Files).ThenInclude(f => f.FileInfo); - - public IQueryable Beatmaps => GetContext().BeatmapInfo - .Include(b => b.BeatmapSet).ThenInclude(s => s.Metadata) - .Include(b => b.BeatmapSet).ThenInclude(s => s.Files).ThenInclude(f => f.FileInfo) - .Include(b => b.Metadata) - .Include(b => b.Ruleset) - .Include(b => b.BaseDifficulty); + public IQueryable Beatmaps => + ContextFactory.Get().BeatmapInfo + .Include(b => b.BeatmapSet).ThenInclude(s => s.Metadata) + .Include(b => b.BeatmapSet).ThenInclude(s => s.Files).ThenInclude(f => f.FileInfo) + .Include(b => b.Metadata) + .Include(b => b.Ruleset) + .Include(b => b.BaseDifficulty); } } diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index 69027ffd73..2b42553891 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -1,6 +1,8 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using OpenTK; + namespace osu.Game.Beatmaps.ControlPoints { public class DifficultyControlPoint : ControlPoint @@ -8,6 +10,12 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// The speed multiplier at this control point. /// - public double SpeedMultiplier = 1; + public double SpeedMultiplier + { + get => speedMultiplier; + set => speedMultiplier = MathHelper.Clamp(value, 0.1, 10); + } + + private double speedMultiplier = 1; } } diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index 0592ef38c5..0db1f08a90 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using OpenTK; using osu.Game.Beatmaps.Timing; namespace osu.Game.Beatmaps.ControlPoints @@ -15,6 +16,12 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// The beat length at this control point. /// - public double BeatLength = 1000; + public double BeatLength + { + get => beatLength; + set => beatLength = MathHelper.Clamp(value, 6, 60000); + } + + private double beatLength = 1000; } } diff --git a/osu.Game/Beatmaps/DifficultyCalculator.cs b/osu.Game/Beatmaps/DifficultyCalculator.cs index 798268d05f..2bea31c0d3 100644 --- a/osu.Game/Beatmaps/DifficultyCalculator.cs +++ b/osu.Game/Beatmaps/DifficultyCalculator.cs @@ -24,9 +24,15 @@ namespace osu.Game.Beatmaps protected DifficultyCalculator(Beatmap beatmap, Mod[] mods = null) { - Beatmap = CreateBeatmapConverter(beatmap).Convert(beatmap); Mods = mods ?? new Mod[0]; + var converter = CreateBeatmapConverter(beatmap); + + foreach (var mod in Mods.OfType>()) + mod.ApplyToBeatmapConverter(converter); + + Beatmap = converter.Convert(beatmap); + ApplyMods(Mods); PreprocessHitObjects(); @@ -41,12 +47,12 @@ namespace osu.Game.Beatmaps foreach (var mod in Mods.OfType()) mod.ApplyToDifficulty(Beatmap.BeatmapInfo.BaseDifficulty); + foreach (var h in Beatmap.HitObjects) + h.ApplyDefaults(Beatmap.ControlPointInfo, Beatmap.BeatmapInfo.BaseDifficulty); + foreach (var mod in mods.OfType>()) foreach (var obj in Beatmap.HitObjects) mod.ApplyToHitObject(obj); - - foreach (var h in Beatmap.HitObjects) - h.ApplyDefaults(Beatmap.ControlPointInfo, Beatmap.BeatmapInfo.BaseDifficulty); } protected virtual void PreprocessHitObjects() diff --git a/osu.Game/Beatmaps/Formats/Decoder.cs b/osu.Game/Beatmaps/Formats/Decoder.cs index 1aae52208a..9f10485c5f 100644 --- a/osu.Game/Beatmaps/Formats/Decoder.cs +++ b/osu.Game/Beatmaps/Formats/Decoder.cs @@ -4,38 +4,64 @@ using System; using System.Collections.Generic; using System.IO; -using osu.Game.Storyboards; +using System.Linq; namespace osu.Game.Beatmaps.Formats { + public abstract class Decoder : Decoder + where TOutput : new() + { + protected virtual TOutput CreateTemplateObject() => new TOutput(); + + public TOutput Decode(StreamReader primaryStream, params StreamReader[] otherStreams) + { + var output = CreateTemplateObject(); + foreach (StreamReader stream in new[] { primaryStream }.Concat(otherStreams)) + ParseStreamInto(stream, output); + return output; + } + + protected abstract void ParseStreamInto(StreamReader stream, TOutput beatmap); + } + public abstract class Decoder { - private static readonly Dictionary> decoders = new Dictionary>(); + private static readonly Dictionary>> decoders = new Dictionary>>(); static Decoder() { - LegacyDecoder.Register(); + LegacyBeatmapDecoder.Register(); JsonBeatmapDecoder.Register(); + LegacyStoryboardDecoder.Register(); } /// /// Retrieves a to parse a . /// /// A stream pointing to the . - public static Decoder GetDecoder(StreamReader stream) + public static Decoder GetDecoder(StreamReader stream) + where T : new() { if (stream == null) throw new ArgumentNullException(nameof(stream)); + if (!decoders.TryGetValue(typeof(T), out var typedDecoders)) + throw new IOException(@"Unknown decoder type"); + string line; do - { line = stream.ReadLine()?.Trim(); } - while (line != null && line.Length == 0); + { + line = stream.ReadLine()?.Trim(); + } while (line != null && line.Length == 0); - if (line == null || !decoders.ContainsKey(line)) + if (line == null) throw new IOException(@"Unknown file format"); - return decoders[line](line); + var decoder = typedDecoders.Select(d => line.StartsWith(d.Key) ? d.Value : null).FirstOrDefault(); + if (decoder == null) + throw new IOException(@"Unknown file format"); + + return (Decoder)decoder.Invoke(line); } /// @@ -43,41 +69,12 @@ namespace osu.Game.Beatmaps.Formats /// /// A string in the file which triggers this decoder to be used. /// A function which constructs the given . - protected static void AddDecoder(string magic, Func constructor) + protected static void AddDecoder(string magic, Func constructor) { - decoders[magic] = constructor; + if (!decoders.TryGetValue(typeof(T), out var typedDecoders)) + decoders.Add(typeof(T), typedDecoders = new Dictionary>()); + + typedDecoders[magic] = constructor; } - - /// - /// 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(params StreamReader[] streams) - { - var storyboard = new Storyboard(); - foreach (StreamReader stream in streams) - ParseStoryboard(stream, storyboard); - return storyboard; - } - - protected abstract void ParseStoryboard(StreamReader stream, Storyboard storyboard); } } diff --git a/osu.Game/Beatmaps/Formats/IHasComboColours.cs b/osu.Game/Beatmaps/Formats/IHasComboColours.cs new file mode 100644 index 0000000000..93c6c18eec --- /dev/null +++ b/osu.Game/Beatmaps/Formats/IHasComboColours.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using OpenTK.Graphics; + +namespace osu.Game.Beatmaps.Formats +{ + public interface IHasComboColours + { + List ComboColours { get; set; } + } +} diff --git a/osu.Game/Beatmaps/Formats/IHasCustomColours.cs b/osu.Game/Beatmaps/Formats/IHasCustomColours.cs new file mode 100644 index 0000000000..14614a6728 --- /dev/null +++ b/osu.Game/Beatmaps/Formats/IHasCustomColours.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using OpenTK.Graphics; + +namespace osu.Game.Beatmaps.Formats +{ + public interface IHasCustomColours + { + Dictionary CustomColours { get; set; } + } +} diff --git a/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs index b0798e5a87..add0f39280 100644 --- a/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/JsonBeatmapDecoder.cs @@ -3,20 +3,17 @@ using System.IO; using osu.Game.IO.Serialization; -using osu.Game.Storyboards; namespace osu.Game.Beatmaps.Formats { - public class JsonBeatmapDecoder : Decoder + public class JsonBeatmapDecoder : Decoder { public static void Register() { - AddDecoder("{", m => new JsonBeatmapDecoder()); + AddDecoder("{", m => new JsonBeatmapDecoder()); } - public override Decoder GetStoryboardDecoder() => this; - - protected override void ParseBeatmap(StreamReader stream, Beatmap beatmap) + protected override void ParseStreamInto(StreamReader stream, Beatmap beatmap) { stream.BaseStream.Position = 0; stream.DiscardBufferedData(); @@ -26,10 +23,5 @@ namespace osu.Game.Beatmaps.Formats foreach (var hitObject in beatmap.HitObjects) hitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty); } - - protected override void ParseStoryboard(StreamReader stream, Storyboard storyboard) - { - // throw new System.NotImplementedException(); - } } } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 3847787a4c..74b7d0272e 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -4,89 +4,99 @@ using System; using System.Globalization; using System.IO; -using OpenTK.Graphics; +using System.Linq; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Beatmaps.ControlPoints; +using osu.Framework; namespace osu.Game.Beatmaps.Formats { - public class LegacyBeatmapDecoder : LegacyDecoder + public class LegacyBeatmapDecoder : LegacyDecoder { + public const int LATEST_VERSION = 14; + private Beatmap beatmap; - private bool hasCustomColours; private ConvertHitObjectParser parser; private LegacySampleBank defaultSampleBank; private int defaultSampleVolume = 100; - public LegacyBeatmapDecoder() + public static void Register() { + AddDecoder(@"osu file format v", m => new LegacyBeatmapDecoder(int.Parse(m.Split('v').Last()))); } - public LegacyBeatmapDecoder(string header) + /// + /// lazer's audio timings in general doesn't match stable. this is the result of user testing, albeit limited. + /// This only seems to be required on windows. We need to eventually figure out why, with a bit of luck. + /// + public static int UniversalOffset => RuntimeInfo.OS == RuntimeInfo.Platform.Windows ? -22 : 0; + + /// + /// Whether or not beatmap or runtime offsets should be applied. Defaults on; only disable for testing purposes. + /// + public bool ApplyOffsets = true; + + private readonly int offset = UniversalOffset; + + public LegacyBeatmapDecoder(int version = LATEST_VERSION) : base(version) { - BeatmapVersion = int.Parse(header.Substring(17)); + // BeatmapVersion 4 and lower had an incorrect offset (stable has this set as 24ms off) + offset += FormatVersion < 5 ? 24 : 0; } - protected override void ParseBeatmap(StreamReader stream, Beatmap beatmap) + protected override void ParseStreamInto(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; + this.beatmap.BeatmapInfo.BeatmapVersion = FormatVersion; - ParseContent(stream); + base.ParseStreamInto(stream, beatmap); + + // objects may be out of order *only* if a user has manually edited an .osu file. + // unfortunately there are ranked maps in this state (example: https://osu.ppy.sh/s/594828). + this.beatmap.HitObjects.Sort((x, y) => x.StartTime.CompareTo(y.StartTime)); 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 bool ShouldSkipLine(string line) => base.ShouldSkipLine(line) || line.StartsWith(" ") || line.StartsWith("_"); - protected override void ProcessSection(Section section, string line) + protected override void ParseLine(Beatmap beatmap, Section section, string line) { switch (section) { case Section.General: handleGeneral(line); - break; + return; case Section.Editor: handleEditor(line); - break; + return; case Section.Metadata: handleMetadata(line); - break; + return; case Section.Difficulty: handleDifficulty(line); - break; + return; case Section.Events: handleEvents(line); - break; + return; case Section.TimingPoints: handleTimingPoints(line); - break; - case Section.Colours: - handleColours(line); - break; + return; case Section.HitObjects: handleHitObjects(line); - break; + return; } + + base.ParseLine(beatmap, section, line); } private void handleGeneral(string line) { - var pair = SplitKeyVal(line, ':'); + var pair = SplitKeyVal(line); var metadata = beatmap.BeatmapInfo.Metadata; switch (pair.Key) @@ -98,7 +108,7 @@ namespace osu.Game.Beatmaps.Formats beatmap.BeatmapInfo.AudioLeadIn = int.Parse(pair.Value); break; case @"PreviewTime": - metadata.PreviewTime = int.Parse(pair.Value); + metadata.PreviewTime = getOffsetTime(int.Parse(pair.Value)); break; case @"Countdown": beatmap.BeatmapInfo.Countdown = int.Parse(pair.Value) == 1; @@ -145,7 +155,7 @@ namespace osu.Game.Beatmaps.Formats private void handleEditor(string line) { - var pair = SplitKeyVal(line, ':'); + var pair = SplitKeyVal(line); switch (pair.Key) { @@ -169,7 +179,7 @@ namespace osu.Game.Beatmaps.Formats private void handleMetadata(string line) { - var pair = SplitKeyVal(line, ':'); + var pair = SplitKeyVal(line); var metadata = beatmap.BeatmapInfo.Metadata; switch (pair.Key) @@ -210,7 +220,7 @@ namespace osu.Game.Beatmaps.Formats private void handleDifficulty(string line) { - var pair = SplitKeyVal(line, ':'); + var pair = SplitKeyVal(line); var difficulty = beatmap.BeatmapInfo.BaseDifficulty; switch (pair.Key) @@ -228,10 +238,10 @@ namespace osu.Game.Beatmaps.Formats difficulty.ApproachRate = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); break; case @"SliderMultiplier": - difficulty.SliderMultiplier = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + difficulty.SliderMultiplier = double.Parse(pair.Value, NumberFormatInfo.InvariantInfo); break; case @"SliderTickRate": - difficulty.SliderTickRate = float.Parse(pair.Value, NumberFormatInfo.InvariantInfo); + difficulty.SliderTickRate = double.Parse(pair.Value, NumberFormatInfo.InvariantInfo); break; } } @@ -253,8 +263,8 @@ namespace osu.Game.Beatmaps.Formats case EventType.Break: var breakEvent = new BreakPeriod { - StartTime = double.Parse(split[1], NumberFormatInfo.InvariantInfo), - EndTime = double.Parse(split[2], NumberFormatInfo.InvariantInfo) + StartTime = getOffsetTime(double.Parse(split[1], NumberFormatInfo.InvariantInfo)), + EndTime = getOffsetTime(double.Parse(split[2], NumberFormatInfo.InvariantInfo)) }; if (!breakEvent.HasEffect) @@ -269,7 +279,7 @@ namespace osu.Game.Beatmaps.Formats { string[] split = line.Split(','); - double time = double.Parse(split[0].Trim(), NumberFormatInfo.InvariantInfo); + double time = getOffsetTime(double.Parse(split[0].Trim(), NumberFormatInfo.InvariantInfo)); double beatLength = double.Parse(split[1].Trim(), NumberFormatInfo.InvariantInfo); double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1; @@ -351,38 +361,6 @@ namespace osu.Game.Beatmaps.Formats } } - 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. @@ -392,7 +370,14 @@ namespace osu.Game.Beatmaps.Formats var obj = parser.Parse(line); if (obj != null) + { + obj.StartTime = getOffsetTime(obj.StartTime); beatmap.HitObjects.Add(obj); + } } + + private int getOffsetTime(int time) => time + (ApplyOffsets ? offset : 0); + + private double getOffsetTime(double time) => time + (ApplyOffsets ? offset : 0); } } diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index e0fc439924..131c010c5c 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -4,47 +4,22 @@ using System; using System.Collections.Generic; using System.IO; -using osu.Game.Beatmaps.Legacy; -using osu.Game.Storyboards; +using osu.Framework.Logging; +using OpenTK.Graphics; namespace osu.Game.Beatmaps.Formats { - public abstract class LegacyDecoder : Decoder + public abstract class LegacyDecoder : Decoder + where T : new() { - public static void Register() + protected readonly int FormatVersion; + + protected LegacyDecoder(int version) { - AddDecoder(@"osu file format v14", m => new LegacyBeatmapDecoder(m)); - AddDecoder(@"osu file format v13", m => new LegacyBeatmapDecoder(m)); - AddDecoder(@"osu file format v12", m => new LegacyBeatmapDecoder(m)); - AddDecoder(@"osu file format v11", m => new LegacyBeatmapDecoder(m)); - AddDecoder(@"osu file format v10", m => new LegacyBeatmapDecoder(m)); - AddDecoder(@"osu file format v9", m => new LegacyBeatmapDecoder(m)); - AddDecoder(@"osu file format v8", m => new LegacyBeatmapDecoder(m)); - AddDecoder(@"osu file format v7", m => new LegacyBeatmapDecoder(m)); - AddDecoder(@"osu file format v6", m => new LegacyBeatmapDecoder(m)); - AddDecoder(@"osu file format v5", m => new LegacyBeatmapDecoder(m)); - AddDecoder(@"osu file format v4", m => new LegacyBeatmapDecoder(m)); - AddDecoder(@"osu file format v3", m => new LegacyBeatmapDecoder(m)); - // TODO: differences between versions + FormatVersion = version; } - protected int BeatmapVersion; - - 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) + protected override void ParseStreamInto(StreamReader stream, T beatmap) { Section section = Section.None; @@ -54,34 +29,72 @@ namespace osu.Game.Beatmaps.Formats 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}"); + { + Logger.Log($"Unknown section \"{line}\" in {beatmap}"); + section = Section.None; + } + continue; } - ProcessSection(section, line); + ParseLine(beatmap, section, line); } } - protected virtual bool ShouldSkipLine(string line) + protected virtual bool ShouldSkipLine(string line) => string.IsNullOrWhiteSpace(line) || line.StartsWith("//"); + + protected virtual void ParseLine(T output, Section section, string line) { - if (string.IsNullOrWhiteSpace(line) || line.StartsWith("//")) - return true; - return false; + switch (section) + { + case Section.Colours: + handleColours(output, line); + return; + } } - protected abstract void ProcessSection(Section section, string line); + private bool hasComboColours; - protected KeyValuePair SplitKeyVal(string line, char separator) + private void handleColours(T output, string line) + { + var pair = SplitKeyVal(line); + + bool isCombo = pair.Key.StartsWith(@"Combo"); + + string[] split = pair.Value.Split(','); + + if (split.Length != 3) + throw new InvalidOperationException($@"Color specified in incorrect format (should be R,G,B): {pair.Value}"); + + if (!byte.TryParse(split[0], out var r) || !byte.TryParse(split[1], out var g) || !byte.TryParse(split[2], out var b)) + throw new InvalidOperationException(@"Color must be specified with 8-bit integer components"); + + Color4 colour = new Color4(r, g, b, 255); + + if (isCombo) + { + if (!(output is IHasComboColours tHasComboColours)) return; + + if (!hasComboColours) + { + // remove default colours. + tHasComboColours.ComboColours.Clear(); + hasComboColours = true; + } + + tHasComboColours.ComboColours.Add(colour); + } + else + { + if (!(output is IHasCustomColours tHasCustomColours)) return; + tHasCustomColours.CustomColours[pair.Key] = colour; + } + } + + protected KeyValuePair SplitKeyVal(string line, char separator = ':') { var split = line.Trim().Split(new[] { separator }, 2); @@ -104,6 +117,7 @@ namespace osu.Game.Beatmaps.Formats Colours, HitObjects, Variables, + Fonts } internal enum LegacySampleBank diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index a4ff060c83..85b0f8d42e 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -2,7 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Globalization; using System.IO; using OpenTK; @@ -13,47 +13,46 @@ using osu.Game.Storyboards; namespace osu.Game.Beatmaps.Formats { - public class LegacyStoryboardDecoder : LegacyDecoder + public class LegacyStoryboardDecoder : LegacyDecoder { - private Storyboard storyboard; - private StoryboardSprite storyboardSprite; private CommandTimelineGroup timelineGroup; + private Storyboard storyboard; + private readonly Dictionary variables = new Dictionary(); public LegacyStoryboardDecoder() + : base(0) { } - public LegacyStoryboardDecoder(int beatmapVersion) + public static void Register() { - BeatmapVersion = beatmapVersion; + // note that this isn't completely correct + AddDecoder(@"osu file format v", m => new LegacyStoryboardDecoder()); + AddDecoder(@"[Events]", m => new LegacyStoryboardDecoder()); } - protected override void ParseStoryboard(StreamReader stream, Storyboard storyboard) + protected override void ParseStreamInto(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); + base.ParseStreamInto(stream, storyboard); } - protected override void ProcessSection(Section section, string line) + protected override void ParseLine(Storyboard storyboard, Section section, string line) { switch (section) { case Section.Events: handleEvents(line); - break; + return; case Section.Variables: handleVariables(line); - break; + return; } + + base.ParseLine(storyboard, section, line); } private void handleEvents(string line) @@ -80,38 +79,38 @@ namespace osu.Game.Beatmaps.Formats 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); - } + { + 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); - } + { + 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)); - } + { + 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; } } @@ -124,120 +123,120 @@ namespace osu.Game.Beatmaps.Formats 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); - } + { + 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); - } + { + 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) { - 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": { - 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}"); + 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; } } @@ -269,6 +268,7 @@ namespace osu.Game.Beatmaps.Formats case LegacyOrigins.BottomRight: return Anchor.BottomRight; } + throw new InvalidDataException($@"Unknown origin: {value}"); } diff --git a/osu.Game/Beatmaps/IBeatmapConverter.cs b/osu.Game/Beatmaps/IBeatmapConverter.cs new file mode 100644 index 0000000000..ebd900b97e --- /dev/null +++ b/osu.Game/Beatmaps/IBeatmapConverter.cs @@ -0,0 +1,25 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Beatmaps +{ + public interface IBeatmapConverter + { + /// + /// Invoked when a has been converted. + /// The first argument contains the that was converted. + /// The second argument contains the s that were output from the conversion process. + /// + event Action> ObjectConverted; + + /// + /// Converts a Beatmap using this Beatmap Converter. + /// + /// The un-converted Beatmap. + void Convert(Beatmap beatmap); + } +} diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index c633b94951..5c0ad7685b 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -14,6 +14,7 @@ using osu.Framework.IO.File; using System.IO; using osu.Game.IO.Serialization; using System.Diagnostics; +using osu.Game.Skinning; namespace osu.Game.Beatmaps { @@ -40,6 +41,7 @@ namespace osu.Game.Beatmaps track = new AsyncLazy(populateTrack); waveform = new AsyncLazy(populateWaveform); storyboard = new AsyncLazy(populateStoryboard); + skin = new AsyncLazy(populateSkin); } /// @@ -56,8 +58,9 @@ namespace osu.Game.Beatmaps protected abstract Beatmap GetBeatmap(); protected abstract Texture GetBackground(); protected abstract Track GetTrack(); + protected virtual Skin GetSkin() => new DefaultSkin(); protected virtual Waveform GetWaveform() => new Waveform(); - protected virtual Storyboard GetStoryboard() => new Storyboard(); + protected virtual Storyboard GetStoryboard() => new Storyboard { BeatmapInfo = BeatmapInfo }; public bool BeatmapLoaded => beatmap.IsResultAvailable; public Beatmap Beatmap => beatmap.Value.Result; @@ -109,6 +112,13 @@ namespace osu.Game.Beatmaps private Storyboard populateStoryboard() => GetStoryboard(); + public bool SkinLoaded => skin.IsResultAvailable; + public Skin Skin => skin.Value.Result; + public async Task GetSkinAsync() => await skin.Value; + private readonly AsyncLazy skin; + + private Skin populateSkin() => GetSkin(); + public void TransferTo(WorkingBeatmap other) { if (track.IsResultAvailable && Track != null && BeatmapInfo.AudioEquals(other.BeatmapInfo)) @@ -123,6 +133,7 @@ namespace osu.Game.Beatmaps if (BackgroundLoaded) Background?.Dispose(); if (WaveformLoaded) Waveform?.Dispose(); if (StoryboardLoaded) Storyboard?.Dispose(); + if (SkinLoaded) Skin?.Dispose(); } /// diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index c33dd91330..70260b349e 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -14,6 +14,8 @@ namespace osu.Game.Configuration { // UI/selection defaults Set(OsuSetting.Ruleset, 0, 0, int.MaxValue); + Set(OsuSetting.Skin, 0, 0, int.MaxValue); + Set(OsuSetting.BeatmapDetailTab, BeatmapDetailTab.Details); Set(OsuSetting.ShowConvertedBeatmaps, true); @@ -80,6 +82,8 @@ namespace osu.Game.Configuration Set(OsuSetting.ReleaseStream, ReleaseStream.Lazer); Set(OsuSetting.Version, string.Empty); + + Set(OsuSetting.ScreenshotFormat, ScreenshotFormat.Jpg); } public OsuConfigManager(Storage storage) : base(storage) @@ -122,6 +126,8 @@ namespace osu.Game.Configuration ChatDisplayHeight, Version, ShowConvertedBeatmaps, - SpeedChangeVisualisation + SpeedChangeVisualisation, + Skin, + ScreenshotFormat } } diff --git a/osu.Game/Configuration/ScreenshotFormat.cs b/osu.Game/Configuration/ScreenshotFormat.cs index 1bc3013af9..b9309fae3a 100644 --- a/osu.Game/Configuration/ScreenshotFormat.cs +++ b/osu.Game/Configuration/ScreenshotFormat.cs @@ -7,7 +7,6 @@ namespace osu.Game.Configuration { public enum ScreenshotFormat { - Bmp = 0, // TODO: Figure out the best way to hide this from the dropdown [Description("JPG (web-friendly)")] Jpg = 1, [Description("PNG (lossless)")] diff --git a/osu.Game/Configuration/SettingsStore.cs b/osu.Game/Configuration/SettingsStore.cs index 9b18151c84..7b66002a79 100644 --- a/osu.Game/Configuration/SettingsStore.cs +++ b/osu.Game/Configuration/SettingsStore.cs @@ -12,8 +12,8 @@ namespace osu.Game.Configuration { public event Action SettingChanged; - public SettingsStore(Func createContext) - : base(createContext) + public SettingsStore(DatabaseContextFactory contextFactory) + : base(contextFactory) { } @@ -24,19 +24,16 @@ namespace osu.Game.Configuration /// An optional variant. /// public List Query(int? rulesetId = null, int? variant = null) => - GetContext().DatabasedSetting.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); + ContextFactory.Get().DatabasedSetting.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); public void Update(DatabasedSetting setting) { - var context = GetContext(); - - var newValue = setting.Value; - - Refresh(ref setting); - - setting.Value = newValue; - - context.SaveChanges(); + using (ContextFactory.GetForWrite()) + { + var newValue = setting.Value; + Refresh(ref setting); + setting.Value = newValue; + } SettingChanged?.Invoke(); } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs new file mode 100644 index 0000000000..f0e67a7185 --- /dev/null +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -0,0 +1,346 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Ionic.Zip; +using Microsoft.EntityFrameworkCore; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.IO; +using osu.Game.IO.Archives; +using osu.Game.IPC; +using osu.Game.Overlays.Notifications; +using SharpCompress.Common; +using FileInfo = osu.Game.IO.FileInfo; + +namespace osu.Game.Database +{ + /// + /// Encapsulates a model store class to give it import functionality. + /// Adds cross-functionality with to give access to the central file store for the provided model. + /// + /// The model type. + /// The associated file join type. + public abstract class ArchiveModelManager : ICanAcceptFiles + where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete + where TFileModel : INamedFileInfo, new() + { + /// + /// Set an endpoint for notifications to be posted to. + /// + public Action PostNotification { protected get; set; } + + /// + /// Fired when a new becomes available in the database. + /// + public event Action ItemAdded; + + /// + /// Fired when a is removed from the database. + /// + public event Action ItemRemoved; + + public virtual string[] HandledExtensions => new[] { ".zip" }; + + protected readonly FileStore Files; + + protected readonly IDatabaseContextFactory ContextFactory; + + protected readonly MutableDatabaseBackedStore ModelStore; + + // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) + private ArchiveImportIPCChannel ipc; + + protected ArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, MutableDatabaseBackedStore modelStore, IIpcHost importHost = null) + { + ContextFactory = contextFactory; + + ModelStore = modelStore; + ModelStore.ItemAdded += s => ItemAdded?.Invoke(s); + ModelStore.ItemRemoved += s => ItemRemoved?.Invoke(s); + + Files = new FileStore(contextFactory, storage); + + if (importHost != null) + ipc = new ArchiveImportIPCChannel(importHost, this); + + ModelStore.Cleanup(); + } + + /// + /// Import one or more items from filesystem . + /// This will post notifications tracking progress. + /// + /// One or more archive locations on disk. + public void Import(params string[] paths) + { + var notification = new ProgressNotification + { + Text = "Import is initialising...", + Progress = 0, + State = ProgressNotificationState.Active, + }; + + PostNotification?.Invoke(notification); + + List imported = new List(); + + int current = 0; + int errors = 0; + foreach (string path in paths) + { + if (notification.State == ProgressNotificationState.Cancelled) + // user requested abort + return; + + try + { + notification.Text = $"Importing ({++current} of {paths.Length})\n{Path.GetFileName(path)}"; + using (ArchiveReader reader = getReaderFrom(path)) + imported.Add(Import(reader)); + + notification.Progress = (float)current / paths.Length; + + // We may or may not want to delete the file depending on where it is stored. + // e.g. reconstructing/repairing database with items from default storage. + // Also, not always a single file, i.e. for LegacyFilesystemReader + // TODO: Add a check to prevent files from storage to be deleted. + try + { + if (File.Exists(path)) + File.Delete(path); + } + catch (Exception e) + { + Logger.Error(e, $@"Could not delete original file after import ({Path.GetFileName(path)})"); + } + } + catch (Exception e) + { + e = e.InnerException ?? e; + Logger.Error(e, $@"Could not import ({Path.GetFileName(path)})"); + errors++; + } + } + + notification.Text = errors > 0 ? $"Import complete with {errors} errors" : "Import successful!"; + notification.State = ProgressNotificationState.Completed; + } + + /// + /// Import an item from an . + /// + /// The archive to be imported. + public TModel Import(ArchiveReader archive) + { + using (ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes. + { + // create a new model (don't yet add to database) + var item = CreateModel(archive); + + var existing = CheckForExisting(item); + + if (existing != null) return existing; + + item.Files = createFileInfos(archive, Files); + + Populate(item, archive); + + // import to store + ModelStore.Add(item); + + return item; + } + } + + /// + /// Import an item from a . + /// + /// The model to be imported. + public void Import(TModel item) => ModelStore.Add(item); + + /// + /// Perform an update of the specified item. + /// TODO: Support file changes. + /// + /// The item to update. + public void Update(TModel item) => ModelStore.Update(item); + + /// + /// Delete an item from the manager. + /// Is a no-op for already deleted items. + /// + /// The item to delete. + public void Delete(TModel item) + { + using (var usage = ContextFactory.GetForWrite()) + { + var context = usage.Context; + + context.ChangeTracker.AutoDetectChangesEnabled = false; + + // re-fetch the model on the import context. + var foundModel = queryModel().Include(s => s.Files).ThenInclude(f => f.FileInfo).First(s => s.ID == item.ID); + + if (foundModel.DeletePending) return; + + if (ModelStore.Delete(foundModel)) + Files.Dereference(foundModel.Files.Select(f => f.FileInfo).ToArray()); + + context.ChangeTracker.AutoDetectChangesEnabled = true; + } + } + + /// + /// Delete multiple items. + /// This will post notifications tracking progress. + /// + public void Delete(List items) + { + if (items.Count == 0) return; + + var notification = new ProgressNotification + { + Progress = 0, + CompletionText = "Deleted all beatmaps!", + State = ProgressNotificationState.Active, + }; + + PostNotification?.Invoke(notification); + + int i = 0; + + using (ContextFactory.GetForWrite()) + { + foreach (var b in items) + { + if (notification.State == ProgressNotificationState.Cancelled) + // user requested abort + return; + + notification.Text = $"Deleting ({++i} of {items.Count})"; + + Delete(b); + + notification.Progress = (float)i / items.Count; + } + } + + notification.State = ProgressNotificationState.Completed; + } + + /// + /// Restore multiple items that were previously deleted. + /// This will post notifications tracking progress. + /// + public void Undelete(List items) + { + if (!items.Any()) return; + + var notification = new ProgressNotification + { + CompletionText = "Restored all deleted items!", + Progress = 0, + State = ProgressNotificationState.Active, + }; + + PostNotification?.Invoke(notification); + + int i = 0; + + using (ContextFactory.GetForWrite()) + { + foreach (var item in items) + { + if (notification.State == ProgressNotificationState.Cancelled) + // user requested abort + return; + + notification.Text = $"Restoring ({++i} of {items.Count})"; + + Undelete(item); + + notification.Progress = (float)i / items.Count; + } + } + + notification.State = ProgressNotificationState.Completed; + } + + /// + /// Restore an item that was previously deleted. Is a no-op if the item is not in a deleted state, or has its protected flag set. + /// + /// The item to restore + public void Undelete(TModel item) + { + using (var usage = ContextFactory.GetForWrite()) + { + usage.Context.ChangeTracker.AutoDetectChangesEnabled = false; + + if (!ModelStore.Undelete(item)) return; + + Files.Reference(item.Files.Select(f => f.FileInfo).ToArray()); + + usage.Context.ChangeTracker.AutoDetectChangesEnabled = true; + } + } + + /// + /// Create all required s for the provided archive, adding them to the global file store. + /// + private List createFileInfos(ArchiveReader reader, FileStore files) + { + var fileInfos = new List(); + + // import files to manager + foreach (string file in reader.Filenames) + using (Stream s = reader.GetStream(file)) + fileInfos.Add(new TFileModel + { + Filename = file, + FileInfo = files.Add(s) + }); + + return fileInfos; + } + + /// + /// Create a barebones model from the provided archive. + /// Actual expensive population should be done in ; this should just prepare for duplicate checking. + /// + /// The archive to create the model for. + /// A model populated with minimal information. + protected abstract TModel CreateModel(ArchiveReader archive); + + /// + /// Populate the provided model completely from the given archive. + /// After this method, the model should be in a state ready to commit to a store. + /// + /// The model to populate. + /// The archive to use as a reference for population. + protected virtual void Populate(TModel model, ArchiveReader archive) + { + } + + protected virtual TModel CheckForExisting(TModel model) => null; + + private DbSet queryModel() => ContextFactory.Get().Set(); + + /// + /// Creates an from a valid storage path. + /// + /// A file or folder path resolving the archive content. + /// A reader giving access to the archive's content. + private ArchiveReader getReaderFrom(string path) + { + if (ZipFile.IsZipFile(path)) + return new ZipArchiveReader(Files.Storage.GetStream(path), Path.GetFileName(path)); + if (Directory.Exists(path)) + return new LegacyFilesystemReader(path); + throw new InvalidFormatException($"{path} is not a valid archive"); + } + } +} diff --git a/osu.Game/Database/DatabaseBackedStore.cs b/osu.Game/Database/DatabaseBackedStore.cs index ec9967e097..0fafb77339 100644 --- a/osu.Game/Database/DatabaseBackedStore.cs +++ b/osu.Game/Database/DatabaseBackedStore.cs @@ -1,10 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; -using System.Collections.Generic; using System.Linq; -using System.Threading; using Microsoft.EntityFrameworkCore; using osu.Framework.Platform; @@ -17,9 +14,7 @@ namespace osu.Game.Database /// /// Create a new instance (separate from the shared context via for performing isolated operations. /// - protected readonly Func CreateContext; - - private readonly ThreadLocal queryContext; + protected readonly IDatabaseContextFactory ContextFactory; /// /// Refresh an instance potentially from a different thread with a local context-tracked instance. @@ -27,35 +22,26 @@ namespace osu.Game.Database /// The object to use as a reference when negotiating a local instance. /// An optional lookup source which will be used to query and populate a freshly retrieved replacement. If not provided, the refreshed object will still be returned but will not have any includes. /// A valid EF-stored type. - protected virtual void Refresh(ref T obj, IEnumerable lookupSource = null) where T : class, IHasPrimaryKey + protected virtual void Refresh(ref T obj, IQueryable lookupSource = null) where T : class, IHasPrimaryKey { - var context = GetContext(); - - if (context.Entry(obj).State != EntityState.Detached) return; - - var id = obj.ID; - var foundObject = lookupSource?.SingleOrDefault(t => t.ID == id) ?? context.Find(id); - if (foundObject != null) + using (var usage = ContextFactory.GetForWrite()) { - obj = foundObject; - context.Entry(obj).Reload(); + var context = usage.Context; + + if (context.Entry(obj).State != EntityState.Detached) return; + + var id = obj.ID; + var foundObject = lookupSource?.SingleOrDefault(t => t.ID == id) ?? context.Find(id); + if (foundObject != null) + obj = foundObject; + else + context.Add(obj); } - else - context.Add(obj); } - /// - /// Retrieve a shared context for performing lookups (or write operations on the update thread, for now). - /// - protected OsuDbContext GetContext() => queryContext.Value; - - protected DatabaseBackedStore(Func createContext, Storage storage = null) + protected DatabaseBackedStore(IDatabaseContextFactory contextFactory, Storage storage = null) { - CreateContext = createContext; - - // todo: while this seems to work quite well, we need to consider that contexts could enter a state where they are never cleaned up. - queryContext = new ThreadLocal(CreateContext); - + ContextFactory = contextFactory; Storage = storage; } diff --git a/osu.Game/Database/DatabaseContextFactory.cs b/osu.Game/Database/DatabaseContextFactory.cs index b1917d92c4..712ed2d0cc 100644 --- a/osu.Game/Database/DatabaseContextFactory.cs +++ b/osu.Game/Database/DatabaseContextFactory.cs @@ -1,27 +1,94 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Threading; using osu.Framework.Platform; namespace osu.Game.Database { - public class DatabaseContextFactory + public class DatabaseContextFactory : IDatabaseContextFactory { private readonly GameHost host; private const string database_name = @"client"; + private ThreadLocal threadContexts; + + private readonly object writeLock = new object(); + + private bool currentWriteDidWrite; + private volatile int currentWriteUsages; + public DatabaseContextFactory(GameHost host) { this.host = host; + recycleThreadContexts(); } - public OsuDbContext GetContext() => new OsuDbContext(host.Storage.GetDatabaseConnectionString(database_name)); + /// + /// Get a context for the current thread for read-only usage. + /// If a is in progress, the existing write-safe context will be returned. + /// + public OsuDbContext Get() => threadContexts.Value; + + /// + /// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying context). + /// This method may block if a write is already active on a different thread. + /// + /// A usage containing a usable context. + public DatabaseWriteUsage GetForWrite() + { + Monitor.Enter(writeLock); + + Interlocked.Increment(ref currentWriteUsages); + + return new DatabaseWriteUsage(threadContexts.Value, usageCompleted); + } + + private void usageCompleted(DatabaseWriteUsage usage) + { + int usages = Interlocked.Decrement(ref currentWriteUsages); + + try + { + currentWriteDidWrite |= usage.PerformedWrite; + + if (usages > 0) return; + + if (currentWriteDidWrite) + { + // explicitly dispose to ensure any outstanding flushes happen as soon as possible (and underlying resources are purged). + usage.Context.Dispose(); + + currentWriteDidWrite = false; + + // once all writes are complete, we want to refresh thread-specific contexts to make sure they don't have stale local caches. + recycleThreadContexts(); + } + } + finally + { + Monitor.Exit(writeLock); + } + } + + private void recycleThreadContexts() => threadContexts = new ThreadLocal(CreateContext); + + protected virtual OsuDbContext CreateContext() + { + var ctx = new OsuDbContext(host.Storage.GetDatabaseConnectionString(database_name)); + ctx.Database.AutoTransactionsEnabled = false; + + return ctx; + } public void ResetDatabase() { - // todo: we probably want to make sure there are no active contexts before performing this operation. - host.Storage.DeleteDatabase(database_name); + lock (writeLock) + { + recycleThreadContexts(); + host.Storage.DeleteDatabase(database_name); + } } } } diff --git a/osu.Game/Database/DatabaseWriteUsage.cs b/osu.Game/Database/DatabaseWriteUsage.cs new file mode 100644 index 0000000000..52dd0ee268 --- /dev/null +++ b/osu.Game/Database/DatabaseWriteUsage.cs @@ -0,0 +1,46 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using Microsoft.EntityFrameworkCore.Storage; + +namespace osu.Game.Database +{ + public class DatabaseWriteUsage : IDisposable + { + public readonly OsuDbContext Context; + private readonly IDbContextTransaction transaction; + private readonly Action usageCompleted; + + public DatabaseWriteUsage(OsuDbContext context, Action onCompleted) + { + Context = context; + transaction = Context.BeginTransaction(); + usageCompleted = onCompleted; + } + + public bool PerformedWrite { get; private set; } + + private bool isDisposed; + + protected void Dispose(bool disposing) + { + if (isDisposed) return; + isDisposed = true; + + PerformedWrite |= Context.SaveChanges(transaction) > 0; + usageCompleted?.Invoke(this); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + ~DatabaseWriteUsage() + { + Dispose(false); + } + } +} diff --git a/osu.Game/Database/ICanAcceptFiles.cs b/osu.Game/Database/ICanAcceptFiles.cs new file mode 100644 index 0000000000..ab26525619 --- /dev/null +++ b/osu.Game/Database/ICanAcceptFiles.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Database +{ + /// + /// A class which can accept files for importing. + /// + public interface ICanAcceptFiles + { + /// + /// Import the specified paths. + /// + /// The files which should be imported. + void Import(params string[] paths); + + /// + /// An array of accepted file extensions (in the standard format of ".abc"). + /// + string[] HandledExtensions { get; } + } +} diff --git a/osu.Game/Database/IDatabaseContextFactory.cs b/osu.Game/Database/IDatabaseContextFactory.cs new file mode 100644 index 0000000000..bc1bc0349c --- /dev/null +++ b/osu.Game/Database/IDatabaseContextFactory.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Database +{ + public interface IDatabaseContextFactory + { + /// + /// Get a context for read-only usage. + /// + OsuDbContext Get(); + + /// + /// Request a context for write usage. Can be consumed in a nested fashion (and will return the same underlying context). + /// This method may block if a write is already active on a different thread. + /// + /// A usage containing a usable context. + DatabaseWriteUsage GetForWrite(); + } +} diff --git a/osu.Game/Database/IHasFiles.cs b/osu.Game/Database/IHasFiles.cs new file mode 100644 index 0000000000..faf3f16dfe --- /dev/null +++ b/osu.Game/Database/IHasFiles.cs @@ -0,0 +1,18 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; + +namespace osu.Game.Database +{ + /// + /// A model that contains a list of files it is responsible for. + /// + /// The model representing a file. + public interface IHasFiles + where TFile : INamedFileInfo + + { + List Files { get; set; } + } +} diff --git a/osu.Game/Database/INamedFileInfo.cs b/osu.Game/Database/INamedFileInfo.cs new file mode 100644 index 0000000000..8de451dd78 --- /dev/null +++ b/osu.Game/Database/INamedFileInfo.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.IO; + +namespace osu.Game.Database +{ + /// + /// Represent a join model which gives a filename and scope to a . + /// + public interface INamedFileInfo + { + FileInfo FileInfo { get; set; } + string Filename { get; set; } + } +} diff --git a/osu.Game/Database/ISoftDelete.cs b/osu.Game/Database/ISoftDelete.cs new file mode 100644 index 0000000000..c884d7af00 --- /dev/null +++ b/osu.Game/Database/ISoftDelete.cs @@ -0,0 +1,16 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Database +{ + /// + /// A model that can be deleted from user's view without being instantly lost. + /// + public interface ISoftDelete + { + /// + /// Whether this model is marked for future deletion. + /// + bool DeletePending { get; set; } + } +} diff --git a/osu.Game/Database/MutableDatabaseBackedStore.cs b/osu.Game/Database/MutableDatabaseBackedStore.cs new file mode 100644 index 0000000000..4ab55691f2 --- /dev/null +++ b/osu.Game/Database/MutableDatabaseBackedStore.cs @@ -0,0 +1,149 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using osu.Framework.Platform; + +namespace osu.Game.Database +{ + /// + /// A typed store which supports basic addition, deletion and updating for soft-deletable models. + /// + /// The databased model. + public abstract class MutableDatabaseBackedStore : DatabaseBackedStore + where T : class, IHasPrimaryKey, ISoftDelete + { + public event Action ItemAdded; + public event Action ItemRemoved; + + protected MutableDatabaseBackedStore(IDatabaseContextFactory contextFactory, Storage storage = null) + : base(contextFactory, storage) + { + } + + /// + /// Access items pre-populated with includes for consumption. + /// + public IQueryable ConsumableItems => AddIncludesForConsumption(ContextFactory.Get().Set()); + + /// + /// Add a to the database. + /// + /// The item to add. + public void Add(T item) + { + using (var usage = ContextFactory.GetForWrite()) + { + var context = usage.Context; + context.Attach(item); + } + + ItemAdded?.Invoke(item); + } + + /// + /// Update a in the database. + /// + /// The item to update. + public void Update(T item) + { + ItemRemoved?.Invoke(item); + + using (var usage = ContextFactory.GetForWrite()) + usage.Context.Update(item); + + ItemAdded?.Invoke(item); + } + + /// + /// Delete a from the database. + /// + /// The item to delete. + public bool Delete(T item) + { + using (ContextFactory.GetForWrite()) + { + Refresh(ref item); + + if (item.DeletePending) return false; + item.DeletePending = true; + } + + ItemRemoved?.Invoke(item); + return true; + } + + /// + /// Restore a from a deleted state. + /// + /// The item to undelete. + public bool Undelete(T item) + { + using (ContextFactory.GetForWrite()) + { + Refresh(ref item, ConsumableItems); + + if (!item.DeletePending) return false; + item.DeletePending = false; + } + + ItemAdded?.Invoke(item); + return true; + } + + /// + /// Allow implementations to add database-side includes or constraints when querying for consumption of items. + /// + /// The input query. + /// A potentially modified output query. + protected virtual IQueryable AddIncludesForConsumption(IQueryable query) => query; + + /// + /// Allow implementations to add database-side includes or constraints when deleting items. + /// Included properties could then be subsequently deleted by overriding . + /// + /// The input query. + /// A potentially modified output query. + protected virtual IQueryable AddIncludesForDeletion(IQueryable query) => query; + + /// + /// Called when removing an item completely from the database. + /// + /// The items to be purged. + /// The write context which can be used to perform subsequent deletions. + protected virtual void Purge(List items, OsuDbContext context) => context.RemoveRange(items); + + public override void Cleanup() + { + base.Cleanup(); + PurgeDeletable(); + } + + /// + /// Purge items in a pending delete state. + /// + /// An optional query limiting the scope of the purge. + public void PurgeDeletable(Expression> query = null) + { + using (var usage = ContextFactory.GetForWrite()) + { + var context = usage.Context; + + var lookup = context.Set().Where(s => s.DeletePending); + + if (query != null) lookup = lookup.Where(query); + + lookup = AddIncludesForDeletion(lookup); + + var purgeable = lookup.ToList(); + + if (!purgeable.Any()) return; + + Purge(purgeable, context); + } + } + } +} diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index ae6d3a2aee..aa1f523a80 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -13,6 +13,7 @@ using osu.Game.IO; using osu.Game.Rulesets; using DatabasedKeyBinding = osu.Game.Input.Bindings.DatabasedKeyBinding; using LogLevel = Microsoft.Extensions.Logging.LogLevel; +using osu.Game.Skinning; namespace osu.Game.Database { @@ -26,6 +27,7 @@ namespace osu.Game.Database public DbSet DatabasedSetting { get; set; } public DbSet FileInfo { get; set; } public DbSet RulesetInfo { get; set; } + public DbSet SkinInfo { get; set; } private readonly string connectionString; @@ -105,7 +107,7 @@ namespace osu.Game.Database public int SaveChanges(IDbContextTransaction transaction = null) { var ret = base.SaveChanges(); - transaction?.Commit(); + if (ret > 0) transaction?.Commit(); return ret; } @@ -180,8 +182,6 @@ namespace osu.Game.Database public void Migrate() { - migrateFromSqliteNet(); - try { Database.Migrate(); @@ -191,86 +191,6 @@ namespace osu.Game.Database throw new MigrationFailedException(e); } } - - private void migrateFromSqliteNet() - { - try - { - // will fail if the database isn't in a sane EF-migrated state. - Database.ExecuteSqlCommand("SELECT MetadataID FROM BeatmapSetInfo LIMIT 1"); - } - catch - { - try - { - Database.ExecuteSqlCommand("DROP TABLE IF EXISTS __EFMigrationsHistory"); - - // will fail (intentionally) if we don't have sqlite-net data present. - Database.ExecuteSqlCommand("SELECT OnlineBeatmapSetId FROM BeatmapMetadata LIMIT 1"); - - try - { - Logger.Log("Performing migration from sqlite-net to EF...", LoggingTarget.Database, Framework.Logging.LogLevel.Important); - - // we are good to perform messy migration of data!. - Database.ExecuteSqlCommand("ALTER TABLE BeatmapDifficulty RENAME TO BeatmapDifficulty_Old"); - Database.ExecuteSqlCommand("ALTER TABLE BeatmapMetadata RENAME TO BeatmapMetadata_Old"); - Database.ExecuteSqlCommand("ALTER TABLE FileInfo RENAME TO FileInfo_Old"); - Database.ExecuteSqlCommand("ALTER TABLE KeyBinding RENAME TO KeyBinding_Old"); - Database.ExecuteSqlCommand("ALTER TABLE BeatmapSetInfo RENAME TO BeatmapSetInfo_Old"); - Database.ExecuteSqlCommand("ALTER TABLE BeatmapInfo RENAME TO BeatmapInfo_Old"); - Database.ExecuteSqlCommand("ALTER TABLE BeatmapSetFileInfo RENAME TO BeatmapSetFileInfo_Old"); - Database.ExecuteSqlCommand("ALTER TABLE RulesetInfo RENAME TO RulesetInfo_Old"); - - Database.ExecuteSqlCommand("DROP TABLE StoreVersion"); - - // perform EF migrations to create sane table structure. - Database.Migrate(); - - // copy data table by table to new structure, dropping old tables as we go. - Database.ExecuteSqlCommand("INSERT INTO FileInfo SELECT * FROM FileInfo_Old"); - Database.ExecuteSqlCommand("DROP TABLE FileInfo_Old"); - - Database.ExecuteSqlCommand("INSERT INTO KeyBinding SELECT ID, [Action], Keys, RulesetID, Variant FROM KeyBinding_Old"); - Database.ExecuteSqlCommand("DROP TABLE KeyBinding_Old"); - - Database.ExecuteSqlCommand( - "INSERT INTO BeatmapMetadata SELECT ID, Artist, ArtistUnicode, AudioFile, Author, BackgroundFile, PreviewTime, Source, Tags, Title, TitleUnicode FROM BeatmapMetadata_Old"); - Database.ExecuteSqlCommand("DROP TABLE BeatmapMetadata_Old"); - - Database.ExecuteSqlCommand( - "INSERT INTO BeatmapDifficulty SELECT `ID`, `ApproachRate`, `CircleSize`, `DrainRate`, `OverallDifficulty`, `SliderMultiplier`, `SliderTickRate` FROM BeatmapDifficulty_Old"); - Database.ExecuteSqlCommand("DROP TABLE BeatmapDifficulty_Old"); - - Database.ExecuteSqlCommand("INSERT INTO BeatmapSetInfo SELECT ID, DeletePending, Hash, BeatmapMetadataID, OnlineBeatmapSetID, Protected FROM BeatmapSetInfo_Old"); - Database.ExecuteSqlCommand("DROP TABLE BeatmapSetInfo_Old"); - - Database.ExecuteSqlCommand("INSERT INTO BeatmapSetFileInfo SELECT ID, BeatmapSetInfoID, FileInfoID, Filename FROM BeatmapSetFileInfo_Old"); - Database.ExecuteSqlCommand("DROP TABLE BeatmapSetFileInfo_Old"); - - Database.ExecuteSqlCommand("INSERT INTO RulesetInfo SELECT ID, Available, InstantiationInfo, Name FROM RulesetInfo_Old"); - Database.ExecuteSqlCommand("DROP TABLE RulesetInfo_Old"); - - Database.ExecuteSqlCommand( - "INSERT INTO BeatmapInfo SELECT ID, AudioLeadIn, BaseDifficultyID, BeatDivisor, BeatmapSetInfoID, Countdown, DistanceSpacing, GridSize, Hash, IFNULL(Hidden, 0), LetterboxInBreaks, MD5Hash, NULLIF(BeatmapMetadataID, 0), NULLIF(OnlineBeatmapID, 0), Path, RulesetID, SpecialStyle, StackLeniency, StarDifficulty, StoredBookmarks, TimelineZoom, Version, WidescreenStoryboard FROM BeatmapInfo_Old"); - Database.ExecuteSqlCommand("DROP TABLE BeatmapInfo_Old"); - - Logger.Log("Migration complete!", LoggingTarget.Database, Framework.Logging.LogLevel.Important); - } - catch (Exception e) - { - throw new MigrationFailedException(e); - } - } - catch (MigrationFailedException) - { - throw; - } - catch - { - } - } - } } public class MigrationFailedException : Exception diff --git a/osu.Game/Database/SingletonContextFactory.cs b/osu.Game/Database/SingletonContextFactory.cs new file mode 100644 index 0000000000..067e4fd8eb --- /dev/null +++ b/osu.Game/Database/SingletonContextFactory.cs @@ -0,0 +1,19 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Database +{ + public class SingletonContextFactory : IDatabaseContextFactory + { + private readonly OsuDbContext context; + + public SingletonContextFactory(OsuDbContext context) + { + this.context = context; + } + + public OsuDbContext Get() => context; + + public DatabaseWriteUsage GetForWrite() => new DatabaseWriteUsage(context, null); + } +} diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 6f9d83473f..89ed8044e6 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -242,7 +242,7 @@ namespace osu.Game.Graphics.Backgrounds triangle, colourInfo, null, - Shared.VertexBatch.Add, + Shared.VertexBatch.AddAction, Vector2.Divide(localInflationAmount, size)); } diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs index 9f1b44af44..1d231ada23 100644 --- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs +++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs @@ -90,6 +90,10 @@ namespace osu.Game.Graphics.Containers case LinkAction.External: Process.Start(url); break; + case LinkAction.OpenUserProfile: + if (long.TryParse(linkArgument, out long userId)) + game?.ShowUser(userId); + break; default: throw new NotImplementedException($"This {nameof(LinkAction)} ({linkType.ToString()}) is missing an associated action."); } diff --git a/osu.Game/Graphics/Containers/ParallaxContainer.cs b/osu.Game/Graphics/Containers/ParallaxContainer.cs index cb894ca382..97d6225534 100644 --- a/osu.Game/Graphics/Containers/ParallaxContainer.cs +++ b/osu.Game/Graphics/Containers/ParallaxContainer.cs @@ -8,12 +8,15 @@ using OpenTK; using osu.Framework.Allocation; using osu.Game.Configuration; using osu.Framework.Configuration; +using osu.Framework.MathUtils; namespace osu.Game.Graphics.Containers { public class ParallaxContainer : Container, IRequireHighFrequencyMousePosition { - public float ParallaxAmount = 0.02f; + public const float DEFAULT_PARALLAX_AMOUNT = 0.02f; + + public float ParallaxAmount = DEFAULT_PARALLAX_AMOUNT; private Bindable parallaxEnabled; @@ -61,9 +64,12 @@ namespace osu.Game.Graphics.Containers if (parallaxEnabled) { - Vector2 offset = input.CurrentState.Mouse == null ? Vector2.Zero : ToLocalSpace(input.CurrentState.Mouse.NativeState.Position) - DrawSize / 2; - content.MoveTo(offset * ParallaxAmount, firstUpdate ? 0 : 1000, Easing.OutQuint); - content.Scale = new Vector2(1 + ParallaxAmount); + Vector2 offset = (input.CurrentState.Mouse == null ? Vector2.Zero : ToLocalSpace(input.CurrentState.Mouse.NativeState.Position) - DrawSize / 2) * ParallaxAmount; + + double elapsed = MathHelper.Clamp(Clock.ElapsedFrameTime, 0, 1000); + + content.Position = Interpolation.ValueAt(elapsed, content.Position, offset, 0, 1000, Easing.OutQuint); + content.Scale = Interpolation.ValueAt(elapsed, content.Scale, new Vector2(1 + ParallaxAmount), 0, 1000, Easing.OutQuint); } firstUpdate = false; diff --git a/osu.Game/Graphics/Containers/ReverseChildIDFillFlowContainer.cs b/osu.Game/Graphics/Containers/ReverseChildIDFillFlowContainer.cs index 9f028490ef..5803c8a5db 100644 --- a/osu.Game/Graphics/Containers/ReverseChildIDFillFlowContainer.cs +++ b/osu.Game/Graphics/Containers/ReverseChildIDFillFlowContainer.cs @@ -1,8 +1,6 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System.Collections.Generic; -using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -11,7 +9,5 @@ namespace osu.Game.Graphics.Containers public class ReverseChildIDFillFlowContainer : FillFlowContainer where T : Drawable { protected override int Compare(Drawable x, Drawable y) => CompareReverseChildID(x, y); - - protected override IEnumerable FlowingChildren => base.FlowingChildren.Reverse(); } } diff --git a/osu.Game/Graphics/DrawableDate.cs b/osu.Game/Graphics/DrawableDate.cs new file mode 100644 index 0000000000..a912f989e0 --- /dev/null +++ b/osu.Game/Graphics/DrawableDate.cs @@ -0,0 +1,66 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using Humanizer; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Threading; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Graphics +{ + public class DrawableDate : OsuSpriteText, IHasTooltip + { + private readonly DateTimeOffset date; + private ScheduledDelegate updateTask; + + public DrawableDate(DateTimeOffset date) + { + AutoSizeAxes = Axes.Both; + Font = "Exo2.0-RegularItalic"; + + this.date = date.ToLocalTime(); + } + + [BackgroundDependencyLoader] + private void load() + { + updateTime(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Scheduler.Add(updateTimeWithReschedule); + } + + private void updateTimeWithReschedule() + { + updateTime(); + + var diffToNow = DateTimeOffset.Now.Subtract(date); + + double timeUntilNextUpdate = 1000; + if (diffToNow.TotalSeconds > 60) + { + timeUntilNextUpdate *= 60; + if (diffToNow.TotalMinutes > 60) + { + timeUntilNextUpdate *= 60; + + if (diffToNow.TotalHours > 24) + timeUntilNextUpdate *= 24; + } + } + + Scheduler.AddDelayed(updateTimeWithReschedule, timeUntilNextUpdate); + } + + public override bool HandleMouseInput => true; + + private void updateTime() => Text = date.Humanize(); + public string TooltipText => date.ToString(); + } +} diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs new file mode 100644 index 0000000000..b0cd997837 --- /dev/null +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -0,0 +1,110 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Drawing.Imaging; +using System.IO; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Configuration; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input; +using osu.Framework.Input.Bindings; +using osu.Framework.Platform; +using osu.Game.Configuration; +using osu.Game.Input.Bindings; +using osu.Game.Overlays; +using osu.Game.Overlays.Notifications; + +namespace osu.Game.Graphics +{ + public class ScreenshotManager : Container, IKeyBindingHandler, IHandleGlobalInput + { + private Bindable screenshotFormat; + private GameHost host; + private Storage storage; + private NotificationOverlay notificationOverlay; + + private SampleChannel shutter; + + [BackgroundDependencyLoader] + private void load(GameHost host, OsuConfigManager config, Storage storage, NotificationOverlay notificationOverlay, AudioManager audio) + { + this.host = host; + this.storage = storage.GetStorageForDirectory(@"screenshots"); + this.notificationOverlay = notificationOverlay; + + screenshotFormat = config.GetBindable(OsuSetting.ScreenshotFormat); + + shutter = audio.Sample.Get("UI/shutter"); + } + + public bool OnPressed(GlobalAction action) + { + switch (action) + { + case GlobalAction.TakeScreenshot: + shutter.Play(); + TakeScreenshotAsync(); + return true; + } + + return false; + } + + public bool OnReleased(GlobalAction action) => false; + + public async void TakeScreenshotAsync() + { + using (var bitmap = await host.TakeScreenshotAsync()) + { + var fileName = getFileName(); + if (fileName == null) return; + + var stream = storage.GetStream(fileName, FileAccess.Write); + + switch (screenshotFormat.Value) + { + case ScreenshotFormat.Png: + bitmap.Save(stream, ImageFormat.Png); + break; + case ScreenshotFormat.Jpg: + bitmap.Save(stream, ImageFormat.Jpeg); + break; + default: + throw new ArgumentOutOfRangeException(nameof(screenshotFormat)); + } + + notificationOverlay.Post(new SimpleNotification + { + Text = $"{fileName} saved!", + Activated = () => + { + storage.OpenInNativeExplorer(); + return true; + } + }); + } + } + + private string getFileName() + { + var dt = DateTime.Now; + var fileExt = screenshotFormat.ToString().ToLower(); + + var withoutIndex = $"osu_{dt:yyyy-MM-dd_HH-mm-ss}.{fileExt}"; + if (!storage.Exists(withoutIndex)) + return withoutIndex; + + for (ulong i = 1; i < ulong.MaxValue; i++) + { + var indexedName = $"osu_{dt:yyyy-MM-dd_HH-mm-ss}-{i}.{fileExt}"; + if (!storage.Exists(indexedName)) + return indexedName; + } + + return null; + } + } +} diff --git a/osu.Game/Graphics/SpriteIcon.cs b/osu.Game/Graphics/SpriteIcon.cs index a93cb4c6cd..4324119481 100644 --- a/osu.Game/Graphics/SpriteIcon.cs +++ b/osu.Game/Graphics/SpriteIcon.cs @@ -988,7 +988,7 @@ namespace osu.Game.Graphics fa_osu_expert_mania = 0xe028, // mod icons - fa_osu_mod_perfect = 0xe02d, + fa_osu_mod_perfect = 0xe049, fa_osu_mod_autopilot = 0xe03a, fa_osu_mod_auto = 0xe03b, fa_osu_mod_cinema = 0xe03c, diff --git a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs index 6d9bf231c3..33786252ab 100644 --- a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs +++ b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs @@ -30,6 +30,9 @@ namespace osu.Game.Graphics.UserInterface } } + // We may not be focused yet, but we need to handle keyboard input to be able to request focus + public override bool HandleKeyboardInput => HoldFocus || base.HandleKeyboardInput; + protected override void OnFocus(InputState state) { base.OnFocus(state); diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index 7ad9bc73a8..20385a7dae 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; 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.Graphics.UserInterface; @@ -56,6 +57,14 @@ namespace osu.Game.Graphics.UserInterface } } + protected override TabFillFlowContainer CreateTabFlow() => new OsuTabFillFlowContainer + { + Direction = FillDirection.Full, + RelativeSizeAxes = Axes.Both, + Depth = -1, + Masking = true + }; + public class OsuTabItem : TabItem, IHasAccentColour { protected readonly SpriteText Text; @@ -239,5 +248,10 @@ namespace osu.Game.Graphics.UserInterface } } } + + private class OsuTabFillFlowContainer : TabFillFlowContainer + { + protected override int Compare(Drawable x, Drawable y) => CompareReverseChildID(x, y); + } } } diff --git a/osu.Game/Graphics/UserInterface/SearchTextBox.cs b/osu.Game/Graphics/UserInterface/SearchTextBox.cs index 9398eb55f3..28d33bbacd 100644 --- a/osu.Game/Graphics/UserInterface/SearchTextBox.cs +++ b/osu.Game/Graphics/UserInterface/SearchTextBox.cs @@ -12,6 +12,8 @@ namespace osu.Game.Graphics.UserInterface { protected virtual bool AllowCommit => false; + public override bool HandleLeftRightArrows => false; + public SearchTextBox() { Height = 35; diff --git a/osu.Game/Graphics/UserInterface/Volume/VolumeMeter.cs b/osu.Game/Graphics/UserInterface/Volume/VolumeMeter.cs deleted file mode 100644 index ef3702fdf3..0000000000 --- a/osu.Game/Graphics/UserInterface/Volume/VolumeMeter.cs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Framework.Configuration; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Bindings; -using osu.Game.Input.Bindings; - -namespace osu.Game.Graphics.UserInterface.Volume -{ - public class VolumeMeter : Container, IKeyBindingHandler - { - private readonly Box meterFill; - public BindableDouble Bindable { get; } = new BindableDouble(); - - public VolumeMeter(string meterName) - { - Size = new Vector2(40, 180); - Children = new Drawable[] - { - new Box - { - Colour = Color4.Black, - RelativeSizeAxes = Axes.Both - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.5f, 0.9f), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] - { - new Box - { - Colour = Color4.DarkGray, - RelativeSizeAxes = Axes.Both - }, - meterFill = new Box - { - Colour = Color4.White, - Scale = new Vector2(1, 0), - RelativeSizeAxes = Axes.Both, - Origin = Anchor.BottomCentre, - Anchor = Anchor.BottomCentre - } - } - }, - new OsuSpriteText - { - Text = meterName, - Anchor = Anchor.BottomCentre, - Origin = Anchor.TopCentre - } - }; - - Bindable.ValueChanged += delegate { updateFill(); }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - updateFill(); - } - - public double Volume - { - get => Bindable.Value; - private set => Bindable.Value = value; - } - - public void Increase() - { - Volume += 0.05f; - } - - public void Decrease() - { - Volume -= 0.05f; - } - - private void updateFill() => meterFill.ScaleTo(new Vector2(1, (float)Volume), 300, Easing.OutQuint); - - public bool OnPressed(GlobalAction action) - { - if (!IsHovered) return false; - - switch (action) - { - case GlobalAction.DecreaseVolume: - Decrease(); - return true; - case GlobalAction.IncreaseVolume: - Increase(); - return true; - } - - return false; - } - - public bool OnReleased(GlobalAction action) => false; - } -} diff --git a/osu.Game/Beatmaps/IO/ArchiveReader.cs b/osu.Game/IO/Archives/ArchiveReader.cs similarity index 76% rename from osu.Game/Beatmaps/IO/ArchiveReader.cs rename to osu.Game/IO/Archives/ArchiveReader.cs index 453a03b882..351a6dff39 100644 --- a/osu.Game/Beatmaps/IO/ArchiveReader.cs +++ b/osu.Game/IO/Archives/ArchiveReader.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using System.IO; using osu.Framework.IO.Stores; -namespace osu.Game.Beatmaps.IO +namespace osu.Game.IO.Archives { public abstract class ArchiveReader : IDisposable, IResourceStore { @@ -17,6 +17,16 @@ namespace osu.Game.Beatmaps.IO public abstract void Dispose(); + /// + /// The name of this archive (usually the containing filename). + /// + public readonly string Name; + + protected ArchiveReader(string name) + { + Name = name; + } + public abstract IEnumerable Filenames { get; } public virtual byte[] Get(string name) diff --git a/osu.Game/Beatmaps/IO/LegacyFilesystemReader.cs b/osu.Game/IO/Archives/LegacyFilesystemReader.cs similarity index 86% rename from osu.Game/Beatmaps/IO/LegacyFilesystemReader.cs rename to osu.Game/IO/Archives/LegacyFilesystemReader.cs index 4a85f6f526..d6d80783db 100644 --- a/osu.Game/Beatmaps/IO/LegacyFilesystemReader.cs +++ b/osu.Game/IO/Archives/LegacyFilesystemReader.cs @@ -1,12 +1,12 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.IO.File; using System.Collections.Generic; using System.IO; using System.Linq; +using osu.Framework.IO.File; -namespace osu.Game.Beatmaps.IO +namespace osu.Game.IO.Archives { /// /// Reads an extracted legacy beatmap from disk. @@ -15,7 +15,7 @@ namespace osu.Game.Beatmaps.IO { private readonly string path; - public LegacyFilesystemReader(string path) + public LegacyFilesystemReader(string path) : base(Path.GetFileName(path)) { this.path = path; } diff --git a/osu.Game/Beatmaps/IO/OszArchiveReader.cs b/osu.Game/IO/Archives/ZipArchiveReader.cs similarity index 84% rename from osu.Game/Beatmaps/IO/OszArchiveReader.cs rename to osu.Game/IO/Archives/ZipArchiveReader.cs index ada7fb47bb..dbf236e835 100644 --- a/osu.Game/Beatmaps/IO/OszArchiveReader.cs +++ b/osu.Game/IO/Archives/ZipArchiveReader.cs @@ -6,14 +6,15 @@ using System.IO; using System.Linq; using SharpCompress.Archives.Zip; -namespace osu.Game.Beatmaps.IO +namespace osu.Game.IO.Archives { - public sealed class OszArchiveReader : ArchiveReader + public sealed class ZipArchiveReader : ArchiveReader { private readonly Stream archiveStream; private readonly ZipArchive archive; - public OszArchiveReader(Stream archiveStream) + public ZipArchiveReader(Stream archiveStream, string name = null) + : base(name) { this.archiveStream = archiveStream; archive = ZipArchive.Open(archiveStream); diff --git a/osu.Game/IO/FileStore.cs b/osu.Game/IO/FileStore.cs index 31c608a5f4..ab81ba4851 100644 --- a/osu.Game/IO/FileStore.cs +++ b/osu.Game/IO/FileStore.cs @@ -21,86 +21,94 @@ namespace osu.Game.IO public new Storage Storage => base.Storage; - public FileStore(Func createContext, Storage storage) : base(createContext, storage.GetStorageForDirectory(@"files")) + public FileStore(IDatabaseContextFactory contextFactory, Storage storage) : base(contextFactory, storage.GetStorageForDirectory(@"files")) { Store = new StorageBackedResourceStore(Storage); } public FileInfo Add(Stream data, bool reference = true) { - var context = GetContext(); - - string hash = data.ComputeSHA2Hash(); - - var existing = context.FileInfo.FirstOrDefault(f => f.Hash == hash); - - var info = existing ?? new FileInfo { Hash = hash }; - - string path = info.StoragePath; - - // we may be re-adding a file to fix missing store entries. - if (!Storage.Exists(path)) + using (var usage = ContextFactory.GetForWrite()) { - data.Seek(0, SeekOrigin.Begin); + string hash = data.ComputeSHA2Hash(); - using (var output = Storage.GetStream(path, FileAccess.Write)) - data.CopyTo(output); + var existing = usage.Context.FileInfo.FirstOrDefault(f => f.Hash == hash); - data.Seek(0, SeekOrigin.Begin); + var info = existing ?? new FileInfo { Hash = hash }; + + string path = info.StoragePath; + + // we may be re-adding a file to fix missing store entries. + if (!Storage.Exists(path)) + { + data.Seek(0, SeekOrigin.Begin); + + using (var output = Storage.GetStream(path, FileAccess.Write)) + data.CopyTo(output); + + data.Seek(0, SeekOrigin.Begin); + } + + if (reference || existing == null) + Reference(info); + + return info; } - - if (reference || existing == null) - Reference(info); - - return info; } - public void Reference(params FileInfo[] files) => reference(GetContext(), files); - - private void reference(OsuDbContext context, FileInfo[] files) + public void Reference(params FileInfo[] files) { - foreach (var f in files.GroupBy(f => f.ID)) - { - var refetch = context.Find(f.First().ID) ?? f.First(); - refetch.ReferenceCount += f.Count(); - context.FileInfo.Update(refetch); - } + if (files.Length == 0) return; - context.SaveChanges(); + using (var usage = ContextFactory.GetForWrite()) + { + var context = usage.Context; + + foreach (var f in files.GroupBy(f => f.ID)) + { + var refetch = context.Find(f.First().ID) ?? f.First(); + refetch.ReferenceCount += f.Count(); + context.FileInfo.Update(refetch); + } + } } - public void Dereference(params FileInfo[] files) => dereference(GetContext(), files); - - private void dereference(OsuDbContext context, FileInfo[] files) + public void Dereference(params FileInfo[] files) { - foreach (var f in files.GroupBy(f => f.ID)) - { - var refetch = context.FileInfo.Find(f.Key); - refetch.ReferenceCount -= f.Count(); - context.FileInfo.Update(refetch); - } + if (files.Length == 0) return; - context.SaveChanges(); + using (var usage = ContextFactory.GetForWrite()) + { + var context = usage.Context; + + foreach (var f in files.GroupBy(f => f.ID)) + { + var refetch = context.FileInfo.Find(f.Key); + refetch.ReferenceCount -= f.Count(); + context.FileInfo.Update(refetch); + } + } } public override void Cleanup() { - var context = GetContext(); - - foreach (var f in context.FileInfo.Where(f => f.ReferenceCount < 1)) + using (var usage = ContextFactory.GetForWrite()) { - try + var context = usage.Context; + + foreach (var f in context.FileInfo.Where(f => f.ReferenceCount < 1)) { - Storage.Delete(f.StoragePath); - context.FileInfo.Remove(f); - } - catch (Exception e) - { - Logger.Error(e, $@"Could not delete beatmap {f}"); + try + { + Storage.Delete(f.StoragePath); + context.FileInfo.Remove(f); + } + catch (Exception e) + { + Logger.Error(e, $@"Could not delete beatmap {f}"); + } } } - - context.SaveChanges(); } } } diff --git a/osu.Game/IPC/BeatmapIPCChannel.cs b/osu.Game/IPC/ArchiveImportIPCChannel.cs similarity index 56% rename from osu.Game/IPC/BeatmapIPCChannel.cs rename to osu.Game/IPC/ArchiveImportIPCChannel.cs index 64e5d526e6..9d7bf17c77 100644 --- a/osu.Game/IPC/BeatmapIPCChannel.cs +++ b/osu.Game/IPC/ArchiveImportIPCChannel.cs @@ -2,23 +2,25 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Diagnostics; +using System.IO; +using System.Linq; using System.Threading.Tasks; using osu.Framework.Platform; -using osu.Game.Beatmaps; +using osu.Game.Database; namespace osu.Game.IPC { - public class BeatmapIPCChannel : IpcChannel + public class ArchiveImportIPCChannel : IpcChannel { - private readonly BeatmapManager beatmaps; + private readonly ICanAcceptFiles importer; - public BeatmapIPCChannel(IIpcHost host, BeatmapManager beatmaps = null) + public ArchiveImportIPCChannel(IIpcHost host, ICanAcceptFiles importer = null) : base(host) { - this.beatmaps = beatmaps; + this.importer = importer; MessageReceived += msg => { - Debug.Assert(beatmaps != null); + Debug.Assert(importer != null); ImportAsync(msg.Path).ContinueWith(t => { if (t.Exception != null) throw t.Exception; @@ -28,18 +30,19 @@ namespace osu.Game.IPC public async Task ImportAsync(string path) { - if (beatmaps == null) + if (importer == null) { //we want to contact a remote osu! to handle the import. - await SendMessageAsync(new BeatmapImportMessage { Path = path }); + await SendMessageAsync(new ArchiveImportMessage { Path = path }); return; } - beatmaps.Import(path); + if (importer.HandledExtensions.Contains(Path.GetExtension(path))) + importer.Import(path); } } - public class BeatmapImportMessage + public class ArchiveImportMessage { public string Path; } diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index b6bc348a52..2b53f77d62 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -45,6 +45,11 @@ namespace osu.Game.Input.Bindings private void load(KeyBindingStore keyBindings) { store = keyBindings; + } + + protected override void LoadComplete() + { + base.LoadComplete(); store.KeyBindingChanged += ReloadMappings; } diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 17ec2af4b9..97e473a797 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -26,6 +26,8 @@ namespace osu.Game.Input.Bindings { new KeyBinding(InputKey.F8, GlobalAction.ToggleChat), new KeyBinding(InputKey.F9, GlobalAction.ToggleSocial), + new KeyBinding(InputKey.F12,GlobalAction.TakeScreenshot), + new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings), new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar), new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings), @@ -72,5 +74,8 @@ namespace osu.Game.Input.Bindings SkipCutscene, [Description("Quick Retry (Hold)")] QuickRetry, + + [Description("Take screenshot")] + TakeScreenshot } } diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs index 92159ab491..33cb0911a8 100644 --- a/osu.Game/Input/KeyBindingStore.cs +++ b/osu.Game/Input/KeyBindingStore.cs @@ -16,14 +16,17 @@ namespace osu.Game.Input { public event Action KeyBindingChanged; - public KeyBindingStore(Func createContext, RulesetStore rulesets, Storage storage = null) - : base(createContext, storage) + public KeyBindingStore(DatabaseContextFactory contextFactory, RulesetStore rulesets, Storage storage = null) + : base(contextFactory, storage) { - foreach (var info in rulesets.AvailableRulesets) + using (ContextFactory.GetForWrite()) { - var ruleset = info.CreateInstance(); - foreach (var variant in ruleset.AvailableVariants) - insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant); + foreach (var info in rulesets.AvailableRulesets) + { + var ruleset = info.CreateInstance(); + foreach (var variant in ruleset.AvailableVariants) + insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant); + } } } @@ -31,9 +34,7 @@ namespace osu.Game.Input private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) { - var context = GetContext(); - - using (var transaction = context.BeginTransaction()) + using (var usage = ContextFactory.GetForWrite()) { // compare counts in database vs defaults foreach (var group in defaults.GroupBy(k => k.Action)) @@ -46,7 +47,7 @@ namespace osu.Game.Input foreach (var insertable in group.Skip(count).Take(aimCount - count)) // insert any defaults which are missing. - context.DatabasedKeyBinding.Add(new DatabasedKeyBinding + usage.Context.DatabasedKeyBinding.Add(new DatabasedKeyBinding { KeyCombination = insertable.KeyCombination, Action = insertable.Action, @@ -54,8 +55,6 @@ namespace osu.Game.Input Variant = variant }); } - - context.SaveChanges(transaction); } } @@ -66,19 +65,20 @@ namespace osu.Game.Input /// An optional variant. /// public List Query(int? rulesetId = null, int? variant = null) => - GetContext().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); + ContextFactory.Get().DatabasedKeyBinding.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList(); public void Update(KeyBinding keyBinding) { - var dbKeyBinding = (DatabasedKeyBinding)keyBinding; + using (ContextFactory.GetForWrite()) + { + var dbKeyBinding = (DatabasedKeyBinding)keyBinding; + Refresh(ref dbKeyBinding); - var context = GetContext(); + if (dbKeyBinding.KeyCombination.Equals(keyBinding.KeyCombination)) + return; - Refresh(ref dbKeyBinding); - - dbKeyBinding.KeyCombination = keyBinding.KeyCombination; - - context.SaveChanges(); + dbKeyBinding.KeyCombination = keyBinding.KeyCombination; + } KeyBindingChanged?.Invoke(); } diff --git a/osu.Game/Migrations/20180219060912_AddSkins.Designer.cs b/osu.Game/Migrations/20180219060912_AddSkins.Designer.cs new file mode 100644 index 0000000000..83b8d6cf8a --- /dev/null +++ b/osu.Game/Migrations/20180219060912_AddSkins.Designer.cs @@ -0,0 +1,379 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage; +using osu.Game.Database; +using System; + +namespace osu.Game.Migrations +{ + [DbContext(typeof(OsuDbContext))] + [Migration("20180219060912_AddSkins")] + partial class AddSkins + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.0.0-rtm-26452"); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("ApproachRate"); + + b.Property("CircleSize"); + + b.Property("DrainRate"); + + b.Property("OverallDifficulty"); + + b.Property("SliderMultiplier"); + + b.Property("SliderTickRate"); + + b.HasKey("ID"); + + b.ToTable("BeatmapDifficulty"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("AudioLeadIn"); + + b.Property("BaseDifficultyID"); + + b.Property("BeatDivisor"); + + b.Property("BeatmapSetInfoID"); + + b.Property("Countdown"); + + b.Property("DistanceSpacing"); + + b.Property("GridSize"); + + b.Property("Hash"); + + b.Property("Hidden"); + + b.Property("LetterboxInBreaks"); + + b.Property("MD5Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapID"); + + b.Property("Path"); + + b.Property("RulesetID"); + + b.Property("SpecialStyle"); + + b.Property("StackLeniency"); + + b.Property("StarDifficulty"); + + b.Property("StoredBookmarks"); + + b.Property("TimelineZoom"); + + b.Property("Version"); + + b.Property("WidescreenStoryboard"); + + b.HasKey("ID"); + + b.HasIndex("BaseDifficultyID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MD5Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapID") + .IsUnique(); + + b.HasIndex("RulesetID"); + + b.ToTable("BeatmapInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Artist"); + + b.Property("ArtistUnicode"); + + b.Property("AudioFile"); + + b.Property("AuthorString") + .HasColumnName("Author"); + + b.Property("BackgroundFile"); + + b.Property("PreviewTime"); + + b.Property("Source"); + + b.Property("Tags"); + + b.Property("Title"); + + b.Property("TitleUnicode"); + + b.HasKey("ID"); + + b.ToTable("BeatmapMetadata"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("BeatmapSetInfoID"); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.HasKey("ID"); + + b.HasIndex("BeatmapSetInfoID"); + + b.HasIndex("FileInfoID"); + + b.ToTable("BeatmapSetFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("DeletePending"); + + b.Property("Hash"); + + b.Property("MetadataID"); + + b.Property("OnlineBeatmapSetID"); + + b.Property("Protected"); + + b.HasKey("ID"); + + b.HasIndex("DeletePending"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("MetadataID"); + + b.HasIndex("OnlineBeatmapSetID") + .IsUnique(); + + b.ToTable("BeatmapSetInfo"); + }); + + modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntKey") + .HasColumnName("Key"); + + b.Property("RulesetID"); + + b.Property("StringValue") + .HasColumnName("Value"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("IntAction") + .HasColumnName("Action"); + + b.Property("KeysString") + .HasColumnName("Keys"); + + b.Property("RulesetID"); + + b.Property("Variant"); + + b.HasKey("ID"); + + b.HasIndex("IntAction"); + + b.HasIndex("RulesetID", "Variant"); + + b.ToTable("KeyBinding"); + }); + + modelBuilder.Entity("osu.Game.IO.FileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Hash"); + + b.Property("ReferenceCount"); + + b.HasKey("ID"); + + b.HasIndex("Hash") + .IsUnique(); + + b.HasIndex("ReferenceCount"); + + b.ToTable("FileInfo"); + }); + + modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Available"); + + b.Property("InstantiationInfo"); + + b.Property("Name"); + + b.Property("ShortName"); + + b.HasKey("ID"); + + b.HasIndex("Available"); + + b.HasIndex("ShortName") + .IsUnique(); + + b.ToTable("RulesetInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.ToTable("SkinInfo"); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") + .WithMany() + .HasForeignKey("BaseDifficultyID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet") + .WithMany("Beatmaps") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("Beatmaps") + .HasForeignKey("MetadataID"); + + b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset") + .WithMany() + .HasForeignKey("RulesetID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo") + .WithMany("Files") + .HasForeignKey("BeatmapSetInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b => + { + b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata") + .WithMany("BeatmapSets") + .HasForeignKey("MetadataID"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/osu.Game/Migrations/20180219060912_AddSkins.cs b/osu.Game/Migrations/20180219060912_AddSkins.cs new file mode 100644 index 0000000000..741fcf4079 --- /dev/null +++ b/osu.Game/Migrations/20180219060912_AddSkins.cs @@ -0,0 +1,73 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using System; +using System.Collections.Generic; + +namespace osu.Game.Migrations +{ + public partial class AddSkins : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "SkinInfo", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Creator = table.Column(type: "TEXT", nullable: true), + DeletePending = table.Column(type: "INTEGER", nullable: false), + Name = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_SkinInfo", x => x.ID); + }); + + migrationBuilder.CreateTable( + name: "SkinFileInfo", + columns: table => new + { + ID = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + FileInfoID = table.Column(type: "INTEGER", nullable: false), + Filename = table.Column(type: "TEXT", nullable: false), + SkinInfoID = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_SkinFileInfo", x => x.ID); + table.ForeignKey( + name: "FK_SkinFileInfo_FileInfo_FileInfoID", + column: x => x.FileInfoID, + principalTable: "FileInfo", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_SkinFileInfo_SkinInfo_SkinInfoID", + column: x => x.SkinInfoID, + principalTable: "SkinInfo", + principalColumn: "ID", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_SkinFileInfo_FileInfoID", + table: "SkinFileInfo", + column: "FileInfoID"); + + migrationBuilder.CreateIndex( + name: "IX_SkinFileInfo_SkinInfoID", + table: "SkinFileInfo", + column: "SkinInfoID"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SkinFileInfo"); + + migrationBuilder.DropTable( + name: "SkinInfo"); + } + } +} diff --git a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs index 56632102cd..b16b9fdefa 100644 --- a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs +++ b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs @@ -277,6 +277,43 @@ namespace osu.Game.Migrations b.ToTable("RulesetInfo"); }); + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("FileInfoID"); + + b.Property("Filename") + .IsRequired(); + + b.Property("SkinInfoID"); + + b.HasKey("ID"); + + b.HasIndex("FileInfoID"); + + b.HasIndex("SkinInfoID"); + + b.ToTable("SkinFileInfo"); + }); + + modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b => + { + b.Property("ID") + .ValueGeneratedOnAdd(); + + b.Property("Creator"); + + b.Property("DeletePending"); + + b.Property("Name"); + + b.HasKey("ID"); + + b.ToTable("SkinInfo"); + }); + modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b => { b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty") @@ -318,6 +355,19 @@ namespace osu.Game.Migrations .WithMany("BeatmapSets") .HasForeignKey("MetadataID"); }); + + modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b => + { + b.HasOne("osu.Game.IO.FileInfo", "FileInfo") + .WithMany() + .HasForeignKey("FileInfoID") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("osu.Game.Skinning.SkinInfo") + .WithMany("Files") + .HasForeignKey("SkinInfoID") + .OnDelete(DeleteBehavior.Cascade); + }); #pragma warning restore 612, 618 } } diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 1d657b8664..957aeac3cd 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -7,17 +7,19 @@ using System.Collections.Generic; using System.Diagnostics; using System.Net; using System.Threading; -using osu.Framework; +using System.Threading.Tasks; using osu.Framework.Configuration; +using osu.Framework.Graphics; using osu.Framework.Logging; -using osu.Framework.Threading; +using osu.Game.Configuration; using osu.Game.Online.API.Requests; using osu.Game.Users; namespace osu.Game.Online.API { - public class APIAccess : IUpdateable + public class APIAccess : Component, IAPIProvider { + private readonly OsuConfigManager config; private readonly OAuth authentication; public string Endpoint = @"https://osu.ppy.sh"; @@ -26,15 +28,14 @@ namespace osu.Game.Online.API private ConcurrentQueue queue = new ConcurrentQueue(); - public Scheduler Scheduler = new Scheduler(); + /// + /// The username/email provided by the user when initiating a login. + /// + public string ProvidedUsername { get; private set; } - public string Username; + private string password; - //private SecurePassword password; - - public string Password; - - public Bindable LocalUser = new Bindable(createGuestUser()); + public Bindable LocalUser { get; } = new Bindable(createGuestUser()); public string Token { @@ -42,24 +43,29 @@ namespace osu.Game.Online.API set { authentication.Token = string.IsNullOrEmpty(value) ? null : OAuthToken.Parse(value); } } - protected bool HasLogin => Token != null || !string.IsNullOrEmpty(Username) && !string.IsNullOrEmpty(Password); + protected bool HasLogin => Token != null || !string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password); - // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable (should dispose of this or at very least keep a reference). - private readonly Thread thread; + private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); private readonly Logger log; - public APIAccess() + public APIAccess(OsuConfigManager config) { + this.config = config; + authentication = new OAuth(client_id, client_secret, Endpoint); log = Logger.GetLogger(LoggingTarget.Network); - thread = new Thread(run) { IsBackground = true }; - thread.Start(); + ProvidedUsername = config.Get(OsuSetting.Username); + Token = config.Get(OsuSetting.Token); + + Task.Factory.StartNew(run, cancellationToken.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); } private readonly List components = new List(); + internal void Schedule(Action action) => base.Schedule(action); + public void Register(IOnlineComponent component) { Scheduler.Add(delegate @@ -86,7 +92,7 @@ namespace osu.Game.Online.API private void run() { - while (thread.IsAlive) + while (!cancellationToken.IsCancellationRequested) { switch (State) { @@ -112,12 +118,15 @@ namespace osu.Game.Online.API State = APIState.Connecting; - if (!authentication.HasValidAccessToken && !authentication.AuthenticateWithLogin(Username, Password)) + // save the username at this point, if the user requested for it to be. + config.Set(OsuSetting.Username, config.Get(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty); + + if (!authentication.HasValidAccessToken && !authentication.AuthenticateWithLogin(ProvidedUsername, password)) { //todo: this fails even on network-related issues. we should probably handle those differently. //NotificationOverlay.ShowMessage("Login failed!"); log.Add(@"Login failed!"); - Password = null; + password = null; authentication.Clear(); continue; } @@ -126,7 +135,6 @@ namespace osu.Game.Online.API userReq.Success += u => { LocalUser.Value = u; - Username = LocalUser.Value.Username; failureCount = 0; //we're connected! @@ -175,8 +183,8 @@ namespace osu.Game.Online.API { Debug.Assert(State == APIState.Offline); - Username = username; - Password = password; + ProvidedUsername = username; + this.password = password; } /// @@ -200,7 +208,7 @@ namespace osu.Game.Online.API } catch (WebException we) { - HttpStatusCode statusCode = (we.Response as HttpWebResponse)?.StatusCode ?? HttpStatusCode.RequestTimeout; + HttpStatusCode statusCode = (we.Response as HttpWebResponse)?.StatusCode ?? (we.Status == WebExceptionStatus.UnknownError ? HttpStatusCode.NotAcceptable : HttpStatusCode.RequestTimeout); switch (statusCode) { @@ -258,10 +266,7 @@ namespace osu.Game.Online.API public bool IsLoggedIn => LocalUser.Value.Id > 1; - public void Queue(APIRequest request) - { - queue.Enqueue(request); - } + public void Queue(APIRequest request) => queue.Enqueue(request); public event StateChangeDelegate OnStateChange; @@ -285,8 +290,8 @@ namespace osu.Game.Online.API public void Logout(bool clearUsername = true) { flushQueue(); - if (clearUsername) Username = null; - Password = null; + if (clearUsername) ProvidedUsername = null; + password = null; authentication.Clear(); LocalUser.Value = createGuestUser(); } @@ -297,9 +302,15 @@ namespace osu.Game.Online.API Id = 1, }; - public void Update() + protected override void Dispose(bool isDisposing) { - Scheduler.Update(); + base.Dispose(isDisposing); + + config.Set(OsuSetting.Token, config.Get(OsuSetting.SavePassword) ? Token : string.Empty); + config.Save(); + + flushQueue(); + cancellationToken.Cancel(); } } diff --git a/osu.Game/Online/API/APIDownloadRequest.cs b/osu.Game/Online/API/APIDownloadRequest.cs new file mode 100644 index 0000000000..0a5210723d --- /dev/null +++ b/osu.Game/Online/API/APIDownloadRequest.cs @@ -0,0 +1,33 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.IO.Network; + +namespace osu.Game.Online.API +{ + public abstract class APIDownloadRequest : APIRequest + { + protected override WebRequest CreateWebRequest() + { + var request = new WebRequest(Uri); + request.DownloadProgress += request_Progress; + return request; + } + + private void request_Progress(long current, long total) => API.Schedule(() => Progress?.Invoke(current, total)); + + protected APIDownloadRequest() + { + base.Success += onSuccess; + } + + private void onSuccess() + { + Success?.Invoke(WebRequest.ResponseData); + } + + public event APIProgressHandler Progress; + + public new event APISuccessHandler Success; + } +} diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index ce6f3c7c7d..4b05df661b 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -27,32 +27,6 @@ namespace osu.Game.Online.API public new event APISuccessHandler Success; } - public abstract class APIDownloadRequest : APIRequest - { - protected override WebRequest CreateWebRequest() - { - var request = new WebRequest(Uri); - request.DownloadProgress += request_Progress; - return request; - } - - private void request_Progress(long current, long total) => API.Scheduler.Add(delegate { Progress?.Invoke(current, total); }); - - protected APIDownloadRequest() - { - base.Success += onSuccess; - } - - private void onSuccess() - { - Success?.Invoke(WebRequest.ResponseData); - } - - public event APIProgressHandler Progress; - - public new event APISuccessHandler Success; - } - /// /// AN API request with no specified response type. /// @@ -111,7 +85,7 @@ namespace osu.Game.Online.API if (checkAndProcessFailure()) return; - api.Scheduler.Add(delegate { Success?.Invoke(); }); + api.Schedule(delegate { Success?.Invoke(); }); } public void Cancel() => Fail(new OperationCanceledException(@"Request cancelled")); @@ -134,7 +108,7 @@ namespace osu.Game.Online.API { if (API == null || pendingFailure == null) return cancelled; - API.Scheduler.Add(pendingFailure); + API.Schedule(pendingFailure); pendingFailure = null; return true; } diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs new file mode 100644 index 0000000000..fc0dc0ef8b --- /dev/null +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -0,0 +1,31 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Configuration; +using osu.Game.Users; + +namespace osu.Game.Online.API +{ + public class DummyAPIAccess : IAPIProvider + { + public Bindable LocalUser { get; } = new Bindable(new User + { + Username = @"Dummy", + Id = 1, + }); + + public bool IsLoggedIn => true; + + public void Update() + { + } + + public virtual void Queue(APIRequest request) + { + } + + public void Register(IOnlineComponent component) + { + } + } +} diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs new file mode 100644 index 0000000000..4119691c85 --- /dev/null +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -0,0 +1,33 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Configuration; +using osu.Game.Users; + +namespace osu.Game.Online.API +{ + public interface IAPIProvider + { + /// + /// The local user. + /// + Bindable LocalUser { get; } + + /// + /// Returns whether the local user is logged in. + /// + bool IsLoggedIn { get; } + + /// + /// Queue a new request. + /// + /// The request to perform. + void Queue(APIRequest request); + + /// + /// Register a component to receive state changes. + /// + /// The component to register. + void Register(IOnlineComponent component); + } +} diff --git a/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.cs b/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.cs new file mode 100644 index 0000000000..d1685b01f3 --- /dev/null +++ b/osu.Game/Online/API/Requests/GetUserRecentActivitiesRequest.cs @@ -0,0 +1,130 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using Newtonsoft.Json; +using osu.Game.Rulesets.Scoring; +using Humanizer; +using System; +using System.Collections.Generic; + +namespace osu.Game.Online.API.Requests +{ + public class GetUserRecentActivitiesRequest : APIRequest> + { + private readonly long userId; + private readonly int offset; + + public GetUserRecentActivitiesRequest(long userId, int offset = 0) + { + this.userId = userId; + this.offset = offset; + } + + protected override string Target => $"users/{userId}/recent_activity?offset={offset}"; + } + + public class RecentActivity + { + [JsonProperty("id")] + public int ID; + + [JsonProperty("createdAt")] + public DateTimeOffset CreatedAt; + + [JsonProperty] + private string type + { + set => Type = (RecentActivityType)Enum.Parse(typeof(RecentActivityType), value.Pascalize()); + } + + public RecentActivityType Type; + + [JsonProperty] + private string scoreRank + { + set => ScoreRank = (ScoreRank)Enum.Parse(typeof(ScoreRank), value); + } + + public ScoreRank ScoreRank; + + [JsonProperty("rank")] + public int Rank; + + [JsonProperty("approval")] + public BeatmapApproval Approval; + + [JsonProperty("count")] + public int Count; + + [JsonProperty("mode")] + public string Mode; + + [JsonProperty("beatmap")] + public RecentActivityBeatmap Beatmap; + + [JsonProperty("beatmapset")] + public RecentActivityBeatmap Beatmapset; + + [JsonProperty("user")] + public RecentActivityUser User; + + [JsonProperty("achievement")] + public RecentActivityAchievement Achievement; + + public class RecentActivityBeatmap + { + [JsonProperty("title")] + public string Title; + + [JsonProperty("url")] + public string Url; + } + + public class RecentActivityUser + { + [JsonProperty("username")] + public string Username; + + [JsonProperty("url")] + public string Url; + + [JsonProperty("previousUsername")] + public string PreviousUsername; + } + + public class RecentActivityAchievement + { + [JsonProperty("slug")] + public string Slug; + + [JsonProperty("name")] + public string Name; + } + + } + + public enum RecentActivityType + { + Achievement, + BeatmapPlaycount, + BeatmapsetApprove, + BeatmapsetDelete, + BeatmapsetRevive, + BeatmapsetUpdate, + BeatmapsetUpload, + Medal, + Rank, + RankLost, + UserSupportAgain, + UserSupportFirst, + UserSupportGift, + UsernameChange, + } + + public enum BeatmapApproval + { + Ranked, + Approved, + Qualified, + } +} diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 906f42d50e..9966f78435 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -118,6 +118,8 @@ namespace osu.Game.Online.Chat case "beatmapsets": case "d": return new LinkDetails(LinkAction.OpenBeatmapSet, args[3]); + case "u": + return new LinkDetails(LinkAction.OpenUserProfile, args[3]); } } @@ -146,6 +148,9 @@ namespace osu.Game.Online.Chat case "spectate": linkType = LinkAction.Spectate; break; + case "u": + linkType = LinkAction.OpenUserProfile; + break; default: linkType = LinkAction.External; break; @@ -205,6 +210,15 @@ namespace osu.Game.Online.Chat return inputMessage; } + public static MessageFormatterResult FormatText(string text) + { + var result = format(text); + + result.Links.Sort(); + + return result; + } + public class MessageFormatterResult { public List Links = new List(); @@ -239,6 +253,7 @@ namespace osu.Game.Online.Chat OpenEditorTimestamp, JoinMultiplayerMatch, Spectate, + OpenUserProfile, } public class Link : IComparable diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 624179cfe1..89447b8ed6 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Overlays; using osu.Framework.Logging; -using osu.Game.Graphics.UserInterface.Volume; using osu.Framework.Allocation; using osu.Game.Overlays.Toolbar; using osu.Game.Screens; @@ -20,6 +19,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Audio; +using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Platform; using osu.Framework.Threading; @@ -30,7 +30,9 @@ using osu.Game.Rulesets; using osu.Game.Screens.Play; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Mods; +using osu.Game.Skinning; using OpenTK.Graphics; +using osu.Game.Overlays.Volume; namespace osu.Game { @@ -73,12 +75,14 @@ namespace osu.Game private OsuScreen screenStack; - private VolumeControl volume; + private VolumeOverlay volume; private OnScreenDisplay onscreenDisplay; private Bindable configRuleset; public Bindable Ruleset = new Bindable(); + private Bindable configSkin; + private readonly string[] args; private SettingsOverlay settings; @@ -105,6 +109,8 @@ namespace osu.Game { this.frameworkConfig = frameworkConfig; + ScoreStore.ScoreImported += score => Schedule(() => LoadScore(score)); + if (!Host.IsPrimaryInstance) { Logger.Log(@"osu! does not support multiple running instances.", LoggingTarget.Runtime, LogLevel.Error); @@ -114,15 +120,23 @@ namespace osu.Game if (args?.Length > 0) { var paths = args.Where(a => !a.StartsWith(@"-")); - Task.Run(() => BeatmapManager.Import(paths.ToArray())); + + Task.Run(() => Import(paths.ToArray())); } dependencies.CacheAs(this); + // bind config int to database RulesetInfo configRuleset = LocalConfig.GetBindable(OsuSetting.Ruleset); Ruleset.Value = RulesetStore.GetRuleset(configRuleset.Value) ?? RulesetStore.AvailableRulesets.First(); Ruleset.ValueChanged += r => configRuleset.Value = r.ID ?? 0; + // bind config int to database SkinInfo + configSkin = LocalConfig.GetBindable(OsuSetting.Skin); + SkinManager.CurrentSkinInfo.ValueChanged += s => configSkin.Value = s.ID; + configSkin.ValueChanged += id => SkinManager.CurrentSkinInfo.Value = SkinManager.Query(s => s.ID == id) ?? SkinInfo.Default; + configSkin.TriggerChange(); + LocalConfig.BindWith(OsuSetting.VolumeInactive, inactiveVolumeAdjust); } @@ -140,6 +154,12 @@ namespace osu.Game /// The set to display. public void ShowBeatmapSet(int setId) => beatmapSetOverlay.ShowBeatmapSet(setId); + /// + /// Show a user's profile as an overlay. + /// + /// The user to display. + public void ShowUser(long userId) => userProfile.ShowUser(userId); + protected void LoadScore(Score s) { scoreLoad?.Cancel(); @@ -184,7 +204,9 @@ namespace osu.Game CursorOverrideContainer.CanShowCursor = currentScreen?.CursorVisible ?? false; // hook up notifications to components. + SkinManager.PostNotification = n => notifications?.Post(n); BeatmapManager.PostNotification = n => notifications?.Post(n); + BeatmapManager.GetStableStorage = GetStorageForStableInstall; AddRange(new Drawable[] @@ -215,8 +237,9 @@ namespace osu.Game }, }, overlayContent.Add); - loadComponentSingleFile(volume = new VolumeControl(), Add); + loadComponentSingleFile(volume = new VolumeOverlay(), overlayContent.Add); loadComponentSingleFile(onscreenDisplay = new OnScreenDisplay(), Add); + loadComponentSingleFile(new ScreenshotManager(), Add); //overlay elements loadComponentSingleFile(direct = new DirectOverlay { Depth = -1 }, mainContent.Add); @@ -279,6 +302,21 @@ namespace osu.Game }; } + var singleDisplaySideOverlays = new OverlayContainer[] { settings, notifications }; + foreach (var overlay in singleDisplaySideOverlays) + { + overlay.StateChanged += state => + { + if (state == Visibility.Hidden) return; + + foreach (var c in singleDisplaySideOverlays) + { + if (c == overlay) continue; + c.State = Visibility.Hidden; + } + }; + } + // eventually informational overlays should be displayed in a stack, but for now let's only allow one to stay open at a time. var informationalOverlays = new OverlayContainer[] { beatmapSetOverlay, userProfile }; foreach (var overlay in informationalOverlays) @@ -398,6 +436,7 @@ namespace osu.Game sensitivity.Disabled = true; frameworkConfig.Set(FrameworkSetting.ActiveInputHandlers, string.Empty); + frameworkConfig.GetBindable(FrameworkSetting.ConfineMouseMode).SetDefault(); return true; case GlobalAction.ToggleToolbar: Toolbar.ToggleVisibility(); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 937b204c81..54a279e977 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -1,8 +1,11 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . +// Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Collections.Generic; using System.Diagnostics; +using System.IO; +using System.Linq; using System.Reflection; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -27,15 +30,18 @@ using osu.Game.Input.Bindings; using osu.Game.IO; using osu.Game.Rulesets; using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; namespace osu.Game { - public class OsuGameBase : Framework.Game, IOnlineComponent + public class OsuGameBase : Framework.Game, ICanAcceptFiles { protected OsuConfigManager LocalConfig; protected BeatmapManager BeatmapManager; + protected SkinManager SkinManager; + protected RulesetStore RulesetStore; protected FileStore FileStore; @@ -50,8 +56,6 @@ namespace osu.Game protected override string MainResourceFile => @"osu.Game.Resources.dll"; - public APIAccess API; - private Container content; protected override Container Content => content; @@ -100,20 +104,26 @@ namespace osu.Game runMigrations(); - dependencies.Cache(API = new APIAccess - { - Username = LocalConfig.Get(OsuSetting.Username), - Token = LocalConfig.Get(OsuSetting.Token) - }); + dependencies.Cache(SkinManager = new SkinManager(Host.Storage, contextFactory, Host, Audio)); + dependencies.CacheAs(SkinManager); - dependencies.Cache(RulesetStore = new RulesetStore(contextFactory.GetContext)); - dependencies.Cache(FileStore = new FileStore(contextFactory.GetContext, Host.Storage)); - dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, contextFactory.GetContext, RulesetStore, API, Host)); - dependencies.Cache(ScoreStore = new ScoreStore(Host.Storage, contextFactory.GetContext, Host, BeatmapManager, RulesetStore)); - dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory.GetContext, RulesetStore)); - dependencies.Cache(SettingsStore = new SettingsStore(contextFactory.GetContext)); + var api = new APIAccess(LocalConfig); + + dependencies.Cache(api); + dependencies.CacheAs(api); + + dependencies.Cache(RulesetStore = new RulesetStore(contextFactory)); + dependencies.Cache(FileStore = new FileStore(contextFactory, Host.Storage)); + dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, contextFactory, RulesetStore, api, Audio, Host)); + dependencies.Cache(ScoreStore = new ScoreStore(Host.Storage, contextFactory, Host, BeatmapManager, RulesetStore)); + dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); + dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(new OsuColour()); + fileImporters.Add(BeatmapManager); + fileImporters.Add(ScoreStore); + fileImporters.Add(SkinManager); + //this completely overrides the framework default. will need to change once we make a proper FontStore. dependencies.Cache(Fonts = new FontStore { ScaleAdjust = 100 }); @@ -170,17 +180,17 @@ namespace osu.Game lastBeatmap = b; }; - API.Register(this); - FileStore.Cleanup(); + + AddInternal(api); } private void runMigrations() { try { - using (var context = contextFactory.GetContext()) - context.Migrate(); + using (var db = contextFactory.GetForWrite()) + db.Context.Migrate(); } catch (MigrationFailedException e) { @@ -191,23 +201,13 @@ namespace osu.Game contextFactory.ResetDatabase(); Logger.Log("Database purged successfully.", LoggingTarget.Database, LogLevel.Important); - using (var context = contextFactory.GetContext()) - context.Migrate(); + using (var db = contextFactory.GetForWrite()) + db.Context.Migrate(); } } private WorkingBeatmap lastBeatmap; - public void APIStateChanged(APIAccess api, APIState state) - { - switch (state) - { - case APIState.Online: - LocalConfig.Set(OsuSetting.Username, LocalConfig.Get(OsuSetting.SaveUsername) ? API.Username : string.Empty); - break; - } - } - protected override void LoadComplete() { base.LoadComplete(); @@ -218,7 +218,7 @@ namespace osu.Game CursorOverrideContainer.Child = globalBinding = new GlobalActionContainer(this) { RelativeSizeAxes = Axes.Both, - Child = content = new OsuTooltipContainer(CursorOverrideContainer.Cursor) { RelativeSizeAxes = Axes.Both } + Child = content = new OsuTooltipContainer(CursorOverrideContainer.Cursor) { RelativeSizeAxes = Axes.Both } }; base.Content.Add(new DrawSizePreservingFillContainer { Child = CursorOverrideContainer }); @@ -240,22 +240,16 @@ namespace osu.Game base.SetHost(host); } - protected override void Update() + private readonly List fileImporters = new List(); + + public void Import(params string[] paths) { - base.Update(); - API.Update(); + var extension = Path.GetExtension(paths.First()); + + foreach (var importer in fileImporters) + if (importer.HandledExtensions.Contains(extension)) importer.Import(paths); } - protected override void Dispose(bool isDisposing) - { - //refresh token may have changed. - if (LocalConfig != null && API != null) - { - LocalConfig.Set(OsuSetting.Token, LocalConfig.Get(OsuSetting.SavePassword) ? API.Token : string.Empty); - LocalConfig.Save(); - } - - base.Dispose(isDisposing); - } + public string[] HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions).ToArray(); } } diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index 36b6a9964a..3ce0dfee31 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -223,13 +223,13 @@ namespace osu.Game.Overlays.BeatmapSet tabsBg.Colour = colours.Gray3; this.beatmaps = beatmaps; - beatmaps.BeatmapSetAdded += handleBeatmapAdd; + beatmaps.ItemAdded += handleBeatmapAdd; } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - if (beatmaps != null) beatmaps.BeatmapSetAdded -= handleBeatmapAdd; + if (beatmaps != null) beatmaps.ItemAdded -= handleBeatmapAdd; } private void handleBeatmapAdd(BeatmapSetInfo beatmap) diff --git a/osu.Game/Overlays/Chat/ChatTabControl.cs b/osu.Game/Overlays/Chat/ChatTabControl.cs index f028590bb4..1d3dab249d 100644 --- a/osu.Game/Overlays/Chat/ChatTabControl.cs +++ b/osu.Game/Overlays/Chat/ChatTabControl.cs @@ -53,9 +53,9 @@ namespace osu.Game.Overlays.Chat protected override void AddTabItem(TabItem item, bool addToDropdown = true) { - if (selectorTab.Depth < float.MaxValue) + if (item != selectorTab && TabContainer.GetLayoutPosition(selectorTab) < float.MaxValue) // performTabSort might've made selectorTab's position wonky, fix it - TabContainer.ChangeChildDepth(selectorTab, float.MaxValue); + TabContainer.SetLayoutPosition(selectorTab, float.MaxValue); base.AddTabItem(item, addToDropdown); diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index b973640837..210f5ce01e 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -479,7 +479,7 @@ namespace osu.Game.Overlays if (!api.IsLoggedIn) { - target.AddNewMessages(new ErrorMessage("Please login to participate in chat!")); + target.AddNewMessages(new ErrorMessage("Please sign in to participate in chat!")); return; } diff --git a/osu.Game/Overlays/Direct/DirectPanel.cs b/osu.Game/Overlays/Direct/DirectPanel.cs index e0d806c90f..cba63b4a49 100644 --- a/osu.Game/Overlays/Direct/DirectPanel.cs +++ b/osu.Game/Overlays/Direct/DirectPanel.cs @@ -186,7 +186,7 @@ namespace osu.Game.Overlays.Direct progressBar.FadeOut(500); }; - request.DownloadProgressed += progress => progressBar.Current.Value = progress; + request.DownloadProgressed += progress => Schedule(() => progressBar.Current.Value = progress); request.Success += data => { diff --git a/osu.Game/Overlays/Direct/PlayButton.cs b/osu.Game/Overlays/Direct/PlayButton.cs index 1d67bc2d90..0fb988ead7 100644 --- a/osu.Game/Overlays/Direct/PlayButton.cs +++ b/osu.Game/Overlays/Direct/PlayButton.cs @@ -22,6 +22,7 @@ namespace osu.Game.Overlays.Direct public Track Preview { get; private set; } private BeatmapSetInfo beatmapSet; + public BeatmapSetInfo BeatmapSet { get { return beatmapSet; } @@ -199,8 +200,7 @@ namespace osu.Game.Overlays.Direct // add back the user's music volume setting (since we are no longer in the global TrackManager's hierarchy). config.BindWith(FrameworkSetting.VolumeMusic, trackManager.Volume); - if (!string.IsNullOrEmpty(preview)) - Preview = trackManager.Get(preview); + Preview = trackManager.Get(preview); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs index 05b5bba09c..8d8a4aebaa 100644 --- a/osu.Game/Overlays/DirectOverlay.cs +++ b/osu.Game/Overlays/DirectOverlay.cs @@ -185,7 +185,7 @@ namespace osu.Game.Overlays resultCountsContainer.Colour = colours.Yellow; - beatmaps.BeatmapSetAdded += setAdded; + beatmaps.ItemAdded += setAdded; } private void setAdded(BeatmapSetInfo set) diff --git a/osu.Game/Overlays/Mods/DifficultyIncreaseSection.cs b/osu.Game/Overlays/Mods/DifficultyIncreaseSection.cs index cbf67893a9..1d9fdab8d5 100644 --- a/osu.Game/Overlays/Mods/DifficultyIncreaseSection.cs +++ b/osu.Game/Overlays/Mods/DifficultyIncreaseSection.cs @@ -21,7 +21,7 @@ namespace osu.Game.Overlays.Mods public DifficultyIncreaseSection() { - Header = @"Gameplay Difficulty Increase"; + Header = @"Difficulty Increase"; } } } diff --git a/osu.Game/Overlays/Mods/DifficultyReductionSection.cs b/osu.Game/Overlays/Mods/DifficultyReductionSection.cs index c44af8fc4d..651fc222b5 100644 --- a/osu.Game/Overlays/Mods/DifficultyReductionSection.cs +++ b/osu.Game/Overlays/Mods/DifficultyReductionSection.cs @@ -21,7 +21,7 @@ namespace osu.Game.Overlays.Mods public DifficultyReductionSection() { - Header = @"Gameplay Difficulty Reduction"; + Header = @"Difficulty Reduction"; } } } diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs index 91063bfa38..a4cc79bca6 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -4,9 +4,6 @@ using OpenTK; using OpenTK.Graphics; using OpenTK.Input; -using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -30,7 +27,6 @@ namespace osu.Game.Overlays.Mods private ModIcon backgroundIcon; private readonly SpriteText text; private readonly Container iconsContainer; - private SampleChannel sampleOn, sampleOff; /// /// Fired when the selection changes. @@ -100,7 +96,6 @@ namespace osu.Game.Overlays.Mods foregroundIcon.Highlighted = Selected; - (selectedIndex == -1 ? sampleOff : sampleOn).Play(); SelectionChanged?.Invoke(SelectedMod); return true; } @@ -152,13 +147,6 @@ namespace osu.Game.Overlays.Mods public virtual Mod SelectedMod => Mods.ElementAtOrDefault(selectedIndex); - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - sampleOn = audio.Sample.Get(@"UI/check-on"); - sampleOff = audio.Sample.Get(@"UI/check-off"); - } - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) { switch (args.Button) diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 03c1f0468c..4765787caf 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -27,14 +27,8 @@ namespace osu.Game.Overlays.Mods public string Header { - get - { - return headerLabel.Text; - } - set - { - headerLabel.Text = value; - } + get => headerLabel.Text; + set => headerLabel.Text = value; } public IEnumerable SelectedMods => buttons.Select(b => b.SelectedMod).Where(m => m != null); @@ -47,12 +41,12 @@ namespace osu.Game.Overlays.Mods { if (m == null) return new ModButtonEmpty(); - else - return new ModButton(m) - { - SelectedColour = selectedColour, - SelectionChanged = Action, - }; + + return new ModButton(m) + { + SelectedColour = selectedColour, + SelectionChanged = Action, + }; }).ToArray(); ButtonsContainer.Children = modContainers; @@ -65,10 +59,7 @@ namespace osu.Game.Overlays.Mods private Color4 selectedColour = Color4.White; public Color4 SelectedColour { - get - { - return selectedColour; - } + get => selectedColour; set { if (value == selectedColour) return; @@ -102,31 +93,31 @@ namespace osu.Game.Overlays.Mods { Mod selected = button.SelectedMod; if (selected == null) continue; - foreach (Type type in modTypes) + foreach (var type in modTypes) if (type.IsInstanceOfType(selected)) { if (immediate) button.Deselect(); else - Scheduler.AddDelayed(() => button.Deselect(), delay += 50); + Scheduler.AddDelayed(button.Deselect, delay += 50); } } } /// - /// Select one or more mods in this section. + /// Select one or more mods in this section and deselects all other ones. /// - /// The types of s which should be deselected. - public void SelectTypes(IEnumerable mods) + /// The types of s which should be selected. + public void SelectTypes(IEnumerable modTypes) { foreach (var button in buttons) { - for (int i = 0; i < button.Mods.Length; i++) - { - foreach (var mod in mods) - if (mod.GetType().IsInstanceOfType(button.Mods[i])) - button.SelectAt(i); - } + int i = Array.FindIndex(button.Mods, m => modTypes.Any(t => t.IsInstanceOfType(m))); + + if (i >= 0) + button.SelectAt(i); + else + button.Deselect(); } } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index d7268fb186..d8c95da94f 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -15,6 +15,8 @@ using osu.Game.Rulesets.Mods; using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets; using osu.Game.Graphics.UserInterface; @@ -49,7 +51,7 @@ namespace osu.Game.Overlays.Mods } [BackgroundDependencyLoader(permitNulls: true)] - private void load(OsuColour colours, OsuGame osu, RulesetStore rulesets) + private void load(OsuColour colours, OsuGame osu, RulesetStore rulesets, AudioManager audio) { SelectedMods.ValueChanged += selectedModsChanged; @@ -63,12 +65,23 @@ namespace osu.Game.Overlays.Mods Ruleset.ValueChanged += rulesetChanged; Ruleset.TriggerChange(); + + sampleOn = audio.Sample.Get(@"UI/check-on"); + sampleOff = audio.Sample.Get(@"UI/check-off"); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + Ruleset.UnbindAll(); + SelectedMods.UnbindAll(); } private void selectedModsChanged(IEnumerable obj) { foreach (ModSection section in ModSectionsContainer.Children) - section.SelectTypes(obj); + section.SelectTypes(obj.Select(m => m.GetType()).ToList()); updateMods(); } @@ -146,10 +159,21 @@ namespace osu.Game.Overlays.Mods section.DeselectTypes(modTypes, immediate); } + + private SampleChannel sampleOn, sampleOff; + private void modButtonPressed(Mod selectedMod) { if (selectedMod != null) + { + if (State == Visibility.Visible) sampleOn?.Play(); DeselectTypes(selectedMod.IncompatibleMods, true); + } + else + { + if (State == Visibility.Visible) sampleOff?.Play(); + } + refreshSelectedMods(); } @@ -279,7 +303,7 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.TopCentre, Action = modButtonPressed, }, - new AssistedSection + new SpecialSection { RelativeSizeAxes = Axes.X, Origin = Anchor.TopCentre, diff --git a/osu.Game/Overlays/Mods/AssistedSection.cs b/osu.Game/Overlays/Mods/SpecialSection.cs similarity index 82% rename from osu.Game/Overlays/Mods/AssistedSection.cs rename to osu.Game/Overlays/Mods/SpecialSection.cs index 978b12da19..75b2462ff5 100644 --- a/osu.Game/Overlays/Mods/AssistedSection.cs +++ b/osu.Game/Overlays/Mods/SpecialSection.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Overlays.Mods { - public class AssistedSection : ModSection + public class SpecialSection : ModSection { protected override Key[] ToggleKeys => new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M }; public override ModType ModType => ModType.Special; @@ -19,9 +19,9 @@ namespace osu.Game.Overlays.Mods SelectedColour = colours.BlueLight; } - public AssistedSection() + public SpecialSection() { - Header = @"Assisted"; + Header = @"Special"; } } } diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 34dcc36699..71fdcff6af 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -34,7 +34,19 @@ namespace osu.Game.Overlays.Music public Action OnSelect; - public bool IsDraggable => handle.IsHovered; + public bool IsDraggable { get; private set; } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + IsDraggable = handle.IsHovered; + return base.OnMouseDown(state, args); + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + IsDraggable = false; + return base.OnMouseUp(state, args); + } private bool selected; public bool Selected diff --git a/osu.Game/Overlays/Music/PlaylistList.cs b/osu.Game/Overlays/Music/PlaylistList.cs index 31b7d0f9aa..03ce7fd88f 100644 --- a/osu.Game/Overlays/Music/PlaylistList.cs +++ b/osu.Game/Overlays/Music/PlaylistList.cs @@ -101,11 +101,10 @@ namespace osu.Game.Overlays.Music public void AddBeatmapSet(BeatmapSetInfo beatmapSet) { - items.Add(new PlaylistItem(beatmapSet) - { - OnSelect = set => OnSelect?.Invoke(set), - Depth = items.Count - }); + var newItem = new PlaylistItem(beatmapSet) { OnSelect = set => OnSelect?.Invoke(set) }; + + items.Add(newItem); + items.SetLayoutPosition(newItem, items.Count); } public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) @@ -197,7 +196,7 @@ namespace osu.Game.Overlays.Music { var itemsPos = items.ToLocalSpace(nativeDragPosition); - int srcIndex = (int)draggedItem.Depth; + int srcIndex = (int)items.GetLayoutPosition(draggedItem); // Find the last item with position < mouse position. Note we can't directly use // the item positions as they are being transformed @@ -219,15 +218,15 @@ namespace osu.Game.Overlays.Music if (srcIndex < dstIndex) { for (int i = srcIndex + 1; i <= dstIndex; i++) - items.ChangeChildDepth(items[i], i - 1); + items.SetLayoutPosition(items[i], i - 1); } else { for (int i = dstIndex; i < srcIndex; i++) - items.ChangeChildDepth(items[i], i + 1); + items.SetLayoutPosition(items[i], i + 1); } - items.ChangeChildDepth(draggedItem, dstIndex); + items.SetLayoutPosition(draggedItem, dstIndex); } private class ItemSearchContainer : FillFlowContainer, IHasFilterableChildren @@ -243,9 +242,6 @@ namespace osu.Game.Overlays.Music } } - // Compare with reversed ChildID and Depth - protected override int Compare(Drawable x, Drawable y) => base.Compare(y, x); - public IEnumerable FilterableChildren => Children; public ItemSearchContainer() diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 2125984785..ac7ec6257b 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -74,8 +74,8 @@ namespace osu.Game.Overlays.Music }, }; - beatmaps.BeatmapSetAdded += list.AddBeatmapSet; - beatmaps.BeatmapSetRemoved += list.RemoveBeatmapSet; + beatmaps.ItemAdded += list.AddBeatmapSet; + beatmaps.ItemRemoved += list.RemoveBeatmapSet; list.BeatmapSets = beatmaps.GetAllUsableBeatmapSets(); diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 5f2c21e5d9..74f6e4435d 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; @@ -65,9 +64,12 @@ namespace osu.Game.Overlays AlwaysPresent = true; } + private Vector2 dragStart; + protected override bool OnDragStart(InputState state) { base.OnDragStart(state); + dragStart = state.Mouse.Position; return true; } @@ -75,10 +77,7 @@ namespace osu.Game.Overlays { if (base.OnDrag(state)) return true; - Trace.Assert(state.Mouse.PositionMouseDown != null, "state.Mouse.PositionMouseDown != null"); - - // ReSharper disable once PossibleInvalidOperationException - Vector2 change = state.Mouse.Position - state.Mouse.PositionMouseDown.Value; + Vector2 change = state.Mouse.Position - dragStart; // Diminish the drag distance as we go further to simulate "rubber band" feeling. change *= change.Length <= 0 ? 0 : (float)Math.Pow(change.Length, 0.7f) / change.Length; diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 2f46bb4a71..f5b281efc1 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -110,17 +110,7 @@ namespace osu.Game.Overlays private int runningDepth; - private void notificationClosed() - { - Schedule(() => - { - // hide ourselves if all notifications have been dismissed. - if (totalCount == 0) - State = Visibility.Hidden; - }); - - updateCounts(); - } + private void notificationClosed() => updateCounts(); private readonly Scheduler postScheduler = new Scheduler(); @@ -129,7 +119,6 @@ namespace osu.Game.Overlays public void Post(Notification notification) => postScheduler.Add(() => { ++runningDepth; - notification.Depth = notification.DisplayOnTop ? runningDepth : -runningDepth; notification.Closed += notificationClosed; @@ -138,7 +127,11 @@ namespace osu.Game.Overlays hasCompletionTarget.CompletionTarget = Post; var ourType = notification.GetType(); - sections.Children.FirstOrDefault(s => s.AcceptTypes.Any(accept => accept.IsAssignableFrom(ourType)))?.Add(notification); + + var section = sections.Children.FirstOrDefault(s => s.AcceptTypes.Any(accept => accept.IsAssignableFrom(ourType))); + section?.Add(notification, notification.DisplayOnTop ? -runningDepth : runningDepth); + + State = Visibility.Visible; updateCounts(); }); diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index 13a69fbe3a..533f5326e3 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -25,10 +25,13 @@ namespace osu.Game.Overlays.Notifications private FlowContainer notifications; public int DisplayedCount => notifications.Count(n => !n.WasClosed); - public int UnreadCount => notifications.Count(n => !n.WasClosed && !n.Read); - public void Add(Notification notification) => notifications.Add(notification); + public void Add(Notification notification, float position) + { + notifications.Add(notification); + notifications.SetLayoutPosition(notification, position); + } public IEnumerable AcceptTypes; diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index d085800f41..f4b363cd91 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -130,11 +130,7 @@ namespace osu.Game.Overlays.Profile } } }, - infoTextLeft = new OsuTextFlowContainer(t => - { - t.TextSize = 14; - t.Alpha = 0.8f; - }) + infoTextLeft = new OsuTextFlowContainer(t => t.TextSize = 14) { X = UserProfileOverlay.CONTENT_X_MARGIN, Y = cover_height + 20, @@ -318,11 +314,23 @@ namespace osu.Game.Overlays.Profile colourBar.Show(); } - void boldItalic(SpriteText t) + void boldItalic(SpriteText t) => t.Font = @"Exo2.0-BoldItalic"; + void lightText(SpriteText t) => t.Alpha = 0.8f; + + OsuSpriteText createScoreText(string text) => new OsuSpriteText { - t.Font = @"Exo2.0-BoldItalic"; - t.Alpha = 1; - } + TextSize = 14, + Text = text + }; + + OsuSpriteText createScoreNumberText(string text) => new OsuSpriteText + { + TextSize = 14, + Font = @"Exo2.0-Bold", + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Text = text + }; if (user.Age != null) { @@ -331,7 +339,7 @@ namespace osu.Game.Overlays.Profile if (user.Country != null) { - infoTextLeft.AddText("from "); + infoTextLeft.AddText("from ", lightText); infoTextLeft.AddText(user.Country.FullName, boldItalic); countryFlag.Country = user.Country; } @@ -344,18 +352,18 @@ namespace osu.Game.Overlays.Profile } else { - infoTextLeft.AddText("Joined "); - infoTextLeft.AddText(user.JoinDate.LocalDateTime.ToShortDateString(), boldItalic); + infoTextLeft.AddText("Joined ", lightText); + infoTextLeft.AddText(new DrawableDate(user.JoinDate), boldItalic); } infoTextLeft.NewLine(); - infoTextLeft.AddText("Last seen "); - infoTextLeft.AddText(user.LastVisit.LocalDateTime.ToShortDateString(), boldItalic); + infoTextLeft.AddText("Last seen ", lightText); + infoTextLeft.AddText(new DrawableDate(user.LastVisit), boldItalic); infoTextLeft.NewParagraph(); if (user.PlayStyle?.Length > 0) { - infoTextLeft.AddText("Plays with "); + infoTextLeft.AddText("Plays with ", lightText); infoTextLeft.AddText(string.Join(", ", user.PlayStyle), boldItalic); } @@ -411,23 +419,6 @@ namespace osu.Game.Overlays.Profile } } - // These could be local functions when C# 7 enabled - - private OsuSpriteText createScoreText(string text) => new OsuSpriteText - { - TextSize = 14, - Text = text - }; - - private OsuSpriteText createScoreNumberText(string text) => new OsuSpriteText - { - TextSize = 14, - Font = @"Exo2.0-Bold", - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Text = text - }; - private void tryAddInfoRightLine(FontAwesome icon, string str, string url = null) { if (string.IsNullOrEmpty(str)) return; @@ -436,10 +427,12 @@ namespace osu.Game.Overlays.Profile if (url != null) { infoTextRight.AddLink(" " + str, url); - } else + } + else { infoTextRight.AddText(" " + str); } + infoTextRight.NewLine(); } diff --git a/osu.Game/Overlays/Profile/RankGraph.cs b/osu.Game/Overlays/Profile/RankGraph.cs index 429049c7bc..369bdee65f 100644 --- a/osu.Game/Overlays/Profile/RankGraph.cs +++ b/osu.Game/Overlays/Profile/RankGraph.cs @@ -95,7 +95,7 @@ namespace osu.Game.Overlays.Profile { placeholder.FadeIn(fade_duration, Easing.Out); - if (user == null) + if (user?.Statistics?.Ranks.Global == null) { rankText.Text = string.Empty; performanceText.Text = string.Empty; @@ -105,7 +105,7 @@ namespace osu.Game.Overlays.Profile return; } - int[] userRanks = user.RankHistory?.Data ?? new[] { user.Statistics.Ranks.Global }; + int[] userRanks = user.RankHistory?.Data ?? new[] { user.Statistics.Ranks.Global.Value }; ranks = userRanks.Select((x, index) => new KeyValuePair(index, x)).Where(x => x.Value != 0).ToArray(); if (ranks.Length > 1) diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs index 51b202844a..509356ae04 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs @@ -40,24 +40,21 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks [BackgroundDependencyLoader(true)] private void load(OsuColour colour) { - RightFlowContainer.Add(new OsuSpriteText + var text = new OsuSpriteText { Text = $"accuracy: {Score.Accuracy:P2}", Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Colour = colour.GrayA, TextSize = 11, - Font = "Exo2.0-RegularItalic", - Depth = -1, - }); + Font = "Exo2.0-RegularItalic" + }; + + RightFlowContainer.Add(text); + RightFlowContainer.SetLayoutPosition(text, 1); LeftFlowContainer.Add(new BeatmapMetadataContainer(Score.Beatmap)); - LeftFlowContainer.Add(new OsuSpriteText - { - Text = Score.Date.LocalDateTime.ToShortDateString(), - TextSize = 11, - Colour = OsuColour.Gray(0xAA), - }); + LeftFlowContainer.Add(new DrawableDate(Score.Date)); foreach (Mod mod in Score.Mods) modsContainer.Add(new ModIcon(mod) { Scale = new Vector2(0.5f) }); diff --git a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs new file mode 100644 index 0000000000..e8be8d1e44 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs @@ -0,0 +1,162 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.Chat; +using osu.Game.Screens.Select.Leaderboards; + +namespace osu.Game.Overlays.Profile.Sections.Recent +{ + public class DrawableRecentActivity : DrawableProfileRow + { + private APIAccess api; + + private readonly RecentActivity activity; + + private LinkFlowContainer content; + + public DrawableRecentActivity(RecentActivity activity) + { + this.activity = activity; + } + + [BackgroundDependencyLoader] + private void load(APIAccess api) + { + this.api = api; + + LeftFlowContainer.Padding = new MarginPadding { Left = 10, Right = 160 }; + + LeftFlowContainer.Add(content = new LinkFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + }); + + RightFlowContainer.Add(new DrawableDate(activity.CreatedAt) + { + TextSize = 13, + Colour = OsuColour.Gray(0xAA), + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }); + + var formatted = createMessage(); + + content.AddLinks(formatted.Text, formatted.Links); + } + + protected override Drawable CreateLeftVisual() + { + switch (activity.Type) + { + case RecentActivityType.Rank: + return new DrawableRank(activity.ScoreRank) + { + RelativeSizeAxes = Axes.Y, + Width = 60, + FillMode = FillMode.Fit, + }; + + case RecentActivityType.Achievement: + return new MedalIcon(activity.Achievement.Slug) + { + RelativeSizeAxes = Axes.Y, + Width = 60, + FillMode = FillMode.Fit, + }; + + default: + return new Container + { + RelativeSizeAxes = Axes.Y, + Width = 60, + FillMode = FillMode.Fit, + }; + } + } + + private string toAbsoluteUrl(string url) => $"{api.Endpoint}{url}"; + + private MessageFormatter.MessageFormatterResult createMessage() + { + string userLinkTemplate() => $"[{toAbsoluteUrl(activity.User?.Url)} {activity.User?.Username}]"; + string beatmapLinkTemplate() => $"[{toAbsoluteUrl(activity.Beatmap?.Url)} {activity.Beatmap?.Title}]"; + string beatmapsetLinkTemplate() => $"[{toAbsoluteUrl(activity.Beatmapset?.Url)} {activity.Beatmapset?.Title}]"; + + string message; + + switch (activity.Type) + { + case RecentActivityType.Achievement: + message = $"{userLinkTemplate()} unlocked the {activity.Achievement.Name} medal!"; + break; + + case RecentActivityType.BeatmapPlaycount: + message = $"{beatmapLinkTemplate()} has been played {activity.Count} times!"; + break; + + case RecentActivityType.BeatmapsetApprove: + message = $"{beatmapsetLinkTemplate()} has been {activity.Approval.ToString().ToLowerInvariant()}!"; + break; + + case RecentActivityType.BeatmapsetDelete: + message = $"{beatmapsetLinkTemplate()} has been deleted."; + break; + + case RecentActivityType.BeatmapsetRevive: + message = $"{beatmapsetLinkTemplate()} has been revived from eternal slumber by {userLinkTemplate()}."; + break; + + case RecentActivityType.BeatmapsetUpdate: + message = $"{userLinkTemplate()} has updated the beatmap {beatmapsetLinkTemplate()}!"; + break; + + case RecentActivityType.BeatmapsetUpload: + message = $"{userLinkTemplate()} has submitted a new beatmap {beatmapsetLinkTemplate()}!"; + break; + + case RecentActivityType.Medal: + // apparently this shouldn't exist look at achievement instead (https://github.com/ppy/osu-web/blob/master/resources/assets/coffee/react/profile-page/recent-activity.coffee#L111) + message = string.Empty; + break; + + case RecentActivityType.Rank: + message = $"{userLinkTemplate()} achieved rank #{activity.Rank} on {beatmapLinkTemplate()} ({activity.Mode}!)"; + break; + + case RecentActivityType.RankLost: + message = $"{userLinkTemplate()} has lost first place on {beatmapLinkTemplate()} ({activity.Mode}!)"; + break; + + case RecentActivityType.UserSupportAgain: + message = $"{userLinkTemplate()} has once again chosen to support osu! - thanks for your generosity!"; + break; + + case RecentActivityType.UserSupportFirst: + message = $"{userLinkTemplate()} has become an osu! supporter - thanks for your generosity!"; + break; + + case RecentActivityType.UserSupportGift: + message = $"{userLinkTemplate()} has received the gift of osu! supporter!"; + break; + + case RecentActivityType.UsernameChange: + message = $"{activity.User?.PreviousUsername} has changed their username to {userLinkTemplate()}!"; + break; + + default: + message = string.Empty; + break; + } + + return MessageFormatter.FormatText(message); + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Recent/MedalIcon.cs b/osu.Game/Overlays/Profile/Sections/Recent/MedalIcon.cs new file mode 100644 index 0000000000..6ffbe7193f --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Recent/MedalIcon.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Overlays.Profile.Sections.Recent +{ + public class MedalIcon : Container + { + private readonly string slug; + private readonly Sprite sprite; + + private string url => $@"https://s.ppy.sh/images/medals-client/{slug}@2x.png"; + + public MedalIcon(string slug) + { + this.slug = slug; + + Child = sprite = new Sprite + { + Height = 40, + Width = 40, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + sprite.Texture = textures.Get(url); + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs new file mode 100644 index 0000000000..d479627cde --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs @@ -0,0 +1,48 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Game.Online.API.Requests; +using osu.Game.Users; +using System.Linq; + +namespace osu.Game.Overlays.Profile.Sections.Recent +{ + public class PaginatedRecentActivityContainer : PaginatedContainer + { + public PaginatedRecentActivityContainer(Bindable user, string header, string missing) + : base(user, header, missing) + { + ItemsPerPage = 5; + } + + protected override void ShowMore() + { + base.ShowMore(); + + var req = new GetUserRecentActivitiesRequest(User.Value.Id, VisiblePages++ * ItemsPerPage); + + req.Success += activities => + { + ShowMoreButton.FadeTo(activities.Count == ItemsPerPage ? 1 : 0); + ShowMoreLoading.Hide(); + + if (!activities.Any() && VisiblePages == 1) + { + MissingText.Show(); + return; + } + + MissingText.Hide(); + + foreach (RecentActivity activity in activities) + { + ItemsContainer.Add(new DrawableRecentActivity(activity)); + } + }; + + Api.Queue(req); + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/RecentSection.cs b/osu.Game/Overlays/Profile/Sections/RecentSection.cs index 78b139efe8..84a941aa1a 100644 --- a/osu.Game/Overlays/Profile/Sections/RecentSection.cs +++ b/osu.Game/Overlays/Profile/Sections/RecentSection.cs @@ -1,12 +1,22 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Overlays.Profile.Sections.Recent; + namespace osu.Game.Overlays.Profile.Sections { public class RecentSection : ProfileSection { public override string Title => "Recent"; - public override string Identifier => "recent_activities"; + public override string Identifier => "recent_activity"; + + public RecentSection() + { + Children = new[] + { + new PaginatedRecentActivityContainer(User, null, @"This user hasn't done anything notable recently!"), + }; + } } } diff --git a/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs index 867410b178..ba591b9456 100644 --- a/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs @@ -18,14 +18,19 @@ namespace osu.Game.Overlays.Settings.Sections.Debug { new SettingsCheckbox { - LabelText = "Bypass caching", - Bindable = config.GetBindable(DebugSetting.BypassCaching) + LabelText = "Show log overlay", + Bindable = frameworkConfig.GetBindable(FrameworkSetting.ShowLogOverlay) }, new SettingsCheckbox { - LabelText = "Debug logs", - Bindable = frameworkConfig.GetBindable(FrameworkSetting.ShowLogOverlay) - } + LabelText = "Performance logging", + Bindable = frameworkConfig.GetBindable(FrameworkSetting.PerformanceLogging) + }, + new SettingsCheckbox + { + LabelText = "Bypass caching (slow)", + Bindable = config.GetBindable(DebugSetting.BypassCaching) + }, }; } } diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index e0f940e14c..2d07704853 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -207,9 +207,9 @@ namespace osu.Game.Overlays.Settings.Sections.General { username = new OsuTextBox { - PlaceholderText = "Username", + PlaceholderText = "Email address", RelativeSizeAxes = Axes.X, - Text = api?.Username ?? string.Empty, + Text = api?.ProvidedUsername ?? string.Empty, TabbableContentContainer = this }, password = new OsuPasswordTextBox @@ -221,12 +221,12 @@ namespace osu.Game.Overlays.Settings.Sections.General }, new SettingsCheckbox { - LabelText = "Remember username", + LabelText = "Remember email address", Bindable = config.GetBindable(OsuSetting.SaveUsername), }, new SettingsCheckbox { - LabelText = "Stay logged in", + LabelText = "Stay signed in", Bindable = config.GetBindable(OsuSetting.SavePassword), }, new SettingsButton @@ -236,7 +236,7 @@ namespace osu.Game.Overlays.Settings.Sections.General }, new SettingsButton { - Text = "Register new account", + Text = "Register", //Action = registerLink } }; diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs index b9d76c05f0..fa57a85454 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs @@ -1,6 +1,8 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Game.Configuration; namespace osu.Game.Overlays.Settings.Sections.Graphics @@ -12,7 +14,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - Children = new[] + Children = new Drawable[] { new SettingsCheckbox { @@ -24,6 +26,11 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics LabelText = "Rotate cursor when dragging", Bindable = config.GetBindable(OsuSetting.CursorRotation) }, + new SettingsEnumDropdown + { + LabelText = "Screenshot format", + Bindable = config.GetBindable(OsuSetting.ScreenshotFormat) + } }; } } diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 1223310c74..d9fedd0225 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() => { deleteButton.Enabled.Value = false; - Task.Run(() => beatmaps.DeleteAll()).ContinueWith(t => Schedule(() => deleteButton.Enabled.Value = true)); + Task.Run(() => beatmaps.Delete(beatmaps.GetAllUsableBeatmapSets())).ContinueWith(t => Schedule(() => deleteButton.Enabled.Value = true)); })); } }, @@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Action = () => { undeleteButton.Enabled.Value = false; - Task.Run(() => beatmaps.UndeleteAll()).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true)); + Task.Run(() => beatmaps.Undelete(beatmaps.QueryBeatmapSets(b => b.DeletePending).ToList())).ContinueWith(t => Schedule(() => undeleteButton.Enabled.Value = true)); } }, }; diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index f6915896d7..5df5304751 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -1,26 +1,33 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; +using osu.Game.Skinning; using OpenTK; namespace osu.Game.Overlays.Settings.Sections { public class SkinSection : SettingsSection { + private SettingsDropdown skinDropdown; + public override string Header => "Skin"; + public override FontAwesome Icon => FontAwesome.fa_paint_brush; [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(OsuConfigManager config, SkinManager skins) { FlowContent.Spacing = new Vector2(0, 5); Children = new Drawable[] { + skinDropdown = new SettingsDropdown(), new SettingsSlider { LabelText = "Menu cursor size", @@ -39,6 +46,14 @@ namespace osu.Game.Overlays.Settings.Sections Bindable = config.GetBindable(OsuSetting.AutoCursorSize) }, }; + + void reloadSkins() => skinDropdown.Items = skins.GetAllUsableSkins().Select(s => new KeyValuePair(s.ToString(), s.ID)); + skins.ItemAdded += _ => reloadSkins(); + skins.ItemRemoved += _ => reloadSkins(); + + reloadSkins(); + + skinDropdown.Bindable = config.GetBindable(OsuSetting.Skin); } private class SizeSlider : OsuSliderBar diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 5afc415d83..cc290fe1bb 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -45,7 +45,8 @@ namespace osu.Game.Overlays.Settings if (text == null) { // construct lazily for cases where the label is not needed (may be provided by the Control). - Add(text = new OsuSpriteText { Depth = 1 }); + Add(text = new OsuSpriteText()); + FlowContent.SetLayoutPosition(text, -1); } text.Text = value; diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs index c2dfea9a08..16586adc0c 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs @@ -55,7 +55,7 @@ namespace osu.Game.Overlays.Toolbar avatar.User = new User(); break; case APIState.Online: - Text = api.Username; + Text = api.LocalUser.Value.Username; avatar.User = api.LocalUser; break; } diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 59f940a19d..aed0a6d7c6 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -73,6 +73,14 @@ namespace osu.Game.Overlays FadeEdgeEffectTo(0, DISAPPEAR_DURATION, Easing.Out); } + public void ShowUser(long userId) + { + if (userId == Header.User.Id) + return; + + ShowUser(new User { Id = userId }); + } + public void ShowUser(User user, bool fetchOnline = true) { userReq?.Cancel(); @@ -82,7 +90,7 @@ namespace osu.Game.Overlays sections = new ProfileSection[] { //new AboutSection(), - //new RecentSection(), + new RecentSection(), new RanksSection(), //new MedalsSection(), new HistoricalSection(), @@ -161,15 +169,18 @@ namespace osu.Game.Overlays { Header.User = user; - foreach (string id in user.ProfileOrder) + if (user.ProfileOrder != null) { - var sec = sections.FirstOrDefault(s => s.Identifier == id); - if (sec != null) + foreach (string id in user.ProfileOrder) { - sec.User.Value = user; + var sec = sections.FirstOrDefault(s => s.Identifier == id); + if (sec != null) + { + sec.User.Value = user; - sectionsContainer.Add(sec); - tabs.AddItem(sec); + sectionsContainer.Add(sec); + tabs.AddItem(sec); + } } } } diff --git a/osu.Game/Overlays/Volume/MuteButton.cs b/osu.Game/Overlays/Volume/MuteButton.cs new file mode 100644 index 0000000000..adfc9c610f --- /dev/null +++ b/osu.Game/Overlays/Volume/MuteButton.cs @@ -0,0 +1,83 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; +using osu.Game.Graphics; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Overlays.Volume +{ + public class MuteButton : Container, IHasCurrentValue + { + public Bindable Current { get; } = new Bindable(); + + private Color4 hoveredColour, unhoveredColour; + private const float width = 100; + public const float HEIGHT = 35; + + public MuteButton() + { + Masking = true; + BorderThickness = 3; + CornerRadius = HEIGHT / 2; + Size = new Vector2(width, HEIGHT); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + hoveredColour = colours.YellowDark; + BorderColour = unhoveredColour = colours.Gray1.Opacity(0.9f); + + SpriteIcon icon; + AddRange(new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray1, + Alpha = 0.9f, + }, + icon = new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(20), + } + }); + + Current.ValueChanged += newValue => + { + icon.Icon = newValue ? FontAwesome.fa_volume_off : FontAwesome.fa_volume_up; + icon.Margin = new MarginPadding { Left = newValue ? width / 2 - 15 : width / 2 - 10 }; //Magic numbers to line up both icons because they're different widths + }; + Current.TriggerChange(); + } + + protected override bool OnHover(InputState state) + { + this.TransformTo("BorderColour", hoveredColour, 500, Easing.OutQuint); + return true; + } + + protected override void OnHoverLost(InputState state) + { + this.TransformTo("BorderColour", unhoveredColour, 500, Easing.OutQuint); + } + + protected override bool OnClick(InputState state) + { + Current.Value = !Current.Value; + return true; + } + } +} diff --git a/osu.Game/Graphics/UserInterface/Volume/VolumeControlReceptor.cs b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs similarity index 90% rename from osu.Game/Graphics/UserInterface/Volume/VolumeControlReceptor.cs rename to osu.Game/Overlays/Volume/VolumeControlReceptor.cs index 2328533665..a5be7dc445 100644 --- a/osu.Game/Graphics/UserInterface/Volume/VolumeControlReceptor.cs +++ b/osu.Game/Overlays/Volume/VolumeControlReceptor.cs @@ -7,7 +7,7 @@ using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Game.Input.Bindings; -namespace osu.Game.Graphics.UserInterface.Volume +namespace osu.Game.Overlays.Volume { public class VolumeControlReceptor : Container, IKeyBindingHandler, IHandleGlobalInput { diff --git a/osu.Game/Overlays/Volume/VolumeMeter.cs b/osu.Game/Overlays/Volume/VolumeMeter.cs new file mode 100644 index 0000000000..b1951f4d72 --- /dev/null +++ b/osu.Game/Overlays/Volume/VolumeMeter.cs @@ -0,0 +1,193 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Globalization; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Bindings; +using osu.Framework.MathUtils; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Input.Bindings; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Overlays.Volume +{ + public class VolumeMeter : Container, IKeyBindingHandler + { + private CircularProgress volumeCircle; + public BindableDouble Bindable { get; } = new BindableDouble { MinValue = 0, MaxValue = 1 }; + private readonly float circleSize; + private readonly Color4 meterColour; + private readonly string name; + + private OsuSpriteText text; + private BufferedContainer maxGlow; + + public VolumeMeter(string name, float circleSize, Color4 meterColour) + { + this.circleSize = circleSize; + this.meterColour = meterColour; + this.name = name; + + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Add(new Container + { + Size = new Vector2(120, 20), + CornerRadius = 10, + Masking = true, + Margin = new MarginPadding { Left = circleSize + 10 }, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray1, + Alpha = 0.9f, + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = "Exo2.0-Bold", + Text = name + } + } + }); + + CircularProgress bgProgress; + + Add(new CircularContainer + { + Masking = true, + Size = new Vector2(circleSize), + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray1, + Alpha = 0.9f, + }, + bgProgress = new CircularProgress + { + RelativeSizeAxes = Axes.Both, + InnerRadius = 0.05f, + Rotation = 180, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = colours.Gray2, + Size = new Vector2(0.8f) + }, + new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.8f), + Padding = new MarginPadding(-Blur.KernelSize(5)), + Rotation = 180, + Child = (volumeCircle = new CircularProgress + { + RelativeSizeAxes = Axes.Both, + InnerRadius = 0.05f, + }).WithEffect(new GlowEffect + { + Colour = meterColour, + Strength = 2, + PadExtent = true + }), + }, + maxGlow = (text = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = "Venera", + TextSize = 0.16f * circleSize + }).WithEffect(new GlowEffect + { + Colour = Color4.Transparent, + PadExtent = true, + }) + } + }); + + Bindable.ValueChanged += newVolume => { this.TransformTo("DisplayVolume", newVolume, 400, Easing.OutQuint); }; + bgProgress.Current.Value = 0.75f; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Bindable.TriggerChange(); + } + + private double displayVolume; + + protected double DisplayVolume + { + get => displayVolume; + set + { + displayVolume = value; + + if (displayVolume > 0.99f) + { + text.Text = "MAX"; + maxGlow.EffectColour = meterColour.Opacity(2f); + } + else + { + maxGlow.EffectColour = Color4.Transparent; + text.Text = Math.Round(displayVolume * 100).ToString(CultureInfo.CurrentCulture); + } + + volumeCircle.Current.Value = displayVolume * 0.75f; + } + } + + public double Volume + { + get => Bindable; + private set => Bindable.Value = value; + } + + public void Increase() => Volume += 0.05f; + + public void Decrease() => Volume -= 0.05f; + + public bool OnPressed(GlobalAction action) + { + if (!IsHovered) return false; + + switch (action) + { + case GlobalAction.DecreaseVolume: + Decrease(); + return true; + case GlobalAction.IncreaseVolume: + Increase(); + return true; + } + + return false; + } + + public bool OnReleased(GlobalAction action) => false; + } +} diff --git a/osu.Game/Graphics/UserInterface/Volume/VolumeControl.cs b/osu.Game/Overlays/VolumeOverlay.cs similarity index 57% rename from osu.Game/Graphics/UserInterface/Volume/VolumeControl.cs rename to osu.Game/Overlays/VolumeOverlay.cs index ccf70af6ed..17a4b139b0 100644 --- a/osu.Game/Graphics/UserInterface/Volume/VolumeControl.cs +++ b/osu.Game/Overlays/VolumeOverlay.cs @@ -1,57 +1,84 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Threading; -using OpenTK; -using osu.Framework.Audio; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Configuration; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Threading; +using osu.Game.Graphics; using osu.Game.Input.Bindings; +using osu.Game.Overlays.Volume; +using OpenTK; +using OpenTK.Graphics; -namespace osu.Game.Graphics.UserInterface.Volume +namespace osu.Game.Overlays { - public class VolumeControl : OverlayContainer + public class VolumeOverlay : OverlayContainer { - private readonly VolumeMeter volumeMeterMaster; - private readonly IconButton muteIcon; + private const float offset = 10; + + private VolumeMeter volumeMeterMaster; + private VolumeMeter volumeMeterEffect; + private VolumeMeter volumeMeterMusic; + private MuteButton muteButton; protected override bool BlockPassThroughMouse => false; - public VolumeControl() - { - AutoSizeAxes = Axes.Both; - Anchor = Anchor.BottomRight; - Origin = Anchor.BottomRight; + private readonly BindableDouble muteAdjustment = new BindableDouble(); - Children = new Drawable[] + [BackgroundDependencyLoader] + private void load(AudioManager audio, OsuColour colours) + { + AutoSizeAxes = Axes.X; + RelativeSizeAxes = Axes.Y; + + AddRange(new Drawable[] { + new Box + { + RelativeSizeAxes = Axes.Y, + Width = 300, + Colour = ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.75f), Color4.Black.Opacity(0)) + }, new FillFlowContainer { + Direction = FillDirection.Vertical, AutoSizeAxes = Axes.Both, - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Margin = new MarginPadding { Left = 10, Right = 10, Top = 30, Bottom = 30 }, - Spacing = new Vector2(15, 0), + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Spacing = new Vector2(0, offset), + Margin = new MarginPadding { Left = offset }, Children = new Drawable[] { - new Container + volumeMeterEffect = new VolumeMeter("EFFECTS", 125, colours.BlueDarker) { - Size = new Vector2(IconButton.BUTTON_SIZE), - Child = muteIcon = new IconButton - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Icon = FontAwesome.fa_volume_up, - Action = () => Adjust(GlobalAction.ToggleMute), - } + Margin = new MarginPadding { Top = 100 + MuteButton.HEIGHT } //to counter the mute button and re-center the volume meters }, - volumeMeterMaster = new VolumeMeter("Master"), - volumeMeterEffect = new VolumeMeter("Effects"), - volumeMeterMusic = new VolumeMeter("Music") + volumeMeterMaster = new VolumeMeter("MASTER", 150, colours.PinkDarker), + volumeMeterMusic = new VolumeMeter("MUSIC", 125, colours.BlueDarker), + muteButton = new MuteButton + { + Margin = new MarginPadding { Top = 100 } + } } - } + }, + }); + + volumeMeterMaster.Bindable.BindTo(audio.Volume); + volumeMeterEffect.Bindable.BindTo(audio.VolumeSample); + volumeMeterMusic.Bindable.BindTo(audio.VolumeTrack); + + muteButton.Current.ValueChanged += mute => + { + if (mute) + audio.AddAdjustment(AdjustableProperty.Volume, muteAdjustment); + else + audio.RemoveAdjustment(AdjustableProperty.Volume, muteAdjustment); }; } @@ -62,7 +89,13 @@ namespace osu.Game.Graphics.UserInterface.Volume volumeMeterMaster.Bindable.ValueChanged += _ => settingChanged(); volumeMeterEffect.Bindable.ValueChanged += _ => settingChanged(); volumeMeterMusic.Bindable.ValueChanged += _ => settingChanged(); - muted.ValueChanged += _ => settingChanged(); + muteButton.Current.ValueChanged += _ => settingChanged(); + } + + private void settingChanged() + { + Show(); + schedulePopOut(); } public bool Adjust(GlobalAction action) @@ -83,50 +116,15 @@ namespace osu.Game.Graphics.UserInterface.Volume return true; case GlobalAction.ToggleMute: Show(); - muted.Toggle(); + muteButton.Current.Value = !muteButton.Current; return true; } return false; } - private void settingChanged() - { - Show(); - schedulePopOut(); - } - - private readonly BindableDouble muteAdjustment = new BindableDouble(); - - private readonly BindableBool muted = new BindableBool(); - - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - volumeMeterMaster.Bindable.BindTo(audio.Volume); - volumeMeterEffect.Bindable.BindTo(audio.VolumeSample); - volumeMeterMusic.Bindable.BindTo(audio.VolumeTrack); - - muted.ValueChanged += mute => - { - if (mute) - { - audio.AddAdjustment(AdjustableProperty.Volume, muteAdjustment); - muteIcon.Icon = FontAwesome.fa_volume_off; - } - else - { - audio.RemoveAdjustment(AdjustableProperty.Volume, muteAdjustment); - muteIcon.Icon = FontAwesome.fa_volume_up; - } - }; - } - private ScheduledDelegate popOutDelegate; - private readonly VolumeMeter volumeMeterEffect; - private readonly VolumeMeter volumeMeterMusic; - protected override void PopIn() { ClearTransforms(); diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 7f22b3764c..c076b53f51 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -4,17 +4,21 @@ using System; using System.Collections.Generic; using System.Linq; -using OpenTK.Graphics; +using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; using osu.Framework.Logging; +using osu.Framework.MathUtils; using osu.Framework.Timing; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Edit.Layers.Selection; using osu.Game.Rulesets.Edit.Tools; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Edit.Screens.Compose; +using osu.Game.Screens.Edit.Screens.Compose.Layers; using osu.Game.Screens.Edit.Screens.Compose.RadioButtons; namespace osu.Game.Rulesets.Edit @@ -25,6 +29,14 @@ namespace osu.Game.Rulesets.Edit protected ICompositionTool CurrentTool { get; private set; } + private RulesetContainer rulesetContainer; + private readonly List layerContainers = new List(); + + private readonly Bindable beatmap = new Bindable(); + private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); + + private IAdjustableClock adjustableClock; + protected HitObjectComposer(Ruleset ruleset) { this.ruleset = ruleset; @@ -32,13 +44,20 @@ namespace osu.Game.Rulesets.Edit RelativeSizeAxes = Axes.Both; } - [BackgroundDependencyLoader] - private void load(OsuGameBase osuGame) + [BackgroundDependencyLoader(true)] + private void load([NotNull] OsuGameBase osuGame, [NotNull] IAdjustableClock adjustableClock, [NotNull] IFrameBasedClock framedClock, [CanBeNull] BindableBeatDivisor beatDivisor) { - RulesetContainer rulesetContainer; + this.adjustableClock = adjustableClock; + + if (beatDivisor != null) + this.beatDivisor.BindTo(beatDivisor); + + beatmap.BindTo(osuGame.Beatmap); + try { - rulesetContainer = CreateRulesetContainer(ruleset, osuGame.Beatmap.Value); + rulesetContainer = CreateRulesetContainer(ruleset, beatmap.Value); + rulesetContainer.Clock = framedClock; } catch (Exception e) { @@ -46,6 +65,26 @@ namespace osu.Game.Rulesets.Edit return; } + HitObjectMaskLayer hitObjectMaskLayer = new HitObjectMaskLayer(this); + SelectionLayer selectionLayer = new SelectionLayer(rulesetContainer.Playfield); + + var layerBelowRuleset = new BorderLayer + { + RelativeSizeAxes = Axes.Both, + Child = CreateLayerContainer() + }; + + var layerAboveRuleset = CreateLayerContainer(); + layerAboveRuleset.Children = new Drawable[] + { + selectionLayer, // Below object overlays for input + hitObjectMaskLayer, + selectionLayer.CreateProxy() // Proxy above object overlays for selections + }; + + layerContainers.Add(layerBelowRuleset); + layerContainers.Add(layerAboveRuleset); + RadioButtonCollection toolboxCollection; InternalChild = new GridContainer { @@ -66,20 +105,13 @@ namespace osu.Game.Rulesets.Edit }, new Container { + Name = "Content", RelativeSizeAxes = Axes.Both, - Masking = true, - BorderColour = Color4.White, - BorderThickness = 2, Children = new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true, - }, + layerBelowRuleset, rulesetContainer, - new SelectionLayer(rulesetContainer.Playfield) + layerAboveRuleset } } }, @@ -90,7 +122,10 @@ namespace osu.Game.Rulesets.Edit } }; - rulesetContainer.Clock = new InterpolatingFramedClock((IAdjustableClock)osuGame.Beatmap.Value.Track ?? new StopwatchClock()); + selectionLayer.ObjectSelected += hitObjectMaskLayer.AddOverlay; + selectionLayer.ObjectDeselected += hitObjectMaskLayer.RemoveOverlay; + selectionLayer.SelectionCleared += hitObjectMaskLayer.RemoveSelectionOverlay; + selectionLayer.SelectionFinished += hitObjectMaskLayer.AddSelectionOverlay; toolboxCollection.Items = new[] { new RadioButton("Select", () => setCompositionTool(null)) } @@ -102,10 +137,141 @@ namespace osu.Game.Rulesets.Edit toolboxCollection.Items[0].Select(); } + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + layerContainers.ForEach(l => + { + l.Anchor = rulesetContainer.Playfield.Anchor; + l.Origin = rulesetContainer.Playfield.Origin; + l.Position = rulesetContainer.Playfield.Position; + l.Size = rulesetContainer.Playfield.Size; + }); + } + + protected override bool OnWheel(InputState state) + { + if (state.Mouse.WheelDelta > 0) + SeekBackward(true); + else + SeekForward(true); + return true; + } + + /// + /// Seeks the current time one beat-snapped beat-length backwards. + /// + /// Whether to snap to the closest beat. + public void SeekBackward(bool snapped = false) => seek(-1, snapped); + + /// + /// Seeks the current time one beat-snapped beat-length forwards. + /// + /// Whether to snap to the closest beat. + public void SeekForward(bool snapped = false) => seek(1, snapped); + + private void seek(int direction, bool snapped) + { + var cpi = beatmap.Value.Beatmap.ControlPointInfo; + + var timingPoint = cpi.TimingPointAt(adjustableClock.CurrentTime); + if (direction < 0 && timingPoint.Time == adjustableClock.CurrentTime) + { + // When going backwards and we're at the boundary of two timing points, we compute the seek distance with the timing point which we are seeking into + int activeIndex = cpi.TimingPoints.IndexOf(timingPoint); + while (activeIndex > 0 && adjustableClock.CurrentTime == timingPoint.Time) + timingPoint = cpi.TimingPoints[--activeIndex]; + } + + double seekAmount = timingPoint.BeatLength / beatDivisor; + double seekTime = adjustableClock.CurrentTime + seekAmount * direction; + + if (!snapped || cpi.TimingPoints.Count == 0) + { + adjustableClock.Seek(seekTime); + return; + } + + // We will be snapping to beats within timingPoint + seekTime -= timingPoint.Time; + + // Determine the index from timingPoint of the closest beat to seekTime, accounting for scrolling direction + int closestBeat; + if (direction > 0) + closestBeat = (int)Math.Floor(seekTime / seekAmount); + else + closestBeat = (int)Math.Ceiling(seekTime / seekAmount); + + seekTime = timingPoint.Time + closestBeat * seekAmount; + + // Due to the rounding above, we may end up on the current beat. This will effectively cause 0 seeking to happen, but we don't want this. + // Instead, we'll go to the next beat in the direction when this is the case + if (Precision.AlmostEquals(adjustableClock.CurrentTime, seekTime)) + { + closestBeat += direction > 0 ? 1 : -1; + seekTime = timingPoint.Time + closestBeat * seekAmount; + } + + if (seekTime < timingPoint.Time && timingPoint != cpi.TimingPoints.First()) + seekTime = timingPoint.Time; + + var nextTimingPoint = cpi.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time); + if (seekTime > nextTimingPoint?.Time) + seekTime = nextTimingPoint.Time; + + adjustableClock.Seek(seekTime); + } + + public void SeekTo(double seekTime, bool snapped = false) + { + if (!snapped) + { + adjustableClock.Seek(seekTime); + return; + } + + var timingPoint = beatmap.Value.Beatmap.ControlPointInfo.TimingPointAt(seekTime); + double beatSnapLength = timingPoint.BeatLength / beatDivisor; + + // We will be snapping to beats within the timing point + seekTime -= timingPoint.Time; + + // Determine the index from the current timing point of the closest beat to seekTime + int closestBeat = (int)Math.Round(seekTime / beatSnapLength); + seekTime = timingPoint.Time + closestBeat * beatSnapLength; + + // Depending on beatSnapLength, we may snap to a beat that is beyond timingPoint's end time, but we want to instead snap to + // the next timing point's start time + var nextTimingPoint = beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time); + if (seekTime > nextTimingPoint?.Time) + seekTime = nextTimingPoint.Time; + + adjustableClock.Seek(seekTime); + } + private void setCompositionTool(ICompositionTool tool) => CurrentTool = tool; protected virtual RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => ruleset.CreateRulesetContainerWith(beatmap, true); protected abstract IReadOnlyList CompositionTools { get; } + + /// + /// Creates a for a specific . + /// + /// The to create the overlay for. + public virtual HitObjectMask CreateMaskFor(DrawableHitObject hitObject) => null; + + /// + /// Creates a which outlines s + /// and handles all hitobject movement/pattern adjustments. + /// + /// The overlays. + public virtual SelectionBox CreateSelectionOverlay(IReadOnlyList overlays) => new SelectionBox(overlays); + + /// + /// Creates a which provides a layer above or below the . + /// + protected virtual ScalableContainer CreateLayerContainer() => new ScalableContainer { RelativeSizeAxes = Axes.Both }; } } diff --git a/osu.Game/Rulesets/Edit/HitObjectMask.cs b/osu.Game/Rulesets/Edit/HitObjectMask.cs new file mode 100644 index 0000000000..051b42fec6 --- /dev/null +++ b/osu.Game/Rulesets/Edit/HitObjectMask.cs @@ -0,0 +1,21 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Edit +{ + /// + /// A mask placed above a adding editing functionality. + /// + public class HitObjectMask : Container + { + public readonly DrawableHitObject HitObject; + + public HitObjectMask(DrawableHitObject hitObject) + { + HitObject = hitObject; + } + } +} diff --git a/osu.Game/Rulesets/Edit/Layers/Selection/Handle.cs b/osu.Game/Rulesets/Edit/Layers/Selection/Handle.cs deleted file mode 100644 index b10c21af6c..0000000000 --- a/osu.Game/Rulesets/Edit/Layers/Selection/Handle.cs +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input; -using osu.Game.Graphics; -using OpenTK; -using OpenTK.Graphics; -using RectangleF = osu.Framework.Graphics.Primitives.RectangleF; - -namespace osu.Game.Rulesets.Edit.Layers.Selection -{ - /// - /// Represents a marker visible on the border of a which exposes - /// properties that are used to resize a . - /// - public class Handle : CompositeDrawable - { - private const float marker_size = 10; - - /// - /// Invoked when this requires the current drag rectangle. - /// - public Func GetDragRectangle; - - /// - /// Invoked when this wants to update the drag rectangle. - /// - public Action UpdateDragRectangle; - - /// - /// Invoked when this has finished updates to the drag rectangle. - /// - public Action FinishDrag; - - private Color4 normalColour; - private Color4 hoverColour; - - public Handle() - { - Size = new Vector2(marker_size); - - InternalChild = new CircularContainer - { - RelativeSizeAxes = Axes.Both, - Masking = true, - Child = new Box { RelativeSizeAxes = Axes.Both } - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Colour = normalColour = colours.Yellow; - hoverColour = colours.YellowDarker; - } - - protected override bool OnDragStart(InputState state) => true; - - protected override bool OnDrag(InputState state) - { - var currentRectangle = GetDragRectangle(); - - float left = currentRectangle.Left; - float right = currentRectangle.Right; - float top = currentRectangle.Top; - float bottom = currentRectangle.Bottom; - - // Apply modifications to the capture rectangle - if ((Anchor & Anchor.y0) > 0) - top += state.Mouse.Delta.Y; - else if ((Anchor & Anchor.y2) > 0) - bottom += state.Mouse.Delta.Y; - - if ((Anchor & Anchor.x0) > 0) - left += state.Mouse.Delta.X; - else if ((Anchor & Anchor.x2) > 0) - right += state.Mouse.Delta.X; - - UpdateDragRectangle(RectangleF.FromLTRB(left, top, right, bottom)); - return true; - } - - protected override bool OnDragEnd(InputState state) - { - FinishDrag(); - return true; - } - - protected override bool OnHover(InputState state) - { - this.FadeColour(hoverColour, 100); - return true; - } - - protected override void OnHoverLost(InputState state) - { - this.FadeColour(normalColour, 100); - } - } -} diff --git a/osu.Game/Rulesets/Edit/Layers/Selection/HandleContainer.cs b/osu.Game/Rulesets/Edit/Layers/Selection/HandleContainer.cs deleted file mode 100644 index 359cdd009a..0000000000 --- a/osu.Game/Rulesets/Edit/Layers/Selection/HandleContainer.cs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Linq; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; - -namespace osu.Game.Rulesets.Edit.Layers.Selection -{ - /// - /// A that has s around its border. - /// - public class HandleContainer : CompositeDrawable - { - /// - /// Invoked when a requires the current drag rectangle. - /// - public Func GetDragRectangle; - - /// - /// Invoked when a wants to update the drag rectangle. - /// - public Action UpdateDragRectangle; - - /// - /// Invoked when a has finished updates to the drag rectangle. - /// - public Action FinishDrag; - - public HandleContainer() - { - InternalChildren = new Drawable[] - { - new Handle - { - Anchor = Anchor.TopLeft, - Origin = Anchor.Centre - }, - new Handle - { - Anchor = Anchor.TopCentre, - Origin = Anchor.Centre - }, - new Handle - { - Anchor = Anchor.TopRight, - Origin = Anchor.Centre - }, - new Handle - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.Centre - }, - new Handle - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.Centre - }, - new Handle - { - Anchor = Anchor.CentreRight, - Origin = Anchor.Centre - }, - new Handle - { - Anchor = Anchor.BottomRight, - Origin = Anchor.Centre - }, - new Handle - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.Centre - }, - new OriginHandle - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre - } - }; - - InternalChildren.OfType().ForEach(m => - { - m.GetDragRectangle = () => GetDragRectangle(); - m.UpdateDragRectangle = r => UpdateDragRectangle(r); - m.FinishDrag = () => FinishDrag(); - }); - } - } -} diff --git a/osu.Game/Rulesets/Edit/Layers/Selection/HitObjectSelectionBox.cs b/osu.Game/Rulesets/Edit/Layers/Selection/HitObjectSelectionBox.cs deleted file mode 100644 index 864851dcd3..0000000000 --- a/osu.Game/Rulesets/Edit/Layers/Selection/HitObjectSelectionBox.cs +++ /dev/null @@ -1,179 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; -using osu.Game.Rulesets.Objects.Drawables; -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Configuration; -using RectangleF = osu.Framework.Graphics.Primitives.RectangleF; - -namespace osu.Game.Rulesets.Edit.Layers.Selection -{ - /// - /// A box that represents a drag selection. - /// - public class HitObjectSelectionBox : CompositeDrawable - { - public readonly Bindable Selection = new Bindable(); - - /// - /// The s that can be selected through a drag-selection. - /// - public IEnumerable CapturableObjects; - - private readonly Container borderMask; - private readonly Drawable background; - private readonly HandleContainer handles; - - private Color4 captureFinishedColour; - - private readonly Vector2 startPos; - - /// - /// Creates a new . - /// - /// The point at which the drag was initiated, in the parent's coordinates. - public HitObjectSelectionBox(Vector2 startPos) - { - this.startPos = startPos; - - InternalChildren = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(-1), - Child = borderMask = new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderColour = Color4.White, - BorderThickness = 2, - MaskingSmoothness = 1, - Child = background = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0.1f, - AlwaysPresent = true - }, - } - }, - handles = new HandleContainer - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - GetDragRectangle = () => dragRectangle, - UpdateDragRectangle = updateDragRectangle, - FinishDrag = FinishCapture - } - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - captureFinishedColour = colours.Yellow; - } - - /// - /// The secondary corner of the drag selection box. A rectangle will be fit between the starting position and this value. - /// - public Vector2 DragEndPosition { set => updateDragRectangle(RectangleF.FromLTRB(startPos.X, startPos.Y, value.X, value.Y)); } - - private RectangleF dragRectangle; - private void updateDragRectangle(RectangleF rectangle) - { - dragRectangle = rectangle; - - Position = new Vector2( - Math.Min(rectangle.Left, rectangle.Right), - Math.Min(rectangle.Top, rectangle.Bottom)); - - Size = new Vector2( - Math.Max(rectangle.Left, rectangle.Right) - Position.X, - Math.Max(rectangle.Top, rectangle.Bottom) - Position.Y); - } - - private readonly List capturedHitObjects = new List(); - - /// - /// Processes hitobjects to determine which ones are captured by the drag selection. - /// Captured hitobjects will be enclosed by the drag selection upon . - /// - public void BeginCapture() - { - capturedHitObjects.Clear(); - - foreach (var obj in CapturableObjects) - { - if (!obj.IsAlive || !obj.IsPresent) - continue; - - if (ScreenSpaceDrawQuad.Contains(obj.SelectionPoint)) - capturedHitObjects.Add(obj); - } - } - - /// - /// Encloses hitobjects captured through in the drag selection box. - /// - public void FinishCapture() - { - if (capturedHitObjects.Count == 0) - { - Hide(); - return; - } - - // Move the rectangle to cover the hitobjects - var topLeft = new Vector2(float.MaxValue, float.MaxValue); - var bottomRight = new Vector2(float.MinValue, float.MinValue); - - foreach (var obj in capturedHitObjects) - { - topLeft = Vector2.ComponentMin(topLeft, Parent.ToLocalSpace(obj.SelectionQuad.TopLeft)); - bottomRight = Vector2.ComponentMax(bottomRight, Parent.ToLocalSpace(obj.SelectionQuad.BottomRight)); - } - - topLeft -= new Vector2(5); - bottomRight += new Vector2(5); - - this.MoveTo(topLeft, 200, Easing.OutQuint) - .ResizeTo(bottomRight - topLeft, 200, Easing.OutQuint); - - dragRectangle = RectangleF.FromLTRB(topLeft.X, topLeft.Y, bottomRight.X, bottomRight.Y); - - borderMask.BorderThickness = 3; - borderMask.FadeColour(captureFinishedColour, 200); - - // Transform into markers to let the user modify the drag selection further. - background.Delay(50).FadeOut(200); - handles.FadeIn(200); - - Selection.Value = new SelectionInfo - { - Objects = capturedHitObjects, - SelectionQuad = Parent.ToScreenSpace(dragRectangle) - }; - } - - private bool isActive = true; - public override bool HandleKeyboardInput => isActive; - public override bool HandleMouseInput => isActive; - - public override void Hide() - { - isActive = false; - this.FadeOut(400, Easing.OutQuint).Expire(); - - Selection.Value = null; - } - } -} diff --git a/osu.Game/Rulesets/Edit/Layers/Selection/OriginHandle.cs b/osu.Game/Rulesets/Edit/Layers/Selection/OriginHandle.cs deleted file mode 100644 index 6f8c946165..0000000000 --- a/osu.Game/Rulesets/Edit/Layers/Selection/OriginHandle.cs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; -using OpenTK; - -namespace osu.Game.Rulesets.Edit.Layers.Selection -{ - /// - /// Represents the origin of a . - /// - public class OriginHandle : CompositeDrawable - { - private const float marker_size = 10; - private const float line_width = 2; - - public OriginHandle() - { - Size = new Vector2(marker_size); - - InternalChildren = new[] - { - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - Height = line_width - }, - new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Width = line_width - }, - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Colour = colours.Yellow; - } - } -} diff --git a/osu.Game/Rulesets/Edit/Layers/Selection/SelectionInfo.cs b/osu.Game/Rulesets/Edit/Layers/Selection/SelectionInfo.cs deleted file mode 100644 index beedb415c2..0000000000 --- a/osu.Game/Rulesets/Edit/Layers/Selection/SelectionInfo.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System.Collections.Generic; -using osu.Framework.Graphics.Primitives; -using osu.Game.Rulesets.Objects.Drawables; - -namespace osu.Game.Rulesets.Edit.Layers.Selection -{ - public class SelectionInfo - { - /// - /// The objects that are captured by the selection. - /// - public IEnumerable Objects; - - /// - /// The screen space quad of the selection box surrounding . - /// - public Quad SelectionQuad; - } -} diff --git a/osu.Game/Rulesets/Edit/Layers/Selection/SelectionLayer.cs b/osu.Game/Rulesets/Edit/Layers/Selection/SelectionLayer.cs deleted file mode 100644 index 93755d400a..0000000000 --- a/osu.Game/Rulesets/Edit/Layers/Selection/SelectionLayer.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) 2007-2018 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using osu.Framework.Configuration; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input; -using osu.Game.Rulesets.UI; - -namespace osu.Game.Rulesets.Edit.Layers.Selection -{ - public class SelectionLayer : CompositeDrawable - { - public readonly Bindable Selection = new Bindable(); - - private readonly Playfield playfield; - - public SelectionLayer(Playfield playfield) - { - this.playfield = playfield; - - RelativeSizeAxes = Axes.Both; - } - - private HitObjectSelectionBox selectionBoxBox; - - protected override bool OnDragStart(InputState state) - { - // Hide the previous drag box - we won't be working with it any longer - selectionBoxBox?.Hide(); - - AddInternal(selectionBoxBox = new HitObjectSelectionBox(ToLocalSpace(state.Mouse.NativeState.Position)) - { - CapturableObjects = playfield.HitObjects.Objects, - }); - - Selection.BindTo(selectionBoxBox.Selection); - - return true; - } - - protected override bool OnDrag(InputState state) - { - selectionBoxBox.DragEndPosition = ToLocalSpace(state.Mouse.NativeState.Position); - selectionBoxBox.BeginCapture(); - return true; - } - - protected override bool OnDragEnd(InputState state) - { - selectionBoxBox.FinishCapture(); - return true; - } - - protected override bool OnClick(InputState state) - { - selectionBoxBox?.Hide(); - return true; - } - } -} diff --git a/osu.Game/Rulesets/Edit/Types/IHasEditablePosition.cs b/osu.Game/Rulesets/Edit/Types/IHasEditablePosition.cs new file mode 100644 index 0000000000..fa101ed835 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Types/IHasEditablePosition.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets.Objects.Types; +using OpenTK; + +namespace osu.Game.Rulesets.Edit.Types +{ + public interface IHasEditablePosition : IHasPosition + { + void OffsetPosition(Vector2 offset); + } +} diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index c1bf55b214..0f0c43bc88 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -9,7 +9,10 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; +using OpenTK.Graphics; namespace osu.Game.Rulesets.Judgements { @@ -18,43 +21,37 @@ namespace osu.Game.Rulesets.Judgements /// public class DrawableJudgement : Container { + private const float judgement_size = 80; + protected readonly Judgement Judgement; - protected readonly SpriteText JudgementText; + public readonly DrawableHitObject JudgedObject; + + protected SpriteText JudgementText; /// /// Creates a drawable which visualises a . /// /// The judgement to visualise. - public DrawableJudgement(Judgement judgement) + public DrawableJudgement(Judgement judgement, DrawableHitObject judgedObject) { Judgement = judgement; + JudgedObject = judgedObject; - AutoSizeAxes = Axes.Both; - - Children = new[] - { - JudgementText = new OsuSpriteText - { - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - Text = judgement.Result.GetDescription().ToUpper(), - Font = @"Venera", - Scale = new Vector2(0.85f, 1), - TextSize = 12 - } - }; + Size = new Vector2(judgement_size); } [BackgroundDependencyLoader] private void load(OsuColour colours) { - switch (Judgement.Result) + Child = new SkinnableDrawable($"Play/{Judgement.Result}", _ => JudgementText = new OsuSpriteText { - case HitResult.Miss: - Colour = colours.Red; - break; - } + Text = Judgement.Result.GetDescription().ToUpper(), + Font = @"Venera", + Colour = Judgement.Result == HitResult.Miss ? colours.Red : Color4.White, + Scale = new Vector2(0.85f, 1), + TextSize = 12 + }, restrictSize: false); } protected override void LoadComplete() diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 3356a56c33..9f45cada7e 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mods public override string Name => "Autoplay"; public override string ShortenedName => "AT"; public override FontAwesome Icon => FontAwesome.fa_osu_mod_auto; - public override string Description => "Watch a perfect automated play through the song"; + public override string Description => "Watch a perfect automated play through the song."; public override double ScoreMultiplier => 0; public bool AllowFail => false; public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail) }; diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index c0480b0647..015f7381fb 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -11,5 +11,6 @@ namespace osu.Game.Rulesets.Mods public override string ShortenedName => "CN"; public override bool HasImplementation => false; public override FontAwesome Icon => FontAwesome.fa_osu_mod_cinema; + public override string Description => "Watch the video without visual distractions."; } } diff --git a/osu.Game/Rulesets/Mods/ModDaycore.cs b/osu.Game/Rulesets/Mods/ModDaycore.cs index 180199cd70..da4263875b 100644 --- a/osu.Game/Rulesets/Mods/ModDaycore.cs +++ b/osu.Game/Rulesets/Mods/ModDaycore.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Mods public override string Name => "Daycore"; public override string ShortenedName => "DC"; public override FontAwesome Icon => FontAwesome.fa_question; - public override string Description => "whoaaaaa"; + public override string Description => "Whoaaaaa..."; public override void ApplyToClock(IAdjustableClock clock) { diff --git a/osu.Game/Rulesets/Mods/ModDoubleTime.cs b/osu.Game/Rulesets/Mods/ModDoubleTime.cs index 0b8f4b0b5b..6225a6feee 100644 --- a/osu.Game/Rulesets/Mods/ModDoubleTime.cs +++ b/osu.Game/Rulesets/Mods/ModDoubleTime.cs @@ -7,18 +7,16 @@ using osu.Game.Graphics; namespace osu.Game.Rulesets.Mods { - public class ModDoubleTime : Mod, IApplicableToClock + public abstract class ModDoubleTime : Mod, IApplicableToClock { public override string Name => "Double Time"; public override string ShortenedName => "DT"; public override FontAwesome Icon => FontAwesome.fa_osu_mod_doubletime; public override ModType Type => ModType.DifficultyIncrease; - public override string Description => "Zoooooooooom"; + public override string Description => "Zoooooooooom..."; public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModHalfTime) }; - public override double ScoreMultiplier => 1.12; - public virtual void ApplyToClock(IAdjustableClock clock) { clock.Rate = 1.5; diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index 5c5b9b1b44..7037edfa31 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -13,7 +13,6 @@ namespace osu.Game.Rulesets.Mods public override string ShortenedName => "EZ"; public override FontAwesome Icon => FontAwesome.fa_osu_mod_easy; public override ModType Type => ModType.DifficultyReduction; - public override string Description => "Reduces overall difficulty - larger circles, more forgiving HP drain, less accuracy required."; public override double ScoreMultiplier => 0.5; public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) }; diff --git a/osu.Game/Rulesets/Mods/ModHalfTime.cs b/osu.Game/Rulesets/Mods/ModHalfTime.cs index bb9ed0047d..883225a66b 100644 --- a/osu.Game/Rulesets/Mods/ModHalfTime.cs +++ b/osu.Game/Rulesets/Mods/ModHalfTime.cs @@ -13,12 +13,10 @@ namespace osu.Game.Rulesets.Mods public override string ShortenedName => "HT"; public override FontAwesome Icon => FontAwesome.fa_osu_mod_halftime; public override ModType Type => ModType.DifficultyReduction; - public override string Description => "Less zoom"; + public override string Description => "Less zoom..."; public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModDoubleTime) }; - public override double ScoreMultiplier => 1.12; - public virtual void ApplyToClock(IAdjustableClock clock) { clock.Rate = 0.75; diff --git a/osu.Game/Rulesets/Mods/ModHardRock.cs b/osu.Game/Rulesets/Mods/ModHardRock.cs index c4c0f38faf..c998bc123f 100644 --- a/osu.Game/Rulesets/Mods/ModHardRock.cs +++ b/osu.Game/Rulesets/Mods/ModHardRock.cs @@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Mods public override FontAwesome Icon => FontAwesome.fa_osu_mod_hardrock; public override ModType Type => ModType.DifficultyIncrease; public override string Description => "Everything just got a bit harder..."; + public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModEasy) }; public void ApplyToDifficulty(BeatmapDifficulty difficulty) diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index ad4df55b91..c2925f440f 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Mods public override string Name => "Nightcore"; public override string ShortenedName => "NC"; public override FontAwesome Icon => FontAwesome.fa_osu_mod_nightcore; - public override string Description => "uguuuuuuuu"; + public override string Description => "Uguuuuuuuu..."; public override void ApplyToClock(IAdjustableClock clock) { diff --git a/osu.Game/Rulesets/Mods/ModNoFail.cs b/osu.Game/Rulesets/Mods/ModNoFail.cs index 9686eff99c..8a849825a2 100644 --- a/osu.Game/Rulesets/Mods/ModNoFail.cs +++ b/osu.Game/Rulesets/Mods/ModNoFail.cs @@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Mods { public abstract class ModNoFail : Mod, IApplicableFailOverride { - public override string Name => "NoFail"; + public override string Name => "No Fail"; public override string ShortenedName => "NF"; public override FontAwesome Icon => FontAwesome.fa_osu_mod_nofail; public override ModType Type => ModType.DifficultyReduction; diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs index bb12b2e39f..116d13bf0a 100644 --- a/osu.Game/Rulesets/Mods/ModPerfect.cs +++ b/osu.Game/Rulesets/Mods/ModPerfect.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Game.Graphics; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mods @@ -9,6 +10,7 @@ namespace osu.Game.Rulesets.Mods { public override string Name => "Perfect"; public override string ShortenedName => "PF"; + public override FontAwesome Icon => FontAwesome.fa_osu_mod_perfect; public override string Description => "SS or quit."; protected override bool FailCondition(ScoreProcessor scoreProcessor) => scoreProcessor.Accuracy.Value != 1; diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs index 490825220c..ef9ff4c69e 100644 --- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs +++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mods public override string ShortenedName => "SD"; public override FontAwesome Icon => FontAwesome.fa_osu_mod_suddendeath; public override ModType Type => ModType.DifficultyIncrease; - public override string Description => "Miss a note and fail."; + public override string Description => "Miss and fail."; public override double ScoreMultiplier => 1; public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) }; diff --git a/osu.Game/Rulesets/Mods/MultiMod.cs b/osu.Game/Rulesets/Mods/MultiMod.cs index 1de5297e22..5548313f8e 100644 --- a/osu.Game/Rulesets/Mods/MultiMod.cs +++ b/osu.Game/Rulesets/Mods/MultiMod.cs @@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Mods public override string Name => string.Empty; public override string ShortenedName => string.Empty; public override string Description => string.Empty; - public override double ScoreMultiplier => 0.0; + public override double ScoreMultiplier => 0; public Mod[] Mods; } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 2db02724ed..348364a2bf 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -3,24 +3,22 @@ using System; using System.Collections.Generic; -using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; -using osu.Game.Rulesets.Judgements; -using Container = osu.Framework.Graphics.Containers.Container; -using osu.Game.Rulesets.Objects.Types; -using OpenTK.Graphics; -using osu.Game.Audio; using System.Linq; -using osu.Game.Graphics; +using osu.Framework.Allocation; using osu.Framework.Configuration; -using OpenTK; using osu.Framework.Graphics.Primitives; +using osu.Game.Audio; +using osu.Game.Graphics; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; +using OpenTK; +using OpenTK.Graphics; namespace osu.Game.Rulesets.Objects.Drawables { - public abstract class DrawableHitObject : Container, IHasAccentColour + public abstract class DrawableHitObject : SkinReloadableDrawable, IHasAccentColour { public readonly HitObject HitObject; @@ -32,11 +30,13 @@ namespace osu.Game.Rulesets.Objects.Drawables // Todo: Rulesets should be overriding the resources instead, but we need to figure out where/when to apply overrides first protected virtual string SampleNamespace => null; - protected List Samples = new List(); + protected SkinnableSound Samples; + protected virtual IEnumerable GetSamples() => HitObject.Samples; - private List nestedHitObjects; - public IReadOnlyList NestedHitObjects => nestedHitObjects; + private readonly Lazy> nestedHitObjects = new Lazy>(); + public bool HasNestedHitObjects => nestedHitObjects.IsValueCreated; + public IReadOnlyList NestedHitObjects => nestedHitObjects.Value; public event Action OnJudgement; public event Action OnJudgementRemoved; @@ -52,12 +52,12 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// Whether this and all of its nested s have been hit. /// - public bool IsHit => Judgements.Any(j => j.Final && j.IsHit) && (NestedHitObjects?.All(n => n.IsHit) ?? true); + public bool IsHit => Judgements.Any(j => j.Final && j.IsHit) && (!HasNestedHitObjects || NestedHitObjects.All(n => n.IsHit)); /// /// Whether this and all of its nested s have been judged. /// - public bool AllJudged => (!ProvidesJudgement || judgementFinalized) && (NestedHitObjects?.All(h => h.AllJudged) ?? true); + public bool AllJudged => (!ProvidesJudgement || judgementFinalized) && (!HasNestedHitObjects || NestedHitObjects.All(h => h.AllJudged)); /// /// Whether this can be judged. @@ -83,31 +83,22 @@ namespace osu.Game.Rulesets.Objects.Drawables } [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load() { - var samples = GetSamples(); + var samples = GetSamples().ToArray(); + if (samples.Any()) { if (HitObject.SampleControlPoint == null) throw new ArgumentNullException(nameof(HitObject.SampleControlPoint), $"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}." + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); - - foreach (SampleInfo s in samples) + AddInternal(Samples = new SkinnableSound(samples.Select(s => new SampleInfo { - SampleInfo localSampleInfo = new SampleInfo - { - Bank = s.Bank ?? HitObject.SampleControlPoint.SampleBank, - Name = s.Name, - Volume = s.Volume > 0 ? s.Volume : HitObject.SampleControlPoint.SampleVolume - }; - - SampleChannel channel = localSampleInfo.GetChannel(audio.Sample, SampleNamespace); - - if (channel == null) - continue; - - Samples.Add(channel); - } + Bank = s.Bank ?? HitObject.SampleControlPoint.SampleBank, + Name = s.Name, + Volume = s.Volume > 0 ? s.Volume : HitObject.SampleControlPoint.SampleVolume, + Namespace = SampleNamespace + }).ToArray())); } } @@ -139,7 +130,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// Plays all the hitsounds for this . /// - public void PlaySamples() => Samples.ForEach(s => s?.Play()); + public void PlaySamples() => Samples?.Play(); protected override void Update() { @@ -169,14 +160,11 @@ namespace osu.Game.Rulesets.Objects.Drawables protected virtual void AddNested(DrawableHitObject h) { - if (nestedHitObjects == null) - nestedHitObjects = new List(); - h.OnJudgement += (d, j) => OnJudgement?.Invoke(d, j); h.OnJudgementRemoved += (d, j) => OnJudgementRemoved?.Invoke(d, j); h.ApplyCustomUpdateState += (d, j) => ApplyCustomUpdateState?.Invoke(d, j); - nestedHitObjects.Add(h); + nestedHitObjects.Value.Add(h); } /// @@ -220,11 +208,9 @@ namespace osu.Game.Rulesets.Objects.Drawables if (AllJudged) return false; - if (NestedHitObjects != null) - { + if (HasNestedHitObjects) foreach (var d in NestedHitObjects) judgementOccurred |= d.UpdateJudgement(userTriggered); - } if (!ProvidesJudgement || judgementFinalized || judgementOccurred) return judgementOccurred; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 5fdc9a07e1..5084b28cf2 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -9,6 +9,7 @@ using System.Globalization; using osu.Game.Beatmaps.Formats; using osu.Game.Audio; using System.Linq; +using osu.Framework.MathUtils; namespace osu.Game.Rulesets.Objects.Legacy { @@ -41,9 +42,11 @@ namespace osu.Game.Rulesets.Objects.Legacy } else if ((type & ConvertHitObjectType.Slider) > 0) { + var pos = new Vector2(int.Parse(split[0]), int.Parse(split[1])); + CurveType curveType = CurveType.Catmull; double length = 0; - var points = new List { new Vector2(int.Parse(split[0]), int.Parse(split[1])) }; + var points = new List { Vector2.Zero }; string[] pointsplit = split[5].Split('|'); foreach (string t in pointsplit) @@ -69,9 +72,14 @@ namespace osu.Game.Rulesets.Objects.Legacy } string[] temp = t.Split(':'); - points.Add(new Vector2((int)Convert.ToDouble(temp[0], CultureInfo.InvariantCulture), (int)Convert.ToDouble(temp[1], CultureInfo.InvariantCulture))); + points.Add(new Vector2((int)Convert.ToDouble(temp[0], CultureInfo.InvariantCulture), (int)Convert.ToDouble(temp[1], CultureInfo.InvariantCulture)) - pos); } + // osu-stable special-cased colinear perfect curves to a CurveType.Linear + bool isLinear(List p) => Precision.AlmostEquals(0, (p[1].Y - p[0].Y) * (p[2].X - p[0].X) - (p[1].X - p[0].X) * (p[2].Y - p[0].Y)); + if (points.Count == 3 && curveType == CurveType.PerfectCurve && isLinear(points)) + curveType = CurveType.Linear; + int repeatCount = Convert.ToInt32(split[6], CultureInfo.InvariantCulture); if (repeatCount > 9000) @@ -134,7 +142,7 @@ namespace osu.Game.Rulesets.Objects.Legacy for (int i = 0; i < nodes; i++) nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i])); - result = CreateSlider(new Vector2(int.Parse(split[0]), int.Parse(split[1])), combo, points, length, curveType, repeatCount, nodeSamples); + result = CreateSlider(pos, combo, points, length, curveType, repeatCount, nodeSamples); } else if ((type & ConvertHitObjectType.Spinner) > 0) { @@ -180,8 +188,8 @@ namespace osu.Game.Rulesets.Objects.Legacy string[] split = str.Split(':'); - var bank = (LegacyDecoder.LegacySampleBank)Convert.ToInt32(split[0]); - var addbank = (LegacyDecoder.LegacySampleBank)Convert.ToInt32(split[1]); + var bank = (LegacyBeatmapDecoder.LegacySampleBank)Convert.ToInt32(split[0]); + var addbank = (LegacyBeatmapDecoder.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/Objects/Legacy/Osu/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs index 19f9a93976..d2a0530dd9 100644 --- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using OpenTK; using osu.Game.Rulesets.Objects.Types; using System.Collections.Generic; @@ -29,7 +30,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu Position = position, NewCombo = newCombo, ControlPoints = controlPoints, - Distance = length, + Distance = Math.Max(0, length), CurveType = curveType, RepeatSamples = repeatSamples, RepeatCount = repeatCount diff --git a/osu.Game/Rulesets/Objects/Types/IHasComboIndex.cs b/osu.Game/Rulesets/Objects/Types/IHasComboIndex.cs new file mode 100644 index 0000000000..68474a6e2c --- /dev/null +++ b/osu.Game/Rulesets/Objects/Types/IHasComboIndex.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Rulesets.Objects.Types +{ + /// + /// A HitObject that is part of a combo and has extended information about its position relative to other combo objects. + /// + public interface IHasComboIndex : IHasCombo + { + /// + /// The offset of this hitobject in the current combo. + /// + int IndexInCurrentCombo { get; set; } + + /// + /// The offset of this hitobject in the current combo. + /// + int ComboIndex { get; set; } + + /// + /// Whether this is the last object in the current combo. + /// + bool LastInCombo { get; set; } + } +} diff --git a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs new file mode 100644 index 0000000000..1d4f4e0f90 --- /dev/null +++ b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs @@ -0,0 +1,26 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Rulesets.Objects.Types +{ + /// + /// A HitObject that is part of a combo and has extended information about its position relative to other combo objects. + /// + public interface IHasComboInformation : IHasCombo + { + /// + /// The offset of this hitobject in the current combo. + /// + int IndexInCurrentCombo { get; set; } + + /// + /// The offset of this combo in relation to the beatmap. + /// + int ComboIndex { get; set; } + + /// + /// Whether this is the last object in the current combo. + /// + bool LastInCombo { get; set; } + } +} diff --git a/osu.Game/Rulesets/Objects/Types/IHasCurve.cs b/osu.Game/Rulesets/Objects/Types/IHasCurve.cs index 7f03854ea9..251ad3e3cd 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasCurve.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasCurve.cs @@ -30,21 +30,19 @@ namespace osu.Game.Rulesets.Objects.Types public static class HasCurveExtensions { /// - /// Computes the position on the curve at a given progress, accounting for repeat logic. - /// - /// Ranges from [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. - /// + /// Computes the position on the curve relative to how much of the has been completed. /// /// The curve. - /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. - public static Vector2 PositionAt(this IHasCurve obj, double progress) + /// [0, 1] where 0 is the start time of the and 1 is the end time of the . + /// The position on the curve. + public static Vector2 CurvePositionAt(this IHasCurve obj, double progress) => obj.Curve.PositionAt(obj.ProgressAt(progress)); /// - /// Finds the progress along the curve, accounting for repeat logic. + /// Computes the progress along the curve relative to how much of the has been completed. /// /// The curve. - /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. + /// [0, 1] where 0 is the start time of the and 1 is the end time of the . /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve. public static double ProgressAt(this IHasCurve obj, double progress) { diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index c245407bbf..5ffd67423e 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using osu.Framework.Input; -using osu.Framework.MathUtils; using osu.Game.Input.Handlers; using OpenTK; using OpenTK.Input; @@ -17,14 +16,15 @@ namespace osu.Game.Rulesets.Replays /// The ReplayHandler will take a replay and handle the propagation of updates to the input stack. /// It handles logic of any frames which *must* be executed. /// - public abstract class FramedReplayInputHandler : ReplayInputHandler + public abstract class FramedReplayInputHandler : ReplayInputHandler + where TFrame : ReplayFrame { private readonly Replay replay; protected List Frames => replay.Frames; - public ReplayFrame CurrentFrame => !hasFrames ? null : Frames[currentFrameIndex]; - public ReplayFrame NextFrame => !hasFrames ? null : Frames[nextFrameIndex]; + public TFrame CurrentFrame => !HasFrames ? null : (TFrame)Frames[currentFrameIndex]; + public TFrame NextFrame => !HasFrames ? null : (TFrame)Frames[nextFrameIndex]; private int currentFrameIndex; @@ -46,31 +46,14 @@ namespace osu.Game.Rulesets.Replays return true; } - public void SetPosition(Vector2 pos) - { - } - - protected Vector2? Position - { - get - { - if (!hasFrames) - return null; - - return Interpolation.ValueAt(currentTime, CurrentFrame.Position, NextFrame.Position, CurrentFrame.Time, NextFrame.Time); - } - } - public override List GetPendingStates() => new List(); public bool AtLastFrame => currentFrameIndex == Frames.Count - 1; public bool AtFirstFrame => currentFrameIndex == 0; - public Vector2 Size => new Vector2(512, 384); - private const double sixty_frame_time = 1000.0 / 60; - private double currentTime; + protected double CurrentTime { get; private set; } private int currentDirection; /// @@ -79,14 +62,16 @@ namespace osu.Game.Rulesets.Replays /// public bool FrameAccuratePlayback = true; - private bool hasFrames => Frames.Count > 0; + protected bool HasFrames => Frames.Count > 0; private bool inImportantSection => - FrameAccuratePlayback && + HasFrames && FrameAccuratePlayback && //a button is in a pressed state - ((currentDirection > 0 ? CurrentFrame : NextFrame)?.IsImportant ?? false) && + IsImportant(currentDirection > 0 ? CurrentFrame : NextFrame) && //the next frame is within an allowable time span - Math.Abs(currentTime - NextFrame?.Time ?? 0) <= sixty_frame_time * 1.2; + Math.Abs(CurrentTime - NextFrame?.Time ?? 0) <= sixty_frame_time * 1.2; + + protected virtual bool IsImportant(TFrame frame) => false; /// /// Update the current frame based on an incoming time value. @@ -97,10 +82,10 @@ namespace osu.Game.Rulesets.Replays /// The usable time value. If null, we should not advance time as we do not have enough data. public override double? SetFrameFromTime(double time) { - currentDirection = time.CompareTo(currentTime); + currentDirection = time.CompareTo(CurrentTime); if (currentDirection == 0) currentDirection = 1; - if (hasFrames) + if (HasFrames) { // check if the next frame is in the "future" for the current playback direction if (currentDirection != time.CompareTo(NextFrame.Time)) @@ -114,12 +99,12 @@ namespace osu.Game.Rulesets.Replays // If going backwards, we need to execute once _before_ the frame time to reverse any judgements // that would occur as a result of this frame in forward playback if (currentDirection == -1) - return currentTime = CurrentFrame.Time - 1; - return currentTime = CurrentFrame.Time; + return CurrentTime = CurrentFrame.Time - 1; + return CurrentTime = CurrentFrame.Time; } } - return currentTime = time; + return CurrentTime = time; } protected class ReplayMouseState : MouseState diff --git a/osu.Game/Rulesets/Replays/Legacy/LegacyReplayFrame.cs b/osu.Game/Rulesets/Replays/Legacy/LegacyReplayFrame.cs new file mode 100644 index 0000000000..945cb95e79 --- /dev/null +++ b/osu.Game/Rulesets/Replays/Legacy/LegacyReplayFrame.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using OpenTK; + +namespace osu.Game.Rulesets.Replays.Legacy +{ + public class LegacyReplayFrame : ReplayFrame + { + public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0); + + public float? MouseX; + public float? MouseY; + + public bool MouseLeft => MouseLeft1 || MouseLeft2; + public bool MouseRight => MouseRight1 || MouseRight2; + + public bool MouseLeft1 => (ButtonState & ReplayButtonState.Left1) > 0; + public bool MouseRight1 => (ButtonState & ReplayButtonState.Right1) > 0; + public bool MouseLeft2 => (ButtonState & ReplayButtonState.Left2) > 0; + public bool MouseRight2 => (ButtonState & ReplayButtonState.Right2) > 0; + + public ReplayButtonState ButtonState; + + public LegacyReplayFrame(double time, float? mouseX, float? mouseY, ReplayButtonState buttonState) + : base(time) + { + MouseX = mouseX; + MouseY = mouseY; + ButtonState = buttonState; + } + + public override string ToString() + { + return $"{Time}\t({MouseX},{MouseY})\t{MouseLeft}\t{MouseRight}\t{MouseLeft1}\t{MouseRight1}\t{MouseLeft2}\t{MouseRight2}\t{ButtonState}"; + } + } +} diff --git a/osu.Game/Rulesets/Replays/ReplayButtonState.cs b/osu.Game/Rulesets/Replays/Legacy/ReplayButtonState.cs similarity index 85% rename from osu.Game/Rulesets/Replays/ReplayButtonState.cs rename to osu.Game/Rulesets/Replays/Legacy/ReplayButtonState.cs index 4421a79af8..d0706411d2 100644 --- a/osu.Game/Rulesets/Replays/ReplayButtonState.cs +++ b/osu.Game/Rulesets/Replays/Legacy/ReplayButtonState.cs @@ -3,7 +3,7 @@ using System; -namespace osu.Game.Rulesets.Replays +namespace osu.Game.Rulesets.Replays.Legacy { [Flags] public enum ReplayButtonState diff --git a/osu.Game/Rulesets/Replays/Replay.cs b/osu.Game/Rulesets/Replays/Replay.cs index 27a77addba..a0ea2c5655 100644 --- a/osu.Game/Rulesets/Replays/Replay.cs +++ b/osu.Game/Rulesets/Replays/Replay.cs @@ -9,7 +9,6 @@ namespace osu.Game.Rulesets.Replays public class Replay { public User User; - public List Frames = new List(); } } diff --git a/osu.Game/Rulesets/Replays/ReplayFrame.cs b/osu.Game/Rulesets/Replays/ReplayFrame.cs index 4f8ed5163e..61a3646024 100644 --- a/osu.Game/Rulesets/Replays/ReplayFrame.cs +++ b/osu.Game/Rulesets/Replays/ReplayFrame.cs @@ -1,70 +1,19 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK; - namespace osu.Game.Rulesets.Replays { public class ReplayFrame { - public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0); - - public virtual bool IsImportant => MouseX.HasValue && MouseY.HasValue && (MouseLeft || MouseRight); - - public float? MouseX; - public float? MouseY; - - public bool MouseLeft => MouseLeft1 || MouseLeft2; - public bool MouseRight => MouseRight1 || MouseRight2; - - public bool MouseLeft1 - { - get { return (ButtonState & ReplayButtonState.Left1) > 0; } - set { setButtonState(ReplayButtonState.Left1, value); } - } - public bool MouseRight1 - { - get { return (ButtonState & ReplayButtonState.Right1) > 0; } - set { setButtonState(ReplayButtonState.Right1, value); } - } - public bool MouseLeft2 - { - get { return (ButtonState & ReplayButtonState.Left2) > 0; } - set { setButtonState(ReplayButtonState.Left2, value); } - } - public bool MouseRight2 - { - get { return (ButtonState & ReplayButtonState.Right2) > 0; } - set { setButtonState(ReplayButtonState.Right2, value); } - } - - private void setButtonState(ReplayButtonState singleButton, bool pressed) - { - if (pressed) - ButtonState |= singleButton; - else - ButtonState &= ~singleButton; - } - public double Time; - public ReplayButtonState ButtonState; - - protected ReplayFrame() + public ReplayFrame() { } - public ReplayFrame(double time, float? mouseX, float? mouseY, ReplayButtonState buttonState) + public ReplayFrame(double time) { - MouseX = mouseX; - MouseY = mouseY; - ButtonState = buttonState; Time = time; } - - public override string ToString() - { - return $"{Time}\t({MouseX},{MouseY})\t{MouseLeft}\t{MouseRight}\t{MouseLeft1}\t{MouseRight1}\t{MouseLeft2}\t{MouseRight2}\t{ButtonState}"; - } } } diff --git a/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs b/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs new file mode 100644 index 0000000000..ac1e5e29ec --- /dev/null +++ b/osu.Game/Rulesets/Replays/Types/IConvertibleReplayFrame.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Replays.Legacy; + +namespace osu.Game.Rulesets.Replays.Types +{ + /// + /// A type of which can be converted from a . + /// + public interface IConvertibleReplayFrame + { + /// + /// Populates this using values from a . + /// + /// The to extract values from. + /// The score. + /// The beatmap. + void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap); + } +} diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 4f256621fb..cba849a491 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -11,6 +11,7 @@ using osu.Game.Graphics; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -63,7 +64,7 @@ namespace osu.Game.Rulesets /// /// Do not override this unless you are a legacy mode. /// - public virtual int LegacyID => -1; + public virtual int? LegacyID => null; /// /// A unique short name to reference this ruleset in online requests. @@ -89,6 +90,13 @@ namespace osu.Game.Rulesets /// A descriptive name of the variant. public virtual string GetVariantName(int variant) => string.Empty; + /// + /// For rulesets which support legacy (osu-stable) replay conversion, this method will create an empty replay frame + /// for conversion use. + /// + /// An empty frame for the current ruleset, or null if unsupported. + public virtual IConvertibleReplayFrame CreateConvertibleReplayFrame() => null; + /// /// Create a ruleset info based on this ruleset. /// diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 01e3b6848f..e621c3cf2b 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets loadRulesetFromFile(file); } - public RulesetStore(Func factory) + public RulesetStore(IDatabaseContextFactory factory) : base(factory) { AddMissingRulesets(); @@ -56,47 +56,50 @@ namespace osu.Game.Rulesets protected void AddMissingRulesets() { - var context = GetContext(); - - var instances = loaded_assemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r, (RulesetInfo)null)).ToList(); - - //add all legacy modes in correct order - foreach (var r in instances.Where(r => r.LegacyID >= 0).OrderBy(r => r.LegacyID)) + using (var usage = ContextFactory.GetForWrite()) { - if (context.RulesetInfo.SingleOrDefault(rsi => rsi.ID == r.RulesetInfo.ID) == null) - context.RulesetInfo.Add(r.RulesetInfo); - } + var context = usage.Context; - context.SaveChanges(); + var instances = loaded_assemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r, (RulesetInfo)null)).ToList(); - //add any other modes - foreach (var r in instances.Where(r => r.LegacyID < 0)) - if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo == r.RulesetInfo.InstantiationInfo) == null) - context.RulesetInfo.Add(r.RulesetInfo); - - context.SaveChanges(); - - //perform a consistency check - foreach (var r in context.RulesetInfo) - { - try + //add all legacy modes in correct order + foreach (var r in instances.Where(r => r.LegacyID != null).OrderBy(r => r.LegacyID)) { - var instance = r.CreateInstance(); - - r.Name = instance.Description; - r.ShortName = instance.ShortName; - - r.Available = true; + if (context.RulesetInfo.SingleOrDefault(rsi => rsi.ID == r.RulesetInfo.ID) == null) + context.RulesetInfo.Add(r.RulesetInfo); } - catch + + context.SaveChanges(); + + //add any other modes + foreach (var r in instances.Where(r => r.LegacyID == null)) + if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo == r.RulesetInfo.InstantiationInfo) == null) + context.RulesetInfo.Add(r.RulesetInfo); + + context.SaveChanges(); + + //perform a consistency check + foreach (var r in context.RulesetInfo) { - r.Available = false; + try + { + var instance = r.CreateInstance(); + + r.Name = instance.Description; + r.ShortName = instance.ShortName; + + r.Available = true; + } + catch + { + r.Available = false; + } } + + context.SaveChanges(); + + AvailableRulesets = context.RulesetInfo.Where(r => r.Available).ToList(); } - - context.SaveChanges(); - - AvailableRulesets = context.RulesetInfo.Where(r => r.Available).ToList(); } private static void loadRulesetFromFile(string file) diff --git a/osu.Game/Rulesets/Scoring/Legacy/LegacyScoreParser.cs b/osu.Game/Rulesets/Scoring/Legacy/LegacyScoreParser.cs new file mode 100644 index 0000000000..9ebb62a368 --- /dev/null +++ b/osu.Game/Rulesets/Scoring/Legacy/LegacyScoreParser.cs @@ -0,0 +1,152 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.IO; +using osu.Game.Beatmaps; +using osu.Game.IO.Legacy; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Replays.Legacy; +using osu.Game.Users; +using SharpCompress.Compressors.LZMA; + +namespace osu.Game.Rulesets.Scoring.Legacy +{ + public class LegacyScoreParser + { + private readonly RulesetStore rulesets; + private readonly BeatmapManager beatmaps; + + public LegacyScoreParser(RulesetStore rulesets, BeatmapManager beatmaps) + { + this.rulesets = rulesets; + this.beatmaps = beatmaps; + } + + private Beatmap currentBeatmap; + private Ruleset currentRuleset; + + public Score Parse(Stream stream) + { + Score score; + + using (SerializationReader sr = new SerializationReader(stream)) + { + score = new Score { Ruleset = rulesets.GetRuleset(sr.ReadByte()) }; + currentRuleset = score.Ruleset.CreateInstance(); + + /* score.Pass = true;*/ + var version = sr.ReadInt32(); + + /* score.FileChecksum = */ + var beatmapHash = sr.ReadString(); + score.Beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == beatmapHash); + currentBeatmap = beatmaps.GetWorkingBeatmap(score.Beatmap).Beatmap; + + /* score.PlayerName = */ + score.User = new User { Username = sr.ReadString() }; + /* var localScoreChecksum = */ + sr.ReadString(); + /* score.Count300 = */ + sr.ReadUInt16(); + /* score.Count100 = */ + sr.ReadUInt16(); + /* score.Count50 = */ + sr.ReadUInt16(); + /* score.CountGeki = */ + sr.ReadUInt16(); + /* score.CountKatu = */ + sr.ReadUInt16(); + /* score.CountMiss = */ + sr.ReadUInt16(); + score.TotalScore = sr.ReadInt32(); + score.MaxCombo = sr.ReadUInt16(); + /* score.Perfect = */ + sr.ReadBoolean(); + /* score.EnabledMods = (Mods)*/ + sr.ReadInt32(); + /* score.HpGraphString = */ + sr.ReadString(); + /* score.Date = */ + sr.ReadDateTime(); + + var compressedReplay = sr.ReadByteArray(); + + if (version >= 20140721) + /*OnlineId =*/ + sr.ReadInt64(); + else if (version >= 20121008) + /*OnlineId =*/ + sr.ReadInt32(); + + using (var replayInStream = new MemoryStream(compressedReplay)) + { + byte[] properties = new byte[5]; + if (replayInStream.Read(properties, 0, 5) != 5) + throw new IOException("input .lzma is too short"); + long outSize = 0; + for (int i = 0; i < 8; i++) + { + int v = replayInStream.ReadByte(); + if (v < 0) + throw new IOException("Can't Read 1"); + outSize |= (long)(byte)v << (8 * i); + } + + long compressedSize = replayInStream.Length - replayInStream.Position; + + using (var lzma = new LzmaStream(properties, replayInStream, compressedSize, outSize)) + using (var reader = new StreamReader(lzma)) + { + score.Replay = new Replay { User = score.User }; + readLegacyReplay(score.Replay, reader); + } + } + } + + return score; + } + + private void readLegacyReplay(Replay replay, StreamReader reader) + { + float lastTime = 0; + + foreach (var l in reader.ReadToEnd().Split(',')) + { + var split = l.Split('|'); + + if (split.Length < 4) + continue; + + if (split[0] == "-12345") + { + // Todo: The seed is provided in split[3], which we'll need to use at some point + continue; + } + + var diff = float.Parse(split[0]); + lastTime += diff; + + // Todo: At some point we probably want to rewind and play back the negative-time frames + // but for now we'll achieve equal playback to stable by skipping negative frames + if (diff < 0) + continue; + + replay.Frames.Add(convertFrame(new LegacyReplayFrame(lastTime, float.Parse(split[1]), float.Parse(split[2]), (ReplayButtonState)int.Parse(split[3])))); + } + } + + private ReplayFrame convertFrame(LegacyReplayFrame legacyFrame) + { + var convertible = currentRuleset.CreateConvertibleReplayFrame(); + if (convertible == null) + throw new InvalidOperationException($"Legacy replay cannot be converted for the ruleset: {currentRuleset.Description}"); + convertible.ConvertFrom(legacyFrame, currentBeatmap); + + var frame = (ReplayFrame)convertible; + frame.Time = legacyFrame.Time; + + return frame; + } + } +} diff --git a/osu.Game/Rulesets/Scoring/PerformanceCalculator.cs b/osu.Game/Rulesets/Scoring/PerformanceCalculator.cs index ba16d78b37..c047a421fd 100644 --- a/osu.Game/Rulesets/Scoring/PerformanceCalculator.cs +++ b/osu.Game/Rulesets/Scoring/PerformanceCalculator.cs @@ -2,7 +2,9 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Scoring @@ -23,9 +25,15 @@ namespace osu.Game.Rulesets.Scoring protected PerformanceCalculator(Ruleset ruleset, Beatmap beatmap, Score score) { - Beatmap = CreateBeatmapConverter().Convert(beatmap); Score = score; + var converter = CreateBeatmapConverter(); + + foreach (var mod in score.Mods.OfType>()) + mod.ApplyToBeatmapConverter(converter); + + Beatmap = converter.Convert(beatmap); + var diffCalc = ruleset.CreateDifficultyCalculator(beatmap, score.Mods); diffCalc.Calculate(attributes); } diff --git a/osu.Game/Rulesets/Scoring/ScoreStore.cs b/osu.Game/Rulesets/Scoring/ScoreStore.cs index d21ca79736..cb2b76cdcf 100644 --- a/osu.Game/Rulesets/Scoring/ScoreStore.cs +++ b/osu.Game/Rulesets/Scoring/ScoreStore.cs @@ -2,20 +2,16 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.Collections.Generic; using System.IO; using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.IO.Legacy; using osu.Game.IPC; -using osu.Game.Rulesets.Replays; -using osu.Game.Users; -using SharpCompress.Compressors.LZMA; +using osu.Game.Rulesets.Scoring.Legacy; namespace osu.Game.Rulesets.Scoring { - public class ScoreStore : DatabaseBackedStore + public class ScoreStore : DatabaseBackedStore, ICanAcceptFiles { private readonly Storage storage; @@ -24,10 +20,12 @@ namespace osu.Game.Rulesets.Scoring private const string replay_folder = @"replays"; + public event Action ScoreImported; + // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) private ScoreIPCChannel ipc; - public ScoreStore(Storage storage, Func factory, IIpcHost importHost = null, BeatmapManager beatmaps = null, RulesetStore rulesets = null) : base(factory) + public ScoreStore(Storage storage, DatabaseContextFactory factory, IIpcHost importHost = null, BeatmapManager beatmaps = null, RulesetStore rulesets = null) : base(factory) { this.storage = storage; this.beatmaps = beatmaps; @@ -37,128 +35,22 @@ namespace osu.Game.Rulesets.Scoring ipc = new ScoreIPCChannel(importHost, this); } - public Score ReadReplayFile(string replayFilename) + public string[] HandledExtensions => new[] { ".osr" }; + + public void Import(params string[] paths) { - Score score; - - using (Stream s = storage.GetStream(Path.Combine(replay_folder, replayFilename))) - using (SerializationReader sr = new SerializationReader(s)) + foreach (var path in paths) { - score = new Score - { - Ruleset = rulesets.GetRuleset(sr.ReadByte()) - }; - - /* score.Pass = true;*/ - var version = sr.ReadInt32(); - /* score.FileChecksum = */ - var beatmapHash = sr.ReadString(); - score.Beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == beatmapHash); - /* score.PlayerName = */ - score.User = new User { Username = sr.ReadString() }; - /* var localScoreChecksum = */ - sr.ReadString(); - /* score.Count300 = */ - sr.ReadUInt16(); - /* score.Count100 = */ - sr.ReadUInt16(); - /* score.Count50 = */ - sr.ReadUInt16(); - /* score.CountGeki = */ - sr.ReadUInt16(); - /* score.CountKatu = */ - sr.ReadUInt16(); - /* score.CountMiss = */ - sr.ReadUInt16(); - score.TotalScore = sr.ReadInt32(); - score.MaxCombo = sr.ReadUInt16(); - /* score.Perfect = */ - sr.ReadBoolean(); - /* score.EnabledMods = (Mods)*/ - sr.ReadInt32(); - /* score.HpGraphString = */ - sr.ReadString(); - /* score.Date = */ - sr.ReadDateTime(); - - var compressedReplay = sr.ReadByteArray(); - - if (version >= 20140721) - /*OnlineId =*/ - sr.ReadInt64(); - else if (version >= 20121008) - /*OnlineId =*/ - sr.ReadInt32(); - - using (var replayInStream = new MemoryStream(compressedReplay)) - { - byte[] properties = new byte[5]; - if (replayInStream.Read(properties, 0, 5) != 5) - throw new IOException("input .lzma is too short"); - long outSize = 0; - for (int i = 0; i < 8; i++) - { - int v = replayInStream.ReadByte(); - if (v < 0) - throw new IOException("Can't Read 1"); - outSize |= (long)(byte)v << (8 * i); - } - - long compressedSize = replayInStream.Length - replayInStream.Position; - - using (var lzma = new LzmaStream(properties, replayInStream, compressedSize, outSize)) - using (var reader = new StreamReader(lzma)) - { - score.Replay = createLegacyReplay(reader); - score.Replay.User = score.User; - } - } + var score = ReadReplayFile(path); + if (score != null) + ScoreImported?.Invoke(score); } - - return score; } - /// - /// Creates a legacy replay which is read from a stream. - /// - /// The stream reader. - /// The legacy replay. - private Replay createLegacyReplay(StreamReader reader) + public Score ReadReplayFile(string replayFilename) { - var frames = new List(); - - float lastTime = 0; - - foreach (var l in reader.ReadToEnd().Split(',')) - { - var split = l.Split('|'); - - if (split.Length < 4) - continue; - - if (split[0] == "-12345") - { - // Todo: The seed is provided in split[3], which we'll need to use at some point - continue; - } - - var diff = float.Parse(split[0]); - lastTime += diff; - - // Todo: At some point we probably want to rewind and play back the negative-time frames - // but for now we'll achieve equal playback to stable by skipping negative frames - if (diff < 0) - continue; - - frames.Add(new ReplayFrame( - lastTime, - float.Parse(split[1]), - float.Parse(split[2]), - (ReplayButtonState)int.Parse(split[3]) - )); - } - - return new Replay { Frames = frames }; + using (Stream s = storage.GetStream(Path.Combine(replay_folder, replayFilename))) + return new LegacyScoreParser(rulesets, beatmaps).Parse(s); } } } diff --git a/osu.Game/Rulesets/UI/JudgementContainer.cs b/osu.Game/Rulesets/UI/JudgementContainer.cs new file mode 100644 index 0000000000..1291b9fc98 --- /dev/null +++ b/osu.Game/Rulesets/UI/JudgementContainer.cs @@ -0,0 +1,24 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Judgements; + +namespace osu.Game.Rulesets.UI +{ + public class JudgementContainer : Container + where T : DrawableJudgement + { + public override void Add(T judgement) + { + if (judgement == null) throw new ArgumentNullException(nameof(judgement)); + + // remove any existing judgements for the judged object. + // this can be the case when rewinding. + RemoveAll(c => c.JudgedObject == judgement.JudgedObject); + + base.Add(judgement); + } + } +} diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index a7fed7059b..bbf20c2c26 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -3,52 +3,37 @@ using System.Collections.Generic; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; -using OpenTK; using osu.Framework.Allocation; namespace osu.Game.Rulesets.UI { - public abstract class Playfield : Container + public abstract class Playfield : ScalableContainer { /// /// The HitObjects contained in this Playfield. /// public HitObjectContainer HitObjects { get; private set; } - public Container ScaledContent; - - protected override Container Content => content; - private readonly Container content; - - private List nestedPlayfields; - /// /// All the s nested inside this playfield. /// public IReadOnlyList NestedPlayfields => nestedPlayfields; + private List nestedPlayfields; /// /// A container for keeping track of DrawableHitObjects. /// - /// Whether we want our internal coordinate system to be scaled to a specified width. - protected Playfield(float? customWidth = null) + /// The width to scale the internal coordinate space to. + /// May be null if scaling based on is desired. If is also null, no scaling will occur. + /// + /// The height to scale the internal coordinate space to. + /// May be null if scaling based on is desired. If is also null, no scaling will occur. + /// + protected Playfield(float? customWidth = null, float? customHeight = null) + : base(customWidth, customHeight) { RelativeSizeAxes = Axes.Both; - - AddInternal(ScaledContent = new ScaledContainer - { - CustomWidth = customWidth, - RelativeSizeAxes = Axes.Both, - Children = new[] - { - content = new Container - { - RelativeSizeAxes = Axes.Both, - } - } - }); } [BackgroundDependencyLoader] @@ -94,22 +79,5 @@ namespace osu.Game.Rulesets.UI /// Creates the container that will be used to contain the s. /// protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer(); - - private class ScaledContainer : Container - { - /// - /// A value (in game pixels that we should scale our content to match). - /// - public float? CustomWidth; - - //dividing by the customwidth will effectively scale our content to the required container size. - protected override Vector2 DrawScale => CustomWidth.HasValue ? new Vector2(DrawSize.X / CustomWidth.Value) : base.DrawScale; - - protected override void Update() - { - base.Update(); - RelativeChildSize = new Vector2(DrawScale.X, RelativeChildSize.Y); - } - } } } diff --git a/osu.Game/Rulesets/UI/RulesetContainer.cs b/osu.Game/Rulesets/UI/RulesetContainer.cs index 0f88b262f6..2201b6963f 100644 --- a/osu.Game/Rulesets/UI/RulesetContainer.cs +++ b/osu.Game/Rulesets/UI/RulesetContainer.cs @@ -17,6 +17,7 @@ using osu.Framework.Configuration; using osu.Framework.Graphics.Cursor; using osu.Framework.Input; using osu.Game.Configuration; +using osu.Game.Input.Handlers; using osu.Game.Overlays; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Replays; @@ -33,11 +34,6 @@ namespace osu.Game.Rulesets.UI /// public abstract class RulesetContainer : Container { - /// - /// Whether to apply adjustments to the child based on our own size. - /// - public bool AspectAdjust = true; - /// /// The selected variant. /// @@ -115,7 +111,7 @@ namespace osu.Game.Rulesets.UI /// The input manager. public abstract PassThroughInputManager CreateInputManager(); - protected virtual FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => null; + protected virtual ReplayInputHandler CreateReplayInputHandler(Replay replay) => null; public Replay Replay { get; private set; } @@ -325,7 +321,7 @@ namespace osu.Game.Rulesets.UI { base.Update(); - Playfield.Size = AspectAdjust ? GetPlayfieldAspectAdjust() : Vector2.One; + Playfield.Size = GetAspectAdjustedSize() * PlayfieldArea; } /// @@ -336,10 +332,17 @@ namespace osu.Game.Rulesets.UI protected virtual BeatmapProcessor CreateBeatmapProcessor() => new BeatmapProcessor(); /// - /// In some cases we want to apply changes to the relative size of our contained based on custom conditions. + /// Computes the size of the in relative coordinate space after aspect adjustments. /// - /// - protected virtual Vector2 GetPlayfieldAspectAdjust() => new Vector2(0.75f); //a sane default + /// The aspect-adjusted size. + protected virtual Vector2 GetAspectAdjustedSize() => Vector2.One; + + /// + /// The area of this that is available for the to use. + /// Must be specified in relative coordinate space to this . + /// This affects the final size of the but does not affect the 's scale. + /// + protected virtual Vector2 PlayfieldArea => new Vector2(0.75f); // A sane default /// /// Creates a converter to convert Beatmap to a specific mode. diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index f465d0e202..3f8a17e23d 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -91,8 +91,6 @@ namespace osu.Game.Rulesets.UI #region Clock control - protected override bool ShouldProcessClock => false; // We handle processing the clock ourselves - private ManualClock clock; private IFrameBasedClock parentClock; @@ -103,6 +101,7 @@ namespace osu.Game.Rulesets.UI //our clock will now be our parent's clock, but we want to replace this to allow manual control. parentClock = Clock; + ProcessCustomClock = false; Clock = new FramedClock(clock = new ManualClock { CurrentTime = parentClock.CurrentTime, diff --git a/osu.Game/Rulesets/UI/ScalableContainer.cs b/osu.Game/Rulesets/UI/ScalableContainer.cs new file mode 100644 index 0000000000..9762828e7d --- /dev/null +++ b/osu.Game/Rulesets/UI/ScalableContainer.cs @@ -0,0 +1,86 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using OpenTK; + +namespace osu.Game.Rulesets.UI +{ + /// + /// A which can have its internal coordinate system scaled to a specific size. + /// + public class ScalableContainer : Container + { + /// + /// The scaled content. + /// + public readonly Container ScaledContent; + + protected override Container Content => content; + private readonly Container content; + + /// + /// A which can have its internal coordinate system scaled to a specific size. + /// + /// The width to scale the internal coordinate space to. + /// May be null if scaling based on is desired. If is also null, no scaling will occur. + /// + /// The height to scale the internal coordinate space to. + /// May be null if scaling based on is desired. If is also null, no scaling will occur. + /// + public ScalableContainer(float? customWidth = null, float? customHeight = null) + { + AddInternal(ScaledContent = new ScaledContainer + { + CustomWidth = customWidth, + CustomHeight = customHeight, + RelativeSizeAxes = Axes.Both, + Child = content = new Container { RelativeSizeAxes = Axes.Both } + }); + } + + private class ScaledContainer : Container + { + /// + /// The value to scale the width of the content to match. + /// If null, is used. + /// + public float? CustomWidth; + + /// + /// The value to scale the height of the content to match. + /// if null, is used. + /// + public float? CustomHeight; + + /// + /// The scale that is required for the size of the content to match and . + /// + private Vector2 sizeScale + { + get + { + if (CustomWidth.HasValue && CustomHeight.HasValue) + return Vector2.Divide(DrawSize, new Vector2(CustomWidth.Value, CustomHeight.Value)); + if (CustomWidth.HasValue) + return new Vector2(DrawSize.X / CustomWidth.Value); + if (CustomHeight.HasValue) + return new Vector2(DrawSize.Y / CustomHeight.Value); + return Vector2.One; + } + } + + /// + /// Scale the content to the required container size by multiplying by . + /// + protected override Vector2 DrawScale => sizeScale * base.DrawScale; + + protected override void Update() + { + base.Update(); + RelativeChildSize = new Vector2(CustomWidth.HasValue ? sizeScale.X : RelativeChildSize.X, CustomHeight.HasValue ? sizeScale.Y : RelativeChildSize.Y); + } + } + } +} diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index e168f6daec..1c1c8f7f61 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -62,9 +62,14 @@ namespace osu.Game.Rulesets.UI.Scrolling /// Creates a new . /// /// The direction in which s in this container should scroll. - /// Whether we want our internal coordinate system to be scaled to a specified width - protected ScrollingPlayfield(ScrollingDirection direction, float? customWidth = null) - : base(customWidth) + /// The width to scale the internal coordinate space to. + /// May be null if scaling based on is desired. If is also null, no scaling will occur. + /// + /// The height to scale the internal coordinate space to. + /// May be null if scaling based on is desired. If is also null, no scaling will occur. + /// + protected ScrollingPlayfield(ScrollingDirection direction, float? customWidth = null, float? customHeight = null) + : base(customWidth, customHeight) { this.direction = direction; } diff --git a/osu.Game/Rulesets/UI/Scrolling/Visualisers/OverlappingSpeedChangeVisualiser.cs b/osu.Game/Rulesets/UI/Scrolling/Visualisers/OverlappingSpeedChangeVisualiser.cs index 4cce90ee94..48c212efa7 100644 --- a/osu.Game/Rulesets/UI/Scrolling/Visualisers/OverlappingSpeedChangeVisualiser.cs +++ b/osu.Game/Rulesets/UI/Scrolling/Visualisers/OverlappingSpeedChangeVisualiser.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Visualisers var controlPoint = controlPointAt(obj.HitObject.StartTime); obj.LifetimeStart = obj.HitObject.StartTime - timeRange / controlPoint.Multiplier; - if (obj.NestedHitObjects != null) + if (obj.HasNestedHitObjects) { ComputeInitialStates(obj.NestedHitObjects, direction, timeRange, length); ComputePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length); diff --git a/osu.Game/Rulesets/UI/Scrolling/Visualisers/SequentialSpeedChangeVisualiser.cs b/osu.Game/Rulesets/UI/Scrolling/Visualisers/SequentialSpeedChangeVisualiser.cs index 94705426f8..1b7c3714d6 100644 --- a/osu.Game/Rulesets/UI/Scrolling/Visualisers/SequentialSpeedChangeVisualiser.cs +++ b/osu.Game/Rulesets/UI/Scrolling/Visualisers/SequentialSpeedChangeVisualiser.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Visualisers } } - if (obj.NestedHitObjects != null) + if (obj.HasNestedHitObjects) { ComputeInitialStates(obj.NestedHitObjects, direction, timeRange, length); ComputePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length); diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs index 05e47ef5b1..fe2549d300 100644 --- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs +++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; +using osu.Framework.Timing; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -17,10 +18,15 @@ namespace osu.Game.Screens.Edit.Components { public class PlaybackControl : BottomBarContainer { - private readonly IconButton playButton; + private IconButton playButton; - public PlaybackControl() + private IAdjustableClock adjustableClock; + + [BackgroundDependencyLoader] + private void load(IAdjustableClock adjustableClock) { + this.adjustableClock = adjustableClock; + PlaybackTabControl tabs; Children = new Drawable[] @@ -54,22 +60,22 @@ namespace osu.Game.Screens.Edit.Components } }; - tabs.Current.ValueChanged += newValue => Track.Tempo.Value = newValue; + tabs.Current.ValueChanged += newValue => Beatmap.Value.Track.Tempo.Value = newValue; } private void togglePause() { - if (Track.IsRunning) - Track.Stop(); + if (adjustableClock.IsRunning) + adjustableClock.Stop(); else - Track.Start(); + adjustableClock.Start(); } protected override void Update() { base.Update(); - playButton.Icon = Track.IsRunning ? FontAwesome.fa_pause_circle_o : FontAwesome.fa_play_circle_o; + playButton.Icon = adjustableClock.IsRunning ? FontAwesome.fa_pause_circle_o : FontAwesome.fa_play_circle_o; } private class PlaybackTabControl : OsuTabControl diff --git a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs index 9a78e6e189..5a3b6c652b 100644 --- a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs +++ b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs @@ -4,17 +4,20 @@ using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; using System; +using osu.Framework.Allocation; +using osu.Framework.Timing; namespace osu.Game.Screens.Edit.Components { public class TimeInfoContainer : BottomBarContainer { - private const int count_duration = 150; - private readonly OsuSpriteText trackTimer; + private IAdjustableClock adjustableClock; + public TimeInfoContainer() { + Children = new Drawable[] { trackTimer = new OsuSpriteText @@ -28,11 +31,17 @@ namespace osu.Game.Screens.Edit.Components }; } + [BackgroundDependencyLoader] + private void load(IAdjustableClock adjustableClock) + { + this.adjustableClock = adjustableClock; + } + protected override void Update() { base.Update(); - trackTimer.Text = TimeSpan.FromMilliseconds(Track.CurrentTime).ToString(@"mm\:ss\:fff"); + trackTimer.Text = TimeSpan.FromMilliseconds(adjustableClock.CurrentTime).ToString(@"mm\:ss\:fff"); } } } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs index c7f40327a9..b249713581 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input; +using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -19,8 +20,12 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { private readonly Drawable marker; - public MarkerPart() + private readonly IAdjustableClock adjustableClock; + + public MarkerPart(IAdjustableClock adjustableClock) { + this.adjustableClock = adjustableClock; + Add(marker = new MarkerVisualisation()); } @@ -53,12 +58,12 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts seekTo(markerPos / DrawWidth * Beatmap.Value.Track.Length); } - private void seekTo(double time) => Beatmap.Value?.Track.Seek(time); + private void seekTo(double time) => adjustableClock.Seek(time); protected override void Update() { base.Update(); - marker.X = (float)(Beatmap.Value?.Track.CurrentTime ?? 0); + marker.X = (float)adjustableClock.CurrentTime; } protected override void LoadBeatmap(WorkingBeatmap beatmap) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs index 8a472dc357..0e80c13257 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Timing; using osu.Game.Graphics; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; @@ -16,15 +17,14 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary /// public class SummaryTimeline : BottomBarContainer { - private readonly Drawable timelineBar; - - public SummaryTimeline() + [BackgroundDependencyLoader] + private void load(OsuColour colours, IAdjustableClock adjustableClock) { TimelinePart markerPart, controlPointPart, bookmarkPart, breakPart; - Children = new[] + Children = new Drawable[] { - markerPart = new MarkerPart { RelativeSizeAxes = Axes.Both }, + markerPart = new MarkerPart(adjustableClock) { RelativeSizeAxes = Axes.Both }, controlPointPart = new ControlPointPart { Anchor = Anchor.Centre, @@ -39,9 +39,10 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary RelativeSizeAxes = Axes.Both, Height = 0.35f }, - timelineBar = new Container + new Container { RelativeSizeAxes = Axes.Both, + Colour = colours.Gray5, Children = new Drawable[] { new Circle @@ -80,11 +81,5 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary bookmarkPart.Beatmap.BindTo(Beatmap); breakPart.Beatmap.BindTo(Beatmap); } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - timelineBar.Colour = colours.Gray5; - } } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index bb43099352..8b651000fd 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -12,6 +12,7 @@ using osu.Game.Screens.Edit.Menus; using osu.Game.Screens.Edit.Components.Timelines.Summary; using osu.Framework.Allocation; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Timing; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Edit.Screens; using osu.Game.Screens.Edit.Screens.Compose; @@ -26,13 +27,27 @@ namespace osu.Game.Screens.Edit public override bool ShowOverlaysOnEnter => false; - private readonly Box bottomBackground; - private readonly Container screenContainer; + private Box bottomBackground; + private Container screenContainer; private EditorScreen currentScreen; - public Editor() + private DependencyContainer dependencies; + + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) + => dependencies = new DependencyContainer(parent); + + [BackgroundDependencyLoader] + private void load(OsuColour colours) { + // TODO: should probably be done at a RulesetContainer level to share logic with Player. + var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock(); + var adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; + adjustableClock.ChangeSource(sourceClock); + + dependencies.CacheAs(adjustableClock); + dependencies.CacheAs(adjustableClock); + EditorMenuBar menuBar; TimeInfoContainer timeInfo; SummaryTimeline timeline; @@ -130,12 +145,9 @@ namespace osu.Game.Screens.Edit timeline.Beatmap.BindTo(Beatmap); playback.Beatmap.BindTo(Beatmap); menuBar.Mode.ValueChanged += onModeChanged; - } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { bottomBackground.Colour = colours.Gray2; + } private void exportBeatmap() diff --git a/osu.Game/Screens/Edit/Screens/Compose/BeatDivisorControl.cs b/osu.Game/Screens/Edit/Screens/Compose/BeatDivisorControl.cs new file mode 100644 index 0000000000..a7be3c1eb5 --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/BeatDivisorControl.cs @@ -0,0 +1,398 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using OpenTK; +using OpenTK.Graphics; +using OpenTK.Input; + +namespace osu.Game.Screens.Edit.Screens.Compose +{ + public class BeatDivisorControl : CompositeDrawable + { + private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); + private int currentDivisorIndex; + + public BeatDivisorControl(BindableBeatDivisor beatDivisor) + { + this.beatDivisor.BindTo(beatDivisor); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Masking = true; + CornerRadius = 5; + + InternalChildren = new Drawable[] + { + new Box + { + Name = "Gray Background", + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray4 + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Name = "Black Background", + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black + }, + new TickSliderBar(beatDivisor, BindableBeatDivisor.VALID_DIVISORS) + { + RelativeSizeAxes = Axes.Both, + } + } + } + }, + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray4 + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 5 }, + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new DivisorButton + { + Icon = FontAwesome.fa_chevron_left, + Action = beatDivisor.Previous + }, + new DivisorText(beatDivisor), + new DivisorButton + { + Icon = FontAwesome.fa_chevron_right, + Action = beatDivisor.Next + } + }, + }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 20), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 20) + } + } + } + } + } + }, + new Drawable[] + { + new TextFlowContainer(s => s.TextSize = 14) + { + Padding = new MarginPadding { Horizontal = 15 }, + Text = "beat snap divisor", + RelativeSizeAxes = Axes.X, + TextAnchor = Anchor.TopCentre + }, + } + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 30), + new Dimension(GridSizeMode.Absolute, 25), + } + } + }; + } + + private class DivisorText : SpriteText + { + private readonly Bindable beatDivisor = new Bindable(); + + public DivisorText(BindableBeatDivisor beatDivisor) + { + this.beatDivisor.BindTo(beatDivisor); + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Colour = colours.BlueLighter; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + beatDivisor.ValueChanged += v => updateText(); + updateText(); + } + + private void updateText() => Text = $"1/{beatDivisor.Value}"; + } + + private class DivisorButton : IconButton + { + public DivisorButton() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + // Small offset to look a bit better centered along with the divisor text + Y = 1; + + ButtonSize = new Vector2(20); + IconScale = new Vector2(0.6f); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + IconColour = Color4.Black; + HoverColour = colours.Gray7; + FlashColour = colours.Gray9; + } + } + + private class TickSliderBar : SliderBar + { + private Marker marker; + + private readonly BindableBeatDivisor beatDivisor; + private readonly int[] availableDivisors; + + public TickSliderBar(BindableBeatDivisor beatDivisor, params int[] divisors) + { + CurrentNumber.BindTo(this.beatDivisor = beatDivisor); + availableDivisors = divisors; + + Padding = new MarginPadding { Horizontal = 5 }; + } + + [BackgroundDependencyLoader] + private void load() + { + foreach (var t in availableDivisors) + { + AddInternal(new Tick(t) + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopCentre, + RelativePositionAxes = Axes.X, + X = getMappedPosition(t) + }); + } + + AddInternal(marker = new Marker()); + + CurrentNumber.ValueChanged += v => + { + marker.MoveToX(getMappedPosition(v), 100, Easing.OutQuint); + marker.Flash(); + }; + } + + protected override void UpdateValue(float value) + { + } + + public override bool HandleKeyboardInput => IsHovered && !CurrentNumber.Disabled; + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + switch (args.Key) + { + case Key.Right: + beatDivisor.Next(); + OnUserChange(); + return true; + case Key.Left: + beatDivisor.Previous(); + OnUserChange(); + return true; + default: + return false; + } + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + marker.Active = true; + return base.OnMouseDown(state, args); + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + marker.Active = false; + return base.OnMouseUp(state, args); + } + + protected override bool OnClick(InputState state) + { + handleMouseInput(state); + return true; + } + + protected override bool OnDrag(InputState state) + { + handleMouseInput(state); + return true; + } + + private void handleMouseInput(InputState state) + { + // copied from SliderBar so we can do custom spacing logic. + var xPosition = (ToLocalSpace(state?.Mouse.NativeState.Position ?? Vector2.Zero).X - RangePadding) / UsableWidth; + + CurrentNumber.Value = availableDivisors.OrderBy(d => Math.Abs(getMappedPosition(d) - xPosition)).First(); + OnUserChange(); + } + + private float getMappedPosition(float divisor) => (float)Math.Pow((divisor - 1) / (availableDivisors.Last() - 1), 0.90f); + + private class Tick : CompositeDrawable + { + private readonly int divisor; + + public Tick(int divisor) + { + this.divisor = divisor; + Size = new Vector2(2.5f, 10); + + InternalChild = new Box { RelativeSizeAxes = Axes.Both }; + + CornerRadius = 0.5f; + Masking = true; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Colour = getColourForDivisor(divisor, colours); + } + + private ColourInfo getColourForDivisor(int divisor, OsuColour colours) + { + switch (divisor) + { + case 2: + return colours.BlueLight; + case 4: + return colours.Blue; + case 8: + return colours.BlueDarker; + case 16: + return colours.PurpleDark; + case 3: + return colours.YellowLight; + case 6: + return colours.Yellow; + case 12: + return colours.YellowDarker; + default: + return Color4.White; + } + } + } + + private class Marker : CompositeDrawable + { + private Color4 defaultColour; + + private const float size = 7; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Colour = defaultColour = colours.Gray4; + Anchor = Anchor.TopLeft; + Origin = Anchor.TopCentre; + + Width = size; + RelativeSizeAxes = Axes.Y; + RelativePositionAxes = Axes.X; + + InternalChildren = new Drawable[] + { + new Box + { + Width = 2, + RelativeSizeAxes = Axes.Y, + Origin = Anchor.BottomCentre, + Anchor = Anchor.BottomCentre, + Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0.2f), Color4.White), + Blending = BlendingMode.Additive, + }, + new EquilateralTriangle + { + Origin = Anchor.BottomCentre, + Anchor = Anchor.BottomCentre, + Height = size, + EdgeSmoothness = new Vector2(1), + Colour = Color4.White, + } + }; + } + + private bool active; + + public bool Active + { + get => active; + set + { + this.FadeColour(value ? Color4.White : defaultColour, 500, Easing.OutQuint); + active = value; + } + } + + public void Flash() + { + bool wasActive = active; + + Active = true; + + if (wasActive) return; + + using (BeginDelayedSequence(50)) + Active = false; + } + } + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/BindableBeatDivisor.cs b/osu.Game/Screens/Edit/Screens/Compose/BindableBeatDivisor.cs new file mode 100644 index 0000000000..8eb3f1347e --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/BindableBeatDivisor.cs @@ -0,0 +1,39 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Linq; +using osu.Framework.Configuration; + +namespace osu.Game.Screens.Edit.Screens.Compose +{ + public class BindableBeatDivisor : BindableNumber + { + public static readonly int[] VALID_DIVISORS = { 1, 2, 3, 4, 6, 8, 12, 16 }; + + public BindableBeatDivisor(int value = 1) + : base(value) + { + } + + public void Next() => Value = VALID_DIVISORS[Math.Min(VALID_DIVISORS.Length - 1, Array.IndexOf(VALID_DIVISORS, Value) + 1)]; + + public void Previous() => Value = VALID_DIVISORS[Math.Max(0, Array.IndexOf(VALID_DIVISORS, Value) - 1)]; + + public override int Value + { + get { return base.Value; } + set + { + if (!VALID_DIVISORS.Contains(value)) + throw new ArgumentOutOfRangeException($"Provided divisor is not in {nameof(VALID_DIVISORS)}"); + + base.Value = value; + } + } + + protected override int DefaultMinValue => VALID_DIVISORS.First(); + protected override int DefaultMaxValue => VALID_DIVISORS.Last(); + protected override int DefaultPrecision => 1; + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Compose.cs b/osu.Game/Screens/Edit/Screens/Compose/Compose.cs index d42c0bfdac..91adc8324a 100644 --- a/osu.Game/Screens/Edit/Screens/Compose/Compose.cs +++ b/osu.Game/Screens/Edit/Screens/Compose/Compose.cs @@ -1,13 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using osu.Framework.Allocation; using OpenTK.Graphics; 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 @@ -17,10 +17,20 @@ namespace osu.Game.Screens.Edit.Screens.Compose private const float vertical_margins = 10; private const float horizontal_margins = 20; - private readonly Container composerContainer; + private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor(); - public Compose() + private Container composerContainer; + + private DependencyContainer dependencies; + + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) + => dependencies = new DependencyContainer(parent); + + [BackgroundDependencyLoader] + private void load() { + dependencies.Cache(beatDivisor); + ScrollableTimeline timeline; Children = new Drawable[] { @@ -47,15 +57,28 @@ namespace osu.Game.Screens.Edit.Screens.Compose Name = "Timeline content", RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Horizontal = horizontal_margins, Vertical = vertical_margins }, - Children = new Drawable[] + Child = new GridContainer { - new Container + RelativeSizeAxes = Axes.Both, + Content = new[] { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = 115 }, - Child = timeline = new ScrollableTimeline { RelativeSizeAxes = Axes.Both } + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = 5 }, + Child = timeline = new ScrollableTimeline { RelativeSizeAxes = Axes.Both } + }, + new BeatDivisorControl(beatDivisor) { RelativeSizeAxes = Axes.Both } + }, + }, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 90), } - } + }, } } } @@ -75,14 +98,8 @@ namespace osu.Game.Screens.Edit.Screens.Compose }; timeline.Beatmap.BindTo(Beatmap); - Beatmap.ValueChanged += beatmapChanged; - } - private void beatmapChanged(WorkingBeatmap newBeatmap) - { - composerContainer.Clear(); - - var ruleset = newBeatmap.BeatmapInfo.Ruleset?.CreateInstance(); + var ruleset = Beatmap.Value.BeatmapInfo.Ruleset?.CreateInstance(); if (ruleset == null) { Logger.Log("Beatmap doesn't have a ruleset assigned."); diff --git a/osu.Game/Screens/Edit/Screens/Compose/Layers/BorderLayer.cs b/osu.Game/Screens/Edit/Screens/Compose/Layers/BorderLayer.cs new file mode 100644 index 0000000000..49cf078d36 --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/Layers/BorderLayer.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using OpenTK.Graphics; + +namespace osu.Game.Screens.Edit.Screens.Compose.Layers +{ + public class BorderLayer : Container + { + protected override Container Content => content; + private readonly Container content; + + public BorderLayer() + { + InternalChildren = new Drawable[] + { + new Container + { + Name = "Border", + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderColour = Color4.White, + BorderThickness = 2, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + } + }, + content = new Container { RelativeSizeAxes = Axes.Both } + }; + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Layers/HitObjectMaskLayer.cs b/osu.Game/Screens/Edit/Screens/Compose/Layers/HitObjectMaskLayer.cs new file mode 100644 index 0000000000..46b09e2c23 --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/Layers/HitObjectMaskLayer.cs @@ -0,0 +1,66 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Screens.Edit.Screens.Compose.Layers +{ + public class HitObjectMaskLayer : CompositeDrawable + { + private readonly HitObjectComposer composer; + private readonly Container overlayContainer; + + public HitObjectMaskLayer(HitObjectComposer composer) + { + this.composer = composer; + RelativeSizeAxes = Axes.Both; + + InternalChild = overlayContainer = new Container { RelativeSizeAxes = Axes.Both }; + } + + /// + /// Adds an overlay for a which adds movement support. + /// + /// The to create an overlay for. + public void AddOverlay(DrawableHitObject hitObject) + { + var overlay = composer.CreateMaskFor(hitObject); + if (overlay == null) + return; + + overlayContainer.Add(overlay); + } + + /// + /// Removes the overlay for a . + /// + /// The to remove the overlay for. + public void RemoveOverlay(DrawableHitObject hitObject) + { + var existing = overlayContainer.FirstOrDefault(h => h.HitObject == hitObject); + if (existing == null) + return; + + existing.Hide(); + existing.Expire(); + } + + private SelectionBox currentSelectionBox; + + public void AddSelectionOverlay() + { + if (overlayContainer.Count > 0) + AddInternal(currentSelectionBox = composer.CreateSelectionOverlay(overlayContainer)); + } + + public void RemoveSelectionOverlay() + { + currentSelectionBox?.Hide(); + currentSelectionBox?.Expire(); + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Layers/SelectionBox.cs b/osu.Game/Screens/Edit/Screens/Compose/Layers/SelectionBox.cs new file mode 100644 index 0000000000..0e5d824559 --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/Layers/SelectionBox.cs @@ -0,0 +1,102 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Game.Graphics; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Types; +using osu.Game.Rulesets.Objects.Drawables; +using OpenTK; + +namespace osu.Game.Screens.Edit.Screens.Compose.Layers +{ + /// + /// A box which surrounds s and provides interactive handles, context menus etc. + /// + public class SelectionBox : VisibilityContainer + { + private readonly IReadOnlyList overlays; + + public const float BORDER_RADIUS = 2; + + public SelectionBox(IReadOnlyList overlays) + { + this.overlays = overlays; + + Masking = true; + BorderThickness = BORDER_RADIUS; + + InternalChild = new Box + { + RelativeSizeAxes = Axes.Both, + AlwaysPresent = true, + Alpha = 0 + }; + + State = Visibility.Visible; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BorderColour = colours.Yellow; + } + + protected override void Update() + { + base.Update(); + + // Todo: We might need to optimise this + + // Move the rectangle to cover the hitobjects + var topLeft = new Vector2(float.MaxValue, float.MaxValue); + var bottomRight = new Vector2(float.MinValue, float.MinValue); + + foreach (var obj in overlays) + { + topLeft = Vector2.ComponentMin(topLeft, Parent.ToLocalSpace(obj.HitObject.SelectionQuad.TopLeft)); + bottomRight = Vector2.ComponentMax(bottomRight, Parent.ToLocalSpace(obj.HitObject.SelectionQuad.BottomRight)); + } + + topLeft -= new Vector2(5); + bottomRight += new Vector2(5); + + Size = bottomRight - topLeft; + Position = topLeft; + } + + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => overlays.Any(o => o.ReceiveMouseInputAt(screenSpacePos)); + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true; + + protected override bool OnDragStart(InputState state) => true; + + protected override bool OnDrag(InputState state) + { + // Todo: Various forms of snapping + foreach (var hitObject in overlays.Select(o => o.HitObject.HitObject)) + { + switch (hitObject) + { + case IHasEditablePosition editablePosition: + editablePosition.OffsetPosition(state.Mouse.Delta); + break; + } + } + return true; + } + + protected override bool OnDragEnd(InputState state) => true; + + public override bool DisposeOnDeathRemoval => true; + + protected override void PopIn() => this.FadeIn(); + protected override void PopOut() => this.FadeOut(); + } +} diff --git a/osu.Game/Screens/Edit/Screens/Compose/Layers/SelectionLayer.cs b/osu.Game/Screens/Edit/Screens/Compose/Layers/SelectionLayer.cs new file mode 100644 index 0000000000..8c66007bb7 --- /dev/null +++ b/osu.Game/Screens/Edit/Screens/Compose/Layers/SelectionLayer.cs @@ -0,0 +1,239 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Screens.Edit.Screens.Compose.Layers +{ + public class SelectionLayer : CompositeDrawable + { + /// + /// Invoked when a is selected. + /// + public event Action ObjectSelected; + + /// + /// Invoked when a is deselected. + /// + public event Action ObjectDeselected; + + /// + /// Invoked when the selection has been cleared. + /// + public event Action SelectionCleared; + + /// + /// Invoked when the user has finished selecting all s. + /// + public event Action SelectionFinished; + + private readonly Playfield playfield; + + public SelectionLayer(Playfield playfield) + { + this.playfield = playfield; + + RelativeSizeAxes = Axes.Both; + } + + private DragBox dragBox; + + private readonly HashSet selectedHitObjects = new HashSet(); + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + DeselectAll(); + return true; + } + + protected override bool OnDragStart(InputState state) + { + AddInternal(dragBox = new DragBox()); + return true; + } + + protected override bool OnDrag(InputState state) + { + dragBox.Show(); + + var dragPosition = state.Mouse.NativeState.Position; + var dragStartPosition = state.Mouse.NativeState.PositionMouseDown ?? dragPosition; + + var screenSpaceDragQuad = new Quad(dragStartPosition.X, dragStartPosition.Y, dragPosition.X - dragStartPosition.X, dragPosition.Y - dragStartPosition.Y); + + dragBox.SetDragRectangle(screenSpaceDragQuad.AABBFloat); + selectQuad(screenSpaceDragQuad); + + return true; + } + + protected override bool OnDragEnd(InputState state) + { + dragBox.Hide(); + dragBox.Expire(); + + finishSelection(); + + return true; + } + + protected override bool OnClick(InputState state) + { + selectPoint(state.Mouse.NativeState.Position); + finishSelection(); + + return true; + } + + /// + /// Selects a . + /// + /// The to select. + public void Select(DrawableHitObject hitObject) + { + if (!select(hitObject)) + return; + + clearSelection(); + finishSelection(); + } + + /// + /// Selects a without performing capture updates. + /// + /// The to select. + /// Whether was selected. + private bool select(DrawableHitObject hitObject) + { + if (!selectedHitObjects.Add(hitObject)) + return false; + + ObjectSelected?.Invoke(hitObject); + return true; + } + + /// + /// Deselects a . + /// + /// The to deselect. + public void Deselect(DrawableHitObject hitObject) + { + if (!deselect(hitObject)) + return; + + clearSelection(); + finishSelection(); + } + + /// + /// Deselects a without performing capture updates. + /// + /// The to deselect. + /// Whether the was deselected. + private bool deselect(DrawableHitObject hitObject) + { + if (!selectedHitObjects.Remove(hitObject)) + return false; + + ObjectDeselected?.Invoke(hitObject); + return true; + } + + /// + /// Deselects all selected s. + /// + public void DeselectAll() + { + selectedHitObjects.ForEach(h => ObjectDeselected?.Invoke(h)); + selectedHitObjects.Clear(); + + clearSelection(); + } + + /// + /// Selects all hitobjects that are present within the area of a . + /// + /// The selection . + // Todo: If needed we can severely reduce allocations in this method + private void selectQuad(Quad screenSpaceQuad) + { + var expectedSelection = playfield.HitObjects.Objects.Where(h => h.IsAlive && h.IsPresent && screenSpaceQuad.Contains(h.SelectionPoint)).ToList(); + + var toRemove = selectedHitObjects.Except(expectedSelection).ToList(); + foreach (var obj in toRemove) + deselect(obj); + + expectedSelection.ForEach(h => select(h)); + } + + /// + /// Selects the top-most hitobject that is present under a specific point. + /// + /// The to select at. + private void selectPoint(Vector2 screenSpacePoint) + { + var target = playfield.HitObjects.Objects.Reverse().Where(h => h.IsAlive && h.IsPresent).FirstOrDefault(h => h.ReceiveMouseInputAt(screenSpacePoint)); + if (target == null) + return; + + select(target); + } + + private void clearSelection() => SelectionCleared?.Invoke(); + + private void finishSelection() + { + if (selectedHitObjects.Count == 0) + return; + SelectionFinished?.Invoke(); + } + + /// + /// A box that represents a drag selection. + /// + private class DragBox : VisibilityContainer + { + /// + /// Creates a new . + /// + public DragBox() + { + Masking = true; + BorderColour = Color4.White; + BorderThickness = SelectionBox.BORDER_RADIUS; + + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0.1f + }; + } + + public void SetDragRectangle(RectangleF rectangle) + { + var topLeft = Parent.ToLocalSpace(rectangle.TopLeft); + var bottomRight = Parent.ToLocalSpace(rectangle.BottomRight); + + Position = topLeft; + Size = bottomRight - topLeft; + } + + public override bool DisposeOnDeathRemoval => true; + + protected override void PopIn() => this.FadeIn(250, Easing.OutQuint); + protected override void PopOut() => this.FadeOut(250, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Screens/Edit/Screens/EditorScreen.cs b/osu.Game/Screens/Edit/Screens/EditorScreen.cs index 2e654b4373..009830502e 100644 --- a/osu.Game/Screens/Edit/Screens/EditorScreen.cs +++ b/osu.Game/Screens/Edit/Screens/EditorScreen.cs @@ -8,6 +8,9 @@ using osu.Game.Beatmaps; namespace osu.Game.Screens.Edit.Screens { + /// + /// TODO: eventually make this inherit Screen and add a local scren stack inside the Editor. + /// public class EditorScreen : Container { public readonly Bindable Beatmap = new Bindable(); diff --git a/osu.Game/Screens/Menu/FlowContainerWithOrigin.cs b/osu.Game/Screens/Menu/FlowContainerWithOrigin.cs index 29ae35fca4..ae1e995373 100644 --- a/osu.Game/Screens/Menu/FlowContainerWithOrigin.cs +++ b/osu.Game/Screens/Menu/FlowContainerWithOrigin.cs @@ -4,8 +4,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using OpenTK; -using System.Collections.Generic; -using System.Linq; namespace osu.Game.Screens.Menu { @@ -22,8 +20,6 @@ namespace osu.Game.Screens.Menu protected override int Compare(Drawable x, Drawable y) => CompareReverseChildID(x, y); - protected override IEnumerable FlowingChildren => base.FlowingChildren.Reverse(); - public override Anchor Origin => Anchor.Custom; public override Vector2 OriginPosition diff --git a/osu.Game/Screens/Menu/Intro.cs b/osu.Game/Screens/Menu/Intro.cs index 10b08d704d..ce3c93ebcf 100644 --- a/osu.Game/Screens/Menu/Intro.cs +++ b/osu.Game/Screens/Menu/Intro.cs @@ -10,8 +10,8 @@ using osu.Framework.Screens; using osu.Framework.Graphics; using osu.Framework.MathUtils; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.IO; using osu.Game.Configuration; +using osu.Game.IO.Archives; using osu.Game.Screens.Backgrounds; using OpenTK; using OpenTK.Graphics; @@ -62,8 +62,10 @@ namespace osu.Game.Screens.Menu if (setInfo == null) { // we need to import the default menu background beatmap - setInfo = beatmaps.Import(new OszArchiveReader(game.Resources.GetStream(@"Tracks/circles.osz"))); + setInfo = beatmaps.Import(new ZipArchiveReader(game.Resources.GetStream(@"Tracks/circles.osz"), "circles.osz")); + setInfo.Protected = true; + beatmaps.Update(setInfo); } } @@ -73,9 +75,6 @@ namespace osu.Game.Screens.Menu welcome = audio.Sample.Get(@"welcome"); seeya = audio.Sample.Get(@"seeya"); - - if (setInfo.Protected) - beatmaps.Delete(setInfo); } protected override void OnEntering(Screen last) diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 3a3f3d4650..fc747acbb4 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -211,7 +211,7 @@ namespace osu.Game.Screens.Menu rectangle, colourInfo, null, - Shared.VertexBatch.Add, + Shared.VertexBatch.AddAction, //barSize by itself will make it smooth more in the X axis than in the Y axis, this reverts that. Vector2.Divide(inflation, barSize.Yx)); } diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index b91ff0d74b..b7d2ed2e1f 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -143,7 +143,7 @@ namespace osu.Game.Screens.Menu Alpha = 0.5f, Size = new Vector2(0.96f) }, - new BufferedContainer + new Container { AutoSizeAxes = Axes.Both, Children = new Drawable[] @@ -337,12 +337,10 @@ namespace osu.Game.Screens.Menu } } - private bool interactive => Action != null && Alpha > 0.2f; + public override bool HandleMouseInput => base.HandleMouseInput && Action != null && Alpha > 0.2f; protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) { - if (!interactive) return false; - logoBounceContainer.ScaleTo(0.9f, 1000, Easing.Out); return true; } @@ -355,8 +353,6 @@ namespace osu.Game.Screens.Menu protected override bool OnClick(InputState state) { - if (!interactive) return false; - if (Action?.Invoke() ?? true) sampleClick.Play(); @@ -368,8 +364,6 @@ namespace osu.Game.Screens.Menu protected override bool OnHover(InputState state) { - if (!interactive) return false; - logoHoverContainer.ScaleTo(1.1f, 500, Easing.OutElastic); return true; } diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index a2d41dc206..8f4e08d4a2 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -52,6 +52,10 @@ namespace osu.Game.Screens protected readonly Bindable Beatmap = new Bindable(); + protected virtual float BackgroundParallaxAmount => 1; + + private ParallaxContainer backgroundParallaxContainer; + public WorkingBeatmap InitialBeatmap { set @@ -102,11 +106,10 @@ namespace osu.Game.Screens protected override void OnResuming(Screen last) { - base.OnResuming(last); - logo.AppendAnimatingAction(() => LogoArriving(logo, true), true); sampleExit?.Play(); + applyArrivingDefaults(true); - ShowOverlays.Value = ShowOverlaysOnEnter; + base.OnResuming(last); } protected override void OnSuspending(Screen next) @@ -123,6 +126,8 @@ namespace osu.Game.Screens if (lastOsu?.Background != null) { + backgroundParallaxContainer = lastOsu.backgroundParallaxContainer; + if (bg == null || lastOsu.Background.Equals(bg)) //we can keep the previous mode's background. Background = lastOsu.Background; @@ -136,7 +141,7 @@ namespace osu.Game.Screens // this makes up for the fact our padding changes when the global toolbar is visible. bg.Scale = new Vector2(1.06f); - AddInternal(new ParallaxContainer + AddInternal(backgroundParallaxContainer = new ParallaxContainer { Depth = float.MaxValue, Children = new[] @@ -149,11 +154,9 @@ namespace osu.Game.Screens if ((logo = lastOsu?.logo) == null) LoadComponentAsync(logo = new OsuLogo { Alpha = 0 }, AddInternal); - logo.AppendAnimatingAction(() => LogoArriving(logo, false), true); + applyArrivingDefaults(false); base.OnEntering(last); - - ShowOverlays.Value = ShowOverlaysOnEnter; } protected override bool OnExiting(Screen next) @@ -193,6 +196,16 @@ namespace osu.Game.Screens logo.Ripple = true; } + private void applyArrivingDefaults(bool isResuming) + { + logo.AppendAnimatingAction(() => LogoArriving(logo, isResuming), true); + + if (backgroundParallaxContainer != null) + backgroundParallaxContainer.ParallaxAmount = ParallaxContainer.DEFAULT_PARALLAX_AMOUNT * BackgroundParallaxAmount; + + ShowOverlays.Value = ShowOverlaysOnEnter; + } + private void onExitingLogo() { logo.AppendAnimatingAction(() => { LogoExiting(logo); }, false); diff --git a/osu.Game/Screens/Play/BreaksOverlay/BlurredIcon.cs b/osu.Game/Screens/Play/Break/BlurredIcon.cs similarity index 92% rename from osu.Game/Screens/Play/BreaksOverlay/BlurredIcon.cs rename to osu.Game/Screens/Play/Break/BlurredIcon.cs index 5395d7688e..6f47c97f89 100644 --- a/osu.Game/Screens/Play/BreaksOverlay/BlurredIcon.cs +++ b/osu.Game/Screens/Play/Break/BlurredIcon.cs @@ -1,13 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics; -using osu.Game.Graphics; using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using OpenTK; -namespace osu.Game.Screens.Play.BreaksOverlay +namespace osu.Game.Screens.Play.Break { public class BlurredIcon : BufferedContainer { diff --git a/osu.Game/Screens/Play/BreaksOverlay/ArrowsOverlay.cs b/osu.Game/Screens/Play/Break/BreakArrows.cs similarity index 78% rename from osu.Game/Screens/Play/BreaksOverlay/ArrowsOverlay.cs rename to osu.Game/Screens/Play/Break/BreakArrows.cs index 9fdf90bd28..f2a60cdddf 100644 --- a/osu.Game/Screens/Play/BreaksOverlay/ArrowsOverlay.cs +++ b/osu.Game/Screens/Play/Break/BreakArrows.cs @@ -1,18 +1,15 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics; -using OpenTK; +using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; -using osu.Game.Beatmaps.Timing; +using OpenTK; -namespace osu.Game.Screens.Play.BreaksOverlay +namespace osu.Game.Screens.Play.Break { - public class ArrowsOverlay : VisibilityContainer + public class BreakArrows : CompositeDrawable { - private const double fade_duration = BreakPeriod.MIN_BREAK_DURATION / 2; - private const int glow_icon_size = 60; private const int glow_icon_blur_sigma = 10; private const float glow_icon_final_offset = 0.22f; @@ -29,10 +26,10 @@ namespace osu.Game.Screens.Play.BreaksOverlay private readonly BlurredIcon leftBlurredIcon; private readonly BlurredIcon rightBlurredIcon; - public ArrowsOverlay() + public BreakArrows() { RelativeSizeAxes = Axes.Both; - Children = new Drawable[] + InternalChildren = new Drawable[] { leftGlowIcon = new GlowIcon { @@ -82,22 +79,22 @@ namespace osu.Game.Screens.Play.BreaksOverlay }; } - protected override void PopIn() + public void Show(double duration) { - leftGlowIcon.MoveToX(-glow_icon_final_offset, fade_duration, Easing.OutQuint); - rightGlowIcon.MoveToX(glow_icon_final_offset, fade_duration, Easing.OutQuint); + leftGlowIcon.MoveToX(-glow_icon_final_offset, duration, Easing.OutQuint); + rightGlowIcon.MoveToX(glow_icon_final_offset, duration, Easing.OutQuint); - leftBlurredIcon.MoveToX(-blurred_icon_final_offset, fade_duration, Easing.OutQuint); - rightBlurredIcon.MoveToX(blurred_icon_final_offset, fade_duration, Easing.OutQuint); + leftBlurredIcon.MoveToX(-blurred_icon_final_offset, duration, Easing.OutQuint); + rightBlurredIcon.MoveToX(blurred_icon_final_offset, duration, Easing.OutQuint); } - protected override void PopOut() + public void Hide(double duration) { - leftGlowIcon.MoveToX(-glow_icon_offscreen_offset, fade_duration, Easing.OutQuint); - rightGlowIcon.MoveToX(glow_icon_offscreen_offset, fade_duration, Easing.OutQuint); + leftGlowIcon.MoveToX(-glow_icon_offscreen_offset, duration, Easing.OutQuint); + rightGlowIcon.MoveToX(glow_icon_offscreen_offset, duration, Easing.OutQuint); - leftBlurredIcon.MoveToX(-blurred_icon_offscreen_offset, fade_duration, Easing.OutQuint); - rightBlurredIcon.MoveToX(blurred_icon_offscreen_offset, fade_duration, Easing.OutQuint); + leftBlurredIcon.MoveToX(-blurred_icon_offscreen_offset, duration, Easing.OutQuint); + rightBlurredIcon.MoveToX(blurred_icon_offscreen_offset, duration, Easing.OutQuint); } } } diff --git a/osu.Game/Screens/Play/BreaksOverlay/InfoContainer.cs b/osu.Game/Screens/Play/Break/BreakInfo.cs similarity index 62% rename from osu.Game/Screens/Play/BreaksOverlay/InfoContainer.cs rename to osu.Game/Screens/Play/Break/BreakInfo.cs index d7ab4ff2e5..5e011903fe 100644 --- a/osu.Game/Screens/Play/BreaksOverlay/InfoContainer.cs +++ b/osu.Game/Screens/Play/Break/BreakInfo.cs @@ -1,24 +1,21 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Scoring; -using osu.Game.Beatmaps.Timing; +using OpenTK; -namespace osu.Game.Screens.Play.BreaksOverlay +namespace osu.Game.Screens.Play.Break { - public class InfoContainer : VisibilityContainer + public class BreakInfo : Container { - private const double fade_duration = BreakPeriod.MIN_BREAK_DURATION / 2; + public PercentageBreakInfoLine AccuracyDisplay; + public BreakInfoLine RankDisplay; + public BreakInfoLine GradeDisplay; - public PercentageInfoLine AccuracyDisplay; - public InfoLine RankDisplay; - public InfoLine GradeDisplay; - - public InfoContainer() + public BreakInfo() { AutoSizeAxes = Axes.Both; Child = new FillFlowContainer @@ -43,16 +40,13 @@ namespace osu.Game.Screens.Play.BreaksOverlay Direction = FillDirection.Vertical, Children = new Drawable[] { - AccuracyDisplay = new PercentageInfoLine("Accuracy"), - RankDisplay = new InfoLine("Rank"), - GradeDisplay = new InfoLine("Grade"), + AccuracyDisplay = new PercentageBreakInfoLine("Accuracy"), + RankDisplay = new BreakInfoLine("Rank"), + GradeDisplay = new BreakInfoLine("Grade"), }, } }, }; } - - protected override void PopIn() => this.FadeIn(fade_duration); - protected override void PopOut() => this.FadeOut(fade_duration); } } diff --git a/osu.Game/Screens/Play/BreaksOverlay/InfoLine.cs b/osu.Game/Screens/Play/Break/BreakInfoLine.cs similarity index 84% rename from osu.Game/Screens/Play/BreaksOverlay/InfoLine.cs rename to osu.Game/Screens/Play/Break/BreakInfoLine.cs index b39eaf1c22..3d96bca1fa 100644 --- a/osu.Game/Screens/Play/BreaksOverlay/InfoLine.cs +++ b/osu.Game/Screens/Play/Break/BreakInfoLine.cs @@ -8,9 +8,9 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -namespace osu.Game.Screens.Play.BreaksOverlay +namespace osu.Game.Screens.Play.Break { - public class InfoLine : Container + public class BreakInfoLine : Container where T : struct { private const int margin = 2; @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Play.BreaksOverlay private readonly string prefix; - public InfoLine(string name, string prefix = @"") + public BreakInfoLine(string name, string prefix = @"") { this.prefix = prefix; @@ -71,9 +71,9 @@ namespace osu.Game.Screens.Play.BreaksOverlay } } - public class PercentageInfoLine : InfoLine + public class PercentageBreakInfoLine : BreakInfoLine { - public PercentageInfoLine(string name, string prefix = "") : base(name, prefix) + public PercentageBreakInfoLine(string name, string prefix = "") : base(name, prefix) { } diff --git a/osu.Game/Screens/Play/BreaksOverlay/GlowIcon.cs b/osu.Game/Screens/Play/Break/GlowIcon.cs similarity index 93% rename from osu.Game/Screens/Play/BreaksOverlay/GlowIcon.cs rename to osu.Game/Screens/Play/Break/GlowIcon.cs index bad9df2093..79b39a873a 100644 --- a/osu.Game/Screens/Play/BreaksOverlay/GlowIcon.cs +++ b/osu.Game/Screens/Play/Break/GlowIcon.cs @@ -1,13 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Graphics.Containers; +using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using OpenTK; -using osu.Framework.Allocation; -namespace osu.Game.Screens.Play.BreaksOverlay +namespace osu.Game.Screens.Play.Break { public class GlowIcon : Container { @@ -16,24 +16,24 @@ namespace osu.Game.Screens.Play.BreaksOverlay public override Vector2 Size { + get { return base.Size; } set { blurredIcon.Size = spriteIcon.Size = value; blurredIcon.ForceRedraw(); } - get { return base.Size; } } public Vector2 BlurSigma { - set { blurredIcon.BlurSigma = value; } get { return blurredIcon.BlurSigma; } + set { blurredIcon.BlurSigma = value; } } public FontAwesome Icon { - set { spriteIcon.Icon = blurredIcon.Icon = value; } get { return spriteIcon.Icon; } + set { spriteIcon.Icon = blurredIcon.Icon = value; } } public GlowIcon() diff --git a/osu.Game/Screens/Play/BreaksOverlay/LetterboxOverlay.cs b/osu.Game/Screens/Play/Break/LetterboxOverlay.cs similarity index 77% rename from osu.Game/Screens/Play/BreaksOverlay/LetterboxOverlay.cs rename to osu.Game/Screens/Play/Break/LetterboxOverlay.cs index f4c9362fff..21eb5ebea0 100644 --- a/osu.Game/Screens/Play/BreaksOverlay/LetterboxOverlay.cs +++ b/osu.Game/Screens/Play/Break/LetterboxOverlay.cs @@ -1,18 +1,16 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK.Graphics; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Beatmaps.Timing; +using OpenTK.Graphics; -namespace osu.Game.Screens.Play.BreaksOverlay +namespace osu.Game.Screens.Play.Break { - public class LetterboxOverlay : VisibilityContainer + public class LetterboxOverlay : CompositeDrawable { - private const double fade_duration = BreakPeriod.MIN_BREAK_DURATION / 2; private const int height = 350; private static readonly Color4 transparent_black = new Color4(0, 0, 0, 0); @@ -20,7 +18,7 @@ namespace osu.Game.Screens.Play.BreaksOverlay public LetterboxOverlay() { RelativeSizeAxes = Axes.Both; - Children = new Drawable[] + InternalChildren = new Drawable[] { new Container { @@ -48,8 +46,5 @@ namespace osu.Game.Screens.Play.BreaksOverlay } }; } - - protected override void PopIn() => this.FadeIn(fade_duration); - protected override void PopOut() => this.FadeOut(fade_duration); } } diff --git a/osu.Game/Screens/Play/BreaksOverlay/RemainingTimeCounter.cs b/osu.Game/Screens/Play/Break/RemainingTimeCounter.cs similarity index 70% rename from osu.Game/Screens/Play/BreaksOverlay/RemainingTimeCounter.cs rename to osu.Game/Screens/Play/Break/RemainingTimeCounter.cs index 015fefb423..f6e683f519 100644 --- a/osu.Game/Screens/Play/BreaksOverlay/RemainingTimeCounter.cs +++ b/osu.Game/Screens/Play/Break/RemainingTimeCounter.cs @@ -1,18 +1,15 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Game.Graphics.Sprites; -using osu.Framework.Graphics; using System; -using osu.Game.Beatmaps.Timing; +using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.Sprites; -namespace osu.Game.Screens.Play.BreaksOverlay +namespace osu.Game.Screens.Play.Break { public class RemainingTimeCounter : Counter { - private const double fade_duration = BreakPeriod.MIN_BREAK_DURATION / 2; - private readonly OsuSpriteText counter; public RemainingTimeCounter() @@ -25,13 +22,8 @@ namespace osu.Game.Screens.Play.BreaksOverlay TextSize = 33, Font = "Venera", }; - - Alpha = 0; } protected override void OnCountChanged(double count) => counter.Text = ((int)Math.Ceiling(count / 1000)).ToString(); - - public override void Show() => this.FadeIn(fade_duration); - public override void Hide() => this.FadeOut(fade_duration); } } diff --git a/osu.Game/Screens/Play/BreaksOverlay/BreakOverlay.cs b/osu.Game/Screens/Play/BreakOverlay.cs similarity index 51% rename from osu.Game/Screens/Play/BreaksOverlay/BreakOverlay.cs rename to osu.Game/Screens/Play/BreakOverlay.cs index af7c1ef5aa..6c7ee596a1 100644 --- a/osu.Game/Screens/Play/BreaksOverlay/BreakOverlay.cs +++ b/osu.Game/Screens/Play/BreakOverlay.cs @@ -1,15 +1,16 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Scoring; -using System.Collections.Generic; -using osu.Framework.Graphics.UserInterface; +using osu.Game.Screens.Play.Break; -namespace osu.Game.Screens.Play.BreaksOverlay +namespace osu.Game.Screens.Play { public class BreakOverlay : Container { @@ -18,28 +19,26 @@ namespace osu.Game.Screens.Play.BreaksOverlay private const int vertical_margin = 25; private List breaks; + + private readonly Container fadeContainer; + public List Breaks { + get => breaks; set { breaks = value; initializeBreaks(); } - get - { - return breaks; - } } public override bool RemoveCompletedTransforms => false; - private readonly bool letterboxing; - private readonly LetterboxOverlay letterboxOverlay; private readonly Container remainingTimeAdjustmentBox; private readonly Container remainingTimeBox; private readonly RemainingTimeCounter remainingTimeCounter; - private readonly InfoContainer info; - private readonly ArrowsOverlay arrowsOverlay; + private readonly BreakInfo info; + private readonly BreakArrows breakArrows; public BreakOverlay(bool letterboxing, ScoreProcessor scoreProcessor) : this(letterboxing) @@ -49,61 +48,72 @@ namespace osu.Game.Screens.Play.BreaksOverlay public BreakOverlay(bool letterboxing) { - this.letterboxing = letterboxing; - RelativeSizeAxes = Axes.Both; - Children = new Drawable[] + Child = fadeContainer = new Container { - letterboxOverlay = new LetterboxOverlay + Alpha = 0, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - remainingTimeAdjustmentBox = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Width = 0, - Child = remainingTimeBox = new Container + new LetterboxOverlay + { + Alpha = letterboxing ? 1 : 0, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + remainingTimeAdjustmentBox = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, - Height = 8, - CornerRadius = 4, - Masking = true, - Child = new Box { RelativeSizeAxes = Axes.Both } + Width = 0, + Child = remainingTimeBox = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Height = 8, + CornerRadius = 4, + Masking = true, + Child = new Box { RelativeSizeAxes = Axes.Both } + } + }, + remainingTimeCounter = new RemainingTimeCounter + { + Anchor = Anchor.Centre, + Origin = Anchor.BottomCentre, + Margin = new MarginPadding { Bottom = vertical_margin }, + }, + info = new BreakInfo + { + Anchor = Anchor.Centre, + Origin = Anchor.TopCentre, + Margin = new MarginPadding { Top = vertical_margin }, + }, + breakArrows = new BreakArrows + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, } - }, - remainingTimeCounter = new RemainingTimeCounter - { - Anchor = Anchor.Centre, - Origin = Anchor.BottomCentre, - Margin = new MarginPadding { Bottom = vertical_margin }, - }, - info = new InfoContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - Margin = new MarginPadding { Top = vertical_margin }, - }, - arrowsOverlay = new ArrowsOverlay - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, } }; } + protected override void LoadComplete() + { + base.LoadComplete(); + initializeBreaks(); + } + private void initializeBreaks() { + if (!IsLoaded) return; // we need a clock. + FinishTransforms(true); Scheduler.CancelDelayedTasks(); - if (breaks == null) - return; + if (breaks == null) return; //we need breaks. foreach (var b in breaks) { @@ -112,6 +122,9 @@ namespace osu.Game.Screens.Play.BreaksOverlay using (BeginAbsoluteSequence(b.StartTime, true)) { + fadeContainer.FadeIn(fade_duration); + breakArrows.Show(fade_duration); + remainingTimeAdjustmentBox .ResizeWidthTo(remaining_time_container_max_size, fade_duration, Easing.OutQuint) .Delay(b.Duration - fade_duration) @@ -123,37 +136,16 @@ namespace osu.Game.Screens.Play.BreaksOverlay .ResizeWidthTo(1); remainingTimeCounter.CountTo(b.Duration).CountTo(0, b.Duration); - } - using (BeginAbsoluteSequence(b.StartTime)) - { - Schedule(showBreak); - using (BeginDelayedSequence(b.Duration - fade_duration)) - Schedule(hideBreak); + using (BeginDelayedSequence(b.Duration - fade_duration, true)) + { + fadeContainer.FadeOut(fade_duration); + breakArrows.Hide(fade_duration); + } } } } - private void showBreak() - { - if (letterboxing) - letterboxOverlay.Show(); - - remainingTimeCounter.Show(); - info.Show(); - arrowsOverlay.Show(); - } - - private void hideBreak() - { - if (letterboxing) - letterboxOverlay.Hide(); - - remainingTimeCounter.Hide(); - info.Hide(); - arrowsOverlay.Hide(); - } - private void bindProcessor(ScoreProcessor processor) { info.AccuracyDisplay.Current.BindTo(processor.Accuracy); diff --git a/osu.Game/Screens/Play/GameplayMenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs index 615c124ea7..29b68abc21 100644 --- a/osu.Game/Screens/Play/GameplayMenuOverlay.cs +++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs @@ -18,7 +18,7 @@ using System.Collections.Generic; namespace osu.Game.Screens.Play { - public abstract class GameplayMenuOverlay : OverlayContainer, IRequireHighFrequencyMousePosition + public abstract class GameplayMenuOverlay : OverlayContainer { private const int transition_duration = 200; private const int button_height = 70; diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index e6cf1f7982..5dba10ffc1 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Play.HUD //CollectionSettings = new CollectionSettings(), //DiscussionSettings = new DiscussionSettings(), PlaybackSettings = new PlaybackSettings(), - VisualSettings = new VisualSettings() + VisualSettings = new VisualSettings { Expanded = false } } }; diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index e68a17f014..b0fbde74d2 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play private static bool hasShownNotificationOnce; - public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContainer, DecoupleableInterpolatingFramedClock decoupledClock, WorkingBeatmap working, IAdjustableClock adjustableSourceClock) + public HUDOverlay(ScoreProcessor scoreProcessor, RulesetContainer rulesetContainer, WorkingBeatmap working, IClock offsetClock, IAdjustableClock adjustableClock) { RelativeSizeAxes = Axes.Both; @@ -66,13 +66,13 @@ namespace osu.Game.Screens.Play BindRulesetContainer(rulesetContainer); Progress.Objects = rulesetContainer.Objects; - Progress.AudioClock = decoupledClock; + Progress.AudioClock = offsetClock; Progress.AllowSeeking = rulesetContainer.HasReplayLoaded; - Progress.OnSeek = pos => decoupledClock.Seek(pos); + Progress.OnSeek = pos => adjustableClock.Seek(pos); ModDisplay.Current.BindTo(working.Mods); - PlayerSettingsOverlay.PlaybackSettings.AdjustableClock = adjustableSourceClock; + PlayerSettingsOverlay.PlaybackSettings.AdjustableClock = adjustableClock; } [BackgroundDependencyLoader(true)] diff --git a/osu.Game/Screens/Play/PauseContainer.cs b/osu.Game/Screens/Play/PauseContainer.cs index ac497868bf..40e734b7df 100644 --- a/osu.Game/Screens/Play/PauseContainer.cs +++ b/osu.Game/Screens/Play/PauseContainer.cs @@ -36,6 +36,7 @@ namespace osu.Game.Screens.Play public int Retries { set { pauseOverlay.Retries = value; } } public bool CanPause => (CheckCanPause?.Invoke() ?? true) && Time.Current >= lastPauseActionTime + pause_cooldown; + public bool IsResuming { get; private set; } public Action OnRetry; public Action OnQuit; @@ -43,63 +44,66 @@ namespace osu.Game.Screens.Play public Action OnResume; public Action OnPause; - public IAdjustableClock AudioClock; - public FramedClock FramedClock; + private readonly IAdjustableClock adjustableClock; + private readonly FramedClock framedClock; - public PauseContainer() + public PauseContainer(FramedClock framedClock, IAdjustableClock adjustableClock) { + this.framedClock = framedClock; + this.adjustableClock = adjustableClock; + RelativeSizeAxes = Axes.Both; - AddInternal(content = new Container { RelativeSizeAxes = Axes.Both }); + AddInternal(content = new Container + { + Clock = this.framedClock, + ProcessCustomClock = false, + RelativeSizeAxes = Axes.Both + }); AddInternal(pauseOverlay = new PauseOverlay { - OnResume = () => this.Delay(400).Schedule(Resume), + OnResume = () => + { + IsResuming = true; + this.Delay(400).Schedule(Resume); + }, OnRetry = () => OnRetry(), OnQuit = () => OnQuit(), }); } - public void Pause(bool force = false) + public void Pause(bool force = false) => Schedule(() => // Scheduled to ensure a stable position in execution order, no matter how it was called. { if (!CanPause && !force) return; if (IsPaused) return; - // stop the decoupled clock (stops the audio eventually) - AudioClock.Stop(); - - // stop processing updatess on the offset clock (instantly freezes time for all our components) - FramedClock.ProcessSourceClockFrames = false; - + // stop the seekable clock (stops the audio eventually) + adjustableClock.Stop(); IsPaused = true; - // we need to do a final check after all of our children have processed up to the paused clock time. - // this is to cover cases where, for instance, the player fails in the current processing frame. - Schedule(() => - { - if (!CanPause) return; + OnPause?.Invoke(); + pauseOverlay.Show(); - lastPauseActionTime = Time.Current; - - OnPause?.Invoke(); - pauseOverlay.Show(); - }); - } + lastPauseActionTime = Time.Current; + }); public void Resume() { if (!IsPaused) return; IsPaused = false; - FramedClock.ProcessSourceClockFrames = true; - + IsResuming = false; lastPauseActionTime = Time.Current; - OnResume?.Invoke(); + // seek back to the time of the framed clock. + // this accounts for the audio clock potentially taking time to enter a completely stopped state. + adjustableClock.Seek(framedClock.CurrentTime); + adjustableClock.Start(); + OnResume?.Invoke(); pauseOverlay.Hide(); - AudioClock.Start(); } private OsuGameBase game; @@ -116,6 +120,9 @@ namespace osu.Game.Screens.Play if (!game.IsActive && CanPause) Pause(); + if (!IsPaused) + framedClock.ProcessFrame(); + base.Update(); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 72864482d5..b0472f0e0d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -25,38 +25,40 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; -using osu.Game.Screens.Backgrounds; -using osu.Game.Screens.Play.BreaksOverlay; using osu.Game.Screens.Ranking; +using osu.Game.Skinning; using osu.Game.Storyboards.Drawables; -using OpenTK; namespace osu.Game.Screens.Play { - public class Player : OsuScreen, IProvideCursor + public class Player : ScreenWithBeatmapBackground, IProvideCursor { - protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap); + protected override float BackgroundParallaxAmount => 0.1f; public override bool ShowOverlaysOnEnter => false; public Action RestartRequested; - public override bool AllowBeatmapRulesetChange => false; - public bool HasFailed { get; private set; } public bool AllowPause { get; set; } = true; public bool AllowLeadIn { get; set; } = true; public bool AllowResults { get; set; } = true; + private Bindable mouseWheelDisabled; + private Bindable userAudioOffset; + public int RestartCount; public CursorContainer Cursor => RulesetContainer.Cursor; public bool ProvidingUserCursor => RulesetContainer?.Cursor != null && !RulesetContainer.HasReplayLoaded.Value; - private IAdjustableClock adjustableSourceClock; - private FramedOffsetClock offsetClock; - private DecoupleableInterpolatingFramedClock decoupledClock; + private IAdjustableClock sourceClock; + + /// + /// The decoupled clock used for gameplay. Should be used for seeks and clock control. + /// + private DecoupleableInterpolatingFramedClock adjustableClock; private PauseContainer pauseContainer; @@ -64,41 +66,27 @@ namespace osu.Game.Screens.Play private APIAccess api; - private ScoreProcessor scoreProcessor; - protected RulesetContainer RulesetContainer; - - #region User Settings - - private Bindable dimLevel; - private Bindable blurLevel; - private Bindable showStoryboard; - private Bindable mouseWheelDisabled; - private Bindable userAudioOffset; - private SampleChannel sampleRestart; - #endregion - - private Container storyboardContainer; - private DrawableStoryboard storyboard; + private ScoreProcessor scoreProcessor; + protected RulesetContainer RulesetContainer; private HUDOverlay hudOverlay; private FailOverlay failOverlay; + private DrawableStoryboard storyboard; + private Container storyboardContainer; + private bool loadedSuccessfully => RulesetContainer?.Objects.Any() == true; [BackgroundDependencyLoader] - private void load(AudioManager audio, OsuConfigManager config, APIAccess api) + private void load(AudioManager audio, APIAccess api, OsuConfigManager config) { this.api = api; - - dimLevel = config.GetBindable(OsuSetting.DimLevel); - blurLevel = config.GetBindable(OsuSetting.BlurLevel); - showStoryboard = config.GetBindable(OsuSetting.ShowStoryboard); + sampleRestart = audio.Sample.Get(@"Gameplay/restart"); mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); - - sampleRestart = audio.Sample.Get(@"Gameplay/restart"); + userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); WorkingBeatmap working = Beatmap.Value; Beatmap beatmap; @@ -138,19 +126,19 @@ namespace osu.Game.Screens.Play return; } - adjustableSourceClock = (IAdjustableClock)working.Track ?? new StopwatchClock(); - decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; + sourceClock = (IAdjustableClock)working.Track ?? new StopwatchClock(); + adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; var firstObjectTime = RulesetContainer.Objects.First().StartTime; - decoupledClock.Seek(AllowLeadIn + adjustableClock.Seek(AllowLeadIn ? Math.Min(0, firstObjectTime - Math.Max(beatmap.ControlPointInfo.TimingPointAt(firstObjectTime).BeatLength * 4, beatmap.BeatmapInfo.AudioLeadIn)) : firstObjectTime); - decoupledClock.ProcessFrame(); + adjustableClock.ProcessFrame(); - offsetClock = new FramedOffsetClock(decoupledClock); + // the final usable gameplay clock with user-set offsets applied. + var offsetClock = new FramedOffsetClock(adjustableClock); - userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); userAudioOffset.ValueChanged += v => offsetClock.Offset = v; userAudioOffset.TriggerChange(); @@ -158,16 +146,8 @@ namespace osu.Game.Screens.Play Children = new Drawable[] { - storyboardContainer = new Container + pauseContainer = new PauseContainer(offsetClock, adjustableClock) { - RelativeSizeAxes = Axes.Both, - Clock = offsetClock, - Alpha = 0, - }, - pauseContainer = new PauseContainer - { - AudioClock = decoupledClock, - FramedClock = offsetClock, OnRetry = Restart, OnQuit = Exit, CheckCanPause = () => AllowPause && ValidForResume && !HasFailed && !RulesetContainer.HasReplayLoaded, @@ -179,15 +159,27 @@ namespace osu.Game.Screens.Play OnResume = () => hudOverlay.KeyCounter.IsCounting = true, Children = new Drawable[] { - new Container + storyboardContainer = new Container { RelativeSizeAxes = Axes.Both, - Clock = offsetClock, - Child = RulesetContainer, + Alpha = 0, }, - new SkipButton(firstObjectTime) { AudioClock = decoupledClock }, - hudOverlay = new HUDOverlay(scoreProcessor, RulesetContainer, decoupledClock, working, adjustableSourceClock) + new LocalSkinOverrideContainer(working.Skin) { + RelativeSizeAxes = Axes.Both, + Child = RulesetContainer + }, + new SkipOverlay(firstObjectTime) + { + Clock = Clock, // skip button doesn't want to use the audio clock directly + ProcessCustomClock = false, + AdjustableClock = adjustableClock, + FramedClock = offsetClock, + }, + hudOverlay = new HUDOverlay(scoreProcessor, RulesetContainer, working, offsetClock, adjustableClock) + { + Clock = Clock, // hud overlay doesn't want to use the audio clock directly + ProcessCustomClock = false, Anchor = Anchor.Centre, Origin = Anchor.Centre }, @@ -195,7 +187,7 @@ namespace osu.Game.Screens.Play { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Clock = decoupledClock, + ProcessCustomClock = false, Breaks = beatmap.Breaks } } @@ -219,7 +211,7 @@ namespace osu.Game.Screens.Play } }; - if (showStoryboard) + if (ShowStoryboard) initializeStoryboard(false); // Bind ScoreProcessor to ourselves @@ -232,24 +224,11 @@ namespace osu.Game.Screens.Play private void applyRateFromMods() { - if (adjustableSourceClock == null) return; + if (sourceClock == null) return; - adjustableSourceClock.Rate = 1; + sourceClock.Rate = 1; foreach (var mod in Beatmap.Value.Mods.Value.OfType()) - mod.ApplyToClock(adjustableSourceClock); - } - - private void initializeStoryboard(bool asyncLoad) - { - var beatmap = Beatmap.Value; - - storyboard = beatmap.Storyboard.CreateDrawable(Beatmap.Value); - storyboard.Masking = true; - - if (asyncLoad) - LoadComponentAsync(storyboard, storyboardContainer.Add); - else - storyboardContainer.Add(storyboard); + mod.ApplyToClock(sourceClock); } public void Restart() @@ -295,7 +274,7 @@ namespace osu.Game.Screens.Play if (Beatmap.Value.Mods.Value.OfType().Any(m => !m.AllowFail)) return false; - decoupledClock.Stop(); + adjustableClock.Stop(); HasFailed = true; failOverlay.Retries = RestartCount; @@ -310,11 +289,6 @@ namespace osu.Game.Screens.Play if (!loadedSuccessfully) return; - dimLevel.ValueChanged += _ => updateBackgroundElements(); - blurLevel.ValueChanged += _ => updateBackgroundElements(); - showStoryboard.ValueChanged += _ => updateBackgroundElements(); - updateBackgroundElements(); - Content.Alpha = 0; Content .ScaleTo(0.7f) @@ -324,17 +298,19 @@ namespace osu.Game.Screens.Play Task.Run(() => { - adjustableSourceClock.Reset(); + sourceClock.Reset(); Schedule(() => { - decoupledClock.ChangeSource(adjustableSourceClock); + adjustableClock.ChangeSource(sourceClock); applyRateFromMods(); this.Delay(750).Schedule(() => { if (!pauseContainer.IsPaused) - decoupledClock.Start(); + { + adjustableClock.Start(); + } }); }); }); @@ -351,7 +327,7 @@ namespace osu.Game.Screens.Play protected override bool OnExiting(Screen next) { - if (!AllowPause || HasFailed || !ValidForResume || pauseContainer?.IsPaused != false || RulesetContainer?.HasReplayLoaded != false) + if ((!AllowPause || HasFailed || !ValidForResume || pauseContainer?.IsPaused != false || RulesetContainer?.HasReplayLoaded != false) && (!pauseContainer?.IsResuming ?? true)) { // In the case of replays, we may have changed the playback rate. applyRateFromMods(); @@ -361,35 +337,11 @@ namespace osu.Game.Screens.Play } if (loadedSuccessfully) - { - pauseContainer.Pause(); - } + pauseContainer?.Pause(); return true; } - private void updateBackgroundElements() - { - if (!IsCurrentScreen) return; - - const float duration = 800; - - var opacity = 1 - (float)dimLevel; - - if (showStoryboard && storyboard == null) - initializeStoryboard(true); - - var beatmap = Beatmap.Value; - var storyboardVisible = showStoryboard && beatmap.Storyboard.HasDrawable; - - storyboardContainer - .FadeColour(OsuColour.Gray(opacity), duration, Easing.OutQuint) - .FadeTo(storyboardVisible && opacity > 0 ? 1 : 0, duration, Easing.OutQuint); - - (Background as BackgroundScreenBeatmap)?.BlurTo(new Vector2((float)blurLevel.Value * 25), duration, Easing.OutQuint); - Background?.FadeTo(!storyboardVisible || beatmap.Background == null ? opacity : 0, duration, Easing.OutQuint); - } - private void fadeOut() { const float fade_out_duration = 250; @@ -403,5 +355,41 @@ namespace osu.Game.Screens.Play } protected override bool OnWheel(InputState state) => mouseWheelDisabled.Value && !pauseContainer.IsPaused; + + private void initializeStoryboard(bool asyncLoad) + { + if (storyboardContainer == null) + return; + + var beatmap = Beatmap.Value; + + storyboard = beatmap.Storyboard.CreateDrawable(); + storyboard.Masking = true; + + if (asyncLoad) + LoadComponentAsync(storyboard, storyboardContainer.Add); + else + storyboardContainer.Add(storyboard); + } + + protected override void UpdateBackgroundElements() + { + if (!IsCurrentScreen) return; + + base.UpdateBackgroundElements(); + + if (ShowStoryboard && storyboard == null) + initializeStoryboard(true); + + var beatmap = Beatmap.Value; + var storyboardVisible = ShowStoryboard && beatmap.Storyboard.HasDrawable; + + storyboardContainer? + .FadeColour(OsuColour.Gray(BackgroundOpacity), BACKGROUND_FADE_DURATION, Easing.OutQuint) + .FadeTo(storyboardVisible && BackgroundOpacity > 0 ? 1 : 0, BACKGROUND_FADE_DURATION, Easing.OutQuint); + + if (storyboardVisible && beatmap.Storyboard.ReplacesBackground) + Background?.FadeTo(0, BACKGROUND_FADE_DURATION, Easing.OutQuint); + } } } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 2950990779..6d55cdb9ca 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -1,35 +1,34 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Screens.Backgrounds; using OpenTK; using osu.Framework.Localisation; +using osu.Framework.Threading; using osu.Game.Screens.Menu; using osu.Game.Screens.Play.PlayerSettings; namespace osu.Game.Screens.Play { - public class PlayerLoader : OsuScreen + public class PlayerLoader : ScreenWithBeatmapBackground { private Player player; private BeatmapMetadataDisplay info; - private VisualSettings visualSettings; private bool showOverlays = true; public override bool ShowOverlaysOnEnter => showOverlays; - public override bool AllowBeatmapRulesetChange => false; - - protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap); + private Task loadTask; public PlayerLoader(Player player) { @@ -51,14 +50,15 @@ namespace osu.Game.Screens.Play Anchor = Anchor.Centre, Origin = Anchor.Centre, }); - Add(visualSettings = new VisualSettings + + Add(new VisualSettings { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Margin = new MarginPadding(25) }); - LoadComponentAsync(player); + loadTask = LoadComponentAsync(player); } protected override void OnResuming(Screen last) @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Play contentIn(); //we will only be resumed if the player has requested a re-run (see ValidForResume setting above) - LoadComponentAsync(player = new Player + loadTask = LoadComponentAsync(player = new Player { RestartCount = player.RestartCount + 1, RestartRequested = player.RestartRequested, @@ -93,14 +93,12 @@ namespace osu.Game.Screens.Play { base.OnEntering(last); - Background.FadeTo(0.4f, 250); - Content.ScaleTo(0.7f); contentIn(); info.Delay(750).FadeIn(500); - this.Delay(2150).Schedule(pushWhenLoaded); + this.Delay(1800).Schedule(pushWhenLoaded); } protected override void LogoArriving(OsuLogo logo, bool resuming) @@ -116,39 +114,97 @@ namespace osu.Game.Screens.Play logo.Delay(resuming ? 0 : 500).MoveToOffset(new Vector2(0, -0.24f), 500, Easing.InOutExpo); } + private bool weHandledMouseDown; + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + weHandledMouseDown = true; + return base.OnMouseDown(state, args); + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + weHandledMouseDown = false; + return base.OnMouseUp(state, args); + } + + private ScheduledDelegate pushDebounce; + + private bool readyForPush => player.LoadState == LoadState.Ready && IsHovered && (!GetContainingInputManager().CurrentState.Mouse.HasAnyButtonPressed || weHandledMouseDown); + private void pushWhenLoaded() { - if (player.LoadState != LoadState.Ready || visualSettings.IsHovered) + if (!IsCurrentScreen) return; + + try + { + if (!readyForPush) + { + // as the pushDebounce below has a delay, we need to keep checking and cancel a future debounce + // if we become unready for push during the delay. + cancelLoad(); + return; + } + + if (pushDebounce != null) + return; + + pushDebounce = Scheduler.AddDelayed(() => + { + contentOut(); + + this.Delay(250).Schedule(() => + { + if (!IsCurrentScreen) return; + + loadTask = null; + + if (!Push(player)) + Exit(); + else + { + //By default, we want to load the player and never be returned to. + //Note that this may change if the player we load requested a re-run. + ValidForResume = false; + } + }); + }, 500); + } + finally { Schedule(pushWhenLoaded); - return; } + } - contentOut(); + private void cancelLoad() + { + pushDebounce?.Cancel(); + pushDebounce = null; + } - this.Delay(250).Schedule(() => - { - if (!IsCurrentScreen) return; - - if (!Push(player)) - Exit(); - else - { - //By default, we want to load the player and never be returned to. - //Note that this may change if the player we load requested a re-run. - ValidForResume = false; - } - }); + protected override void OnSuspending(Screen next) + { + base.OnSuspending(next); + cancelLoad(); } protected override bool OnExiting(Screen next) { Content.ScaleTo(0.7f, 150, Easing.InQuint); this.FadeOut(150); + cancelLoad(); return base.OnExiting(next); } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + // if the player never got pushed, we should explicitly dispose it. + loadTask?.ContinueWith(_ => player.Dispose()); + } + private class BeatmapMetadataDisplay : Container { private class MetadataLine : Container diff --git a/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs index e8a4bc6b27..e0de89535e 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -31,6 +32,28 @@ namespace osu.Game.Screens.Play.PlayerSettings private bool expanded = true; + public bool Expanded + { + get { return expanded; } + set + { + if (expanded == value) return; + expanded = value; + + content.ClearTransforms(); + + if (expanded) + content.AutoSizeAxes = Axes.Y; + else + { + content.AutoSizeAxes = Axes.None; + content.ResizeHeightTo(0, transition_duration, Easing.OutQuint); + } + + button.FadeColour(expanded ? buttonActiveColour : Color4.White, 200, Easing.OutQuint); + } + } + private Color4 buttonActiveColour; protected PlayerSettingsGroup() @@ -82,7 +105,7 @@ namespace osu.Game.Screens.Play.PlayerSettings Position = new Vector2(-15, 0), Icon = FontAwesome.fa_bars, Scale = new Vector2(0.75f), - Action = toggleContentVisibility, + Action = () => Expanded = !Expanded, }, } }, @@ -112,21 +135,7 @@ namespace osu.Game.Screens.Play.PlayerSettings protected override Container Content => content; - private void toggleContentVisibility() - { - content.ClearTransforms(); - - expanded = !expanded; - - if (expanded) - content.AutoSizeAxes = Axes.Y; - else - { - content.AutoSizeAxes = Axes.None; - content.ResizeHeightTo(0, transition_duration, Easing.OutQuint); - } - - button.FadeColour(expanded ? buttonActiveColour : Color4.White, 200, Easing.OutQuint); - } + protected override bool OnHover(InputState state) => true; + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => true; } } diff --git a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs index 1a7b80ec9a..6c4d929c71 100644 --- a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs @@ -15,7 +15,6 @@ namespace osu.Game.Screens.Play.PlayerSettings private readonly PlayerSliderBar dimSliderBar; private readonly PlayerSliderBar blurSliderBar; private readonly PlayerCheckbox showStoryboardToggle; - private readonly PlayerCheckbox mouseWheelDisabledToggle; public VisualSettings() { @@ -35,8 +34,7 @@ namespace osu.Game.Screens.Play.PlayerSettings { Text = "Toggles:" }, - showStoryboardToggle = new PlayerCheckbox { LabelText = "Storyboards" }, - mouseWheelDisabledToggle = new PlayerCheckbox { LabelText = "Disable mouse wheel" } + showStoryboardToggle = new PlayerCheckbox { LabelText = "Storyboards" } }; } @@ -46,7 +44,6 @@ namespace osu.Game.Screens.Play.PlayerSettings dimSliderBar.Bindable = config.GetBindable(OsuSetting.DimLevel); blurSliderBar.Bindable = config.GetBindable(OsuSetting.BlurLevel); showStoryboardToggle.Bindable = config.GetBindable(OsuSetting.ShowStoryboard); - mouseWheelDisabledToggle.Bindable = config.GetBindable(OsuSetting.MouseDisableWheel); } } } diff --git a/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs b/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs new file mode 100644 index 0000000000..964267cc17 --- /dev/null +++ b/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs @@ -0,0 +1,63 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Screens; +using osu.Game.Configuration; +using osu.Game.Screens.Backgrounds; +using OpenTK; + +namespace osu.Game.Screens.Play +{ + public abstract class ScreenWithBeatmapBackground : OsuScreen + { + protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap); + + public override bool AllowBeatmapRulesetChange => false; + + protected const float BACKGROUND_FADE_DURATION = 800; + + protected float BackgroundOpacity => 1 - (float)DimLevel; + + #region User Settings + + protected Bindable DimLevel; + protected Bindable BlurLevel; + protected Bindable ShowStoryboard; + + #endregion + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + DimLevel = config.GetBindable(OsuSetting.DimLevel); + BlurLevel = config.GetBindable(OsuSetting.BlurLevel); + ShowStoryboard = config.GetBindable(OsuSetting.ShowStoryboard); + } + + protected override void OnEntering(Screen last) + { + base.OnEntering(last); + DimLevel.ValueChanged += _ => UpdateBackgroundElements(); + BlurLevel.ValueChanged += _ => UpdateBackgroundElements(); + ShowStoryboard.ValueChanged += _ => UpdateBackgroundElements(); + UpdateBackgroundElements(); + } + + protected override void OnResuming(Screen last) + { + base.OnResuming(last); + UpdateBackgroundElements(); + } + + protected virtual void UpdateBackgroundElements() + { + if (!IsCurrentScreen) return; + + Background?.FadeTo(BackgroundOpacity, BACKGROUND_FADE_DURATION, Easing.OutQuint); + (Background as BackgroundScreenBeatmap)?.BlurTo(new Vector2((float)BlurLevel.Value * 25), BACKGROUND_FADE_DURATION, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Screens/Play/SkipButton.cs b/osu.Game/Screens/Play/SkipOverlay.cs similarity index 86% rename from osu.Game/Screens/Play/SkipButton.cs rename to osu.Game/Screens/Play/SkipOverlay.cs index f67a9b801e..19ee0cb989 100644 --- a/osu.Game/Screens/Play/SkipButton.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -21,10 +21,12 @@ using osu.Game.Input.Bindings; namespace osu.Game.Screens.Play { - public class SkipButton : OverlayContainer, IKeyBindingHandler + public class SkipOverlay : OverlayContainer, IKeyBindingHandler { private readonly double startTime; - public IAdjustableClock AudioClock; + + public IAdjustableClock AdjustableClock; + public IFrameBasedClock FramedClock; private Button button; private Box remainingTimeBox; @@ -33,8 +35,9 @@ namespace osu.Game.Screens.Play private double displayTime; public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => true; + protected override bool BlockPassThroughMouse => false; - public SkipButton(double startTime) + public SkipOverlay(double startTime) { this.startTime = startTime; @@ -49,19 +52,16 @@ namespace osu.Game.Screens.Play Origin = Anchor.Centre; } - protected override bool OnMouseMove(InputState state) - { - fadeContainer.State = Visibility.Visible; - return base.OnMouseMove(state); - } - [BackgroundDependencyLoader] private void load(OsuColour colours) { var baseClock = Clock; - if (AudioClock != null) - Clock = new FramedClock(AudioClock) { ProcessSourceClockFrames = false }; + if (FramedClock != null) + { + Clock = FramedClock; + ProcessCustomClock = false; + } Children = new Drawable[] { @@ -109,22 +109,16 @@ namespace osu.Game.Screens.Play using (BeginAbsoluteSequence(beginFadeTime)) this.FadeOut(fade_time); - button.Action = () => AudioClock?.Seek(startTime - skip_required_cutoff - fade_time); + button.Action = () => AdjustableClock?.Seek(startTime - skip_required_cutoff - fade_time); displayTime = Time.Current; Expire(); } - protected override void PopIn() - { - this.FadeIn(); - } + protected override void PopIn() => this.FadeIn(); - protected override void PopOut() - { - this.FadeOut(); - } + protected override void PopOut() => this.FadeOut(); protected override void Update() { @@ -132,6 +126,13 @@ namespace osu.Game.Screens.Play remainingTimeBox.ResizeWidthTo((float)Math.Max(0, 1 - (Time.Current - displayTime) / (beginFadeTime - displayTime)), 120, Easing.OutQuint); } + protected override bool OnMouseMove(InputState state) + { + if (!state.Mouse.HasAnyButtonPressed) + fadeContainer.State = Visibility.Visible; + return base.OnMouseMove(state); + } + public bool OnPressed(GlobalAction action) { switch (action) @@ -171,7 +172,7 @@ namespace osu.Game.Screens.Play if (stateChanged) this.FadeIn(500, Easing.OutExpo); - if (!IsHovered) + if (!IsHovered && !IsDragged) using (BeginDelayedSequence(1000)) scheduledHide = Schedule(() => State = Visibility.Hidden); break; @@ -189,6 +190,18 @@ namespace osu.Game.Screens.Play base.LoadComplete(); State = Visibility.Visible; } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + scheduledHide?.Cancel(); + return base.OnMouseDown(state, args); + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + State = Visibility.Visible; + return base.OnMouseUp(state, args); + } } private class Button : OsuClickableContainer @@ -269,7 +282,7 @@ namespace osu.Game.Screens.Play flow.TransformSpacingTo(new Vector2(5), 500, Easing.OutQuint); box.FadeColour(colourHover, 500, Easing.OutQuint); background.FadeTo(0.4f, 500, Easing.OutQuint); - return base.OnHover(state); + return true; } protected override void OnHoverLost(InputState state) diff --git a/osu.Game/Screens/Play/SongProgressBar.cs b/osu.Game/Screens/Play/SongProgressBar.cs index ffe7ae04f8..4f5cc79b53 100644 --- a/osu.Game/Screens/Play/SongProgressBar.cs +++ b/osu.Game/Screens/Play/SongProgressBar.cs @@ -109,7 +109,7 @@ namespace osu.Game.Screens.Play { var xFill = value * UsableWidth; fill.Width = xFill; - handleBase.MoveToX(xFill); + handleBase.X = xFill; } protected override void OnUserChange() => OnSeek?.Invoke(Current); diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 6a6042d7d4..aed8fb110f 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -169,20 +169,43 @@ namespace osu.Game.Screens.Select }); } - public void SelectBeatmap(BeatmapInfo beatmap) + /// + /// Selects a given beatmap on the carousel. + /// + /// If bypassFilters is false, we will try to select another unfiltered beatmap in the same set. If the + /// entire set is filtered, no selection is made. + /// + /// The beatmap to select. + /// Whether to select the beatmap even if it is filtered (i.e., not visible on carousel). + /// True if a selection was made, False if it wasn't. + public bool SelectBeatmap(BeatmapInfo beatmap, bool bypassFilters = true) { if (beatmap?.Hidden != false) - return; + return false; - foreach (CarouselBeatmapSet group in beatmapSets) + foreach (CarouselBeatmapSet set in beatmapSets) { - var item = group.Beatmaps.FirstOrDefault(p => p.Beatmap.Equals(beatmap)); + if (!bypassFilters && set.Filtered) + continue; + + var item = set.Beatmaps.FirstOrDefault(p => p.Beatmap.Equals(beatmap)); + + if (item == null) + // The beatmap that needs to be selected doesn't exist in this set + continue; + + if (!bypassFilters && item.Filtered) + // The beatmap exists in this set but is filtered, so look for the first unfiltered map in the set + item = set.Beatmaps.FirstOrDefault(b => !b.Filtered); + if (item != null) { select(item); - return; + return true; } } + + return false; } /// @@ -192,7 +215,9 @@ namespace osu.Game.Screens.Select /// Whether to skip individual difficulties and only increment over full groups. public void SelectNext(int direction = 1, bool skipDifficulties = true) { - if (!Items.Any()) + var visibleItems = Items.Where(s => !s.Item.Filtered).ToList(); + + if (!visibleItems.Any()) return; DrawableCarouselItem drawable = null; @@ -202,15 +227,15 @@ namespace osu.Game.Screens.Select // we can fix this by changing this method to not reference drawables / Items in the first place. return; - int originalIndex = Items.IndexOf(drawable); + int originalIndex = visibleItems.IndexOf(drawable); int currentIndex = originalIndex; // local function to increment the index in the required direction, wrapping over extremities. - int incrementIndex() => currentIndex = (currentIndex + direction + Items.Count) % Items.Count; + int incrementIndex() => currentIndex = (currentIndex + direction + visibleItems.Count) % visibleItems.Count; while (incrementIndex() != originalIndex) { - var item = Items[currentIndex].Item; + var item = visibleItems[currentIndex].Item; if (item.Filtered || item.State == CarouselItemState.Selected) continue; @@ -303,7 +328,10 @@ namespace osu.Game.Screens.Select public void FlushPendingFilterOperations() { if (FilterTask?.Completed == false) + { applyActiveCriteria(false, false); + Update(); + } } public void Filter(FilterCriteria newCriteria, bool debounce = true) @@ -407,12 +435,14 @@ namespace osu.Game.Screens.Select continue; } + float depth = i + (item is DrawableCarouselBeatmapSet ? -Items.Count : 0); + // Only add if we're not already part of the content. if (!scrollableContent.Contains(item)) { // Makes sure headers are always _below_ items, // and depth flows downward. - item.Depth = i + (item is DrawableCarouselBeatmapSet ? -Items.Count : 0); + item.Depth = depth; switch (item.LoadState) { @@ -426,6 +456,10 @@ namespace osu.Game.Screens.Select break; } } + else + { + scrollableContent.ChangeChildDepth(item, depth); + } } // this is not actually useful right now, but once we have groups may well be. diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index d8cfd79e12..5204b7d787 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -25,10 +25,10 @@ namespace osu.Game.Screens.Select.Carousel { public class DrawableCarouselBeatmapSet : DrawableCarouselItem, IHasContextMenu { - private Action deleteRequested; private Action restoreHiddenRequested; private Action viewDetails; + private DialogOverlay dialogOverlay; private readonly BeatmapSetInfo beatmapSet; public DrawableCarouselBeatmapSet(CarouselBeatmapSet set) @@ -38,13 +38,13 @@ namespace osu.Game.Screens.Select.Carousel } [BackgroundDependencyLoader(true)] - private void load(LocalisationEngine localisation, BeatmapManager manager, BeatmapSetOverlay beatmapOverlay) + private void load(LocalisationEngine localisation, BeatmapManager manager, BeatmapSetOverlay beatmapOverlay, DialogOverlay overlay) { if (localisation == null) throw new ArgumentNullException(nameof(localisation)); restoreHiddenRequested = s => s.Beatmaps.ForEach(manager.Restore); - deleteRequested = manager.Delete; + dialogOverlay = overlay; if (beatmapOverlay != null) viewDetails = beatmapOverlay.ShowBeatmapSet; @@ -104,7 +104,7 @@ namespace osu.Game.Screens.Select.Carousel if (beatmapSet.Beatmaps.Any(b => b.Hidden)) items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested?.Invoke(beatmapSet))); - items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => deleteRequested?.Invoke(beatmapSet))); + items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new BeatmapDeleteDialog(beatmapSet)))); return items.ToArray(); } diff --git a/osu.Game/Screens/Select/Footer.cs b/osu.Game/Screens/Select/Footer.cs index 21e6108489..be83d7b500 100644 --- a/osu.Game/Screens/Select/Footer.cs +++ b/osu.Game/Screens/Select/Footer.cs @@ -41,19 +41,25 @@ namespace osu.Game.Screens.Select /// Higher depth to be put on the left, and lower to be put on the right. /// Notice this is different to ! /// - public void AddButton(string text, Color4 colour, Action action, Key? hotkey = null, float depth = 0) => buttons.Add(new FooterButton + public void AddButton(string text, Color4 colour, Action action, Key? hotkey = null, float depth = 0) { - Text = text, - Height = play_song_select_button_height, - Width = play_song_select_button_width, - Depth = depth, - SelectedColour = colour, - DeselectedColour = colour.Opacity(0.5f), - Hotkey = hotkey, - Hovered = updateModeLight, - HoverLost = updateModeLight, - Action = action, - }); + var button = new FooterButton + { + Text = text, + Height = play_song_select_button_height, + Width = play_song_select_button_width, + Depth = depth, + SelectedColour = colour, + DeselectedColour = colour.Opacity(0.5f), + Hotkey = hotkey, + Hovered = updateModeLight, + HoverLost = updateModeLight, + Action = action, + }; + + buttons.Add(button); + buttons.SetLayoutPosition(button, -depth); + } private readonly List overlays = new List(); diff --git a/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs b/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs index 6be6523175..273cceeeda 100644 --- a/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/Leaderboard.cs @@ -133,7 +133,7 @@ namespace osu.Game.Screens.Select.Leaderboards replacePlaceholder(new MessagePlaceholder(@"No records yet!")); break; case PlaceholderState.NotLoggedIn: - replacePlaceholder(new MessagePlaceholder(@"Please login to view online leaderboards!")); + replacePlaceholder(new MessagePlaceholder(@"Please sign in to view online leaderboards!")); break; case PlaceholderState.NotSupporter: replacePlaceholder(new MessagePlaceholder(@"Please invest in a supporter tag to view this leaderboard!")); diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs index 2e8b2f9014..dee1ec4511 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs @@ -95,7 +95,7 @@ namespace osu.Game.Screens.Select.Options /// public void AddButton(string firstLine, string secondLine, FontAwesome icon, Color4 colour, Action action, Key? hotkey = null, float depth = 0) { - buttonsContainer.Add(new BeatmapOptionsButton + var button = new BeatmapOptionsButton { FirstLineText = firstLine, SecondLineText = secondLine, @@ -108,7 +108,10 @@ namespace osu.Game.Screens.Select.Options action?.Invoke(); }, HotKey = hotkey - }); + }; + + buttonsContainer.Add(button); + buttonsContainer.SetLayoutPosition(button, depth); } } } diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 739bc39269..09524d2eac 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -1,11 +1,13 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Collections.Generic; using System.Linq; using OpenTK.Input; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; @@ -47,13 +49,15 @@ namespace osu.Game.Screens.Select private SampleChannel sampleConfirm; - [BackgroundDependencyLoader(true)] - private void load(OsuColour colours, AudioManager audio, BeatmapManager beatmaps, DialogOverlay dialogOverlay, OsuGame game) - { - sampleConfirm = audio.Sample.Get(@"SongSelect/confirm-selection"); + public readonly Bindable> SelectedMods = new Bindable>(new List()); - if (game != null) - modSelect.SelectedMods.BindTo(game.SelectedMods); + [BackgroundDependencyLoader(true)] + private void load(OsuColour colours, AudioManager audio, BeatmapManager beatmaps, DialogOverlay dialogOverlay, OsuGame osu) + { + if (osu != null) SelectedMods.BindTo(osu.SelectedMods); + modSelect.SelectedMods.BindTo(SelectedMods); + + sampleConfirm = audio.Sample.Get(@"SongSelect/confirm-selection"); Footer.AddButton(@"mods", colours.Yellow, modSelect, Key.F1, float.MaxValue); @@ -80,7 +84,7 @@ namespace osu.Game.Screens.Select { base.UpdateBeatmap(beatmap); - beatmap.Mods.BindTo(modSelect.SelectedMods); + beatmap.Mods.BindTo(SelectedMods); BeatmapDetails.Beatmap = beatmap; @@ -95,7 +99,7 @@ namespace osu.Game.Screens.Select if (removeAutoModOnResume) { var autoType = Ruleset.Value.CreateInstance().GetAutoplayMod().GetType(); - modSelect.SelectedMods.Value = modSelect.SelectedMods.Value.Where(m => m.GetType() != autoType).ToArray(); + modSelect.DeselectTypes(new[] { autoType }, true); removeAutoModOnResume = false; } @@ -125,7 +129,7 @@ namespace osu.Game.Screens.Select if (Beatmap.Value.Track != null) Beatmap.Value.Track.Looping = false; - Beatmap.Value.Mods.UnbindBindings(); + SelectedMods.UnbindAll(); Beatmap.Value.Mods.Value = new Mod[] { }; return false; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 2421a4fdfe..f01616ade2 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -197,8 +197,8 @@ namespace osu.Game.Screens.Select if (osu != null) Ruleset.BindTo(osu.Ruleset); - this.beatmaps.BeatmapSetAdded += onBeatmapSetAdded; - this.beatmaps.BeatmapSetRemoved += onBeatmapSetRemoved; + this.beatmaps.ItemAdded += onBeatmapSetAdded; + this.beatmaps.ItemRemoved += onBeatmapSetRemoved; this.beatmaps.BeatmapHidden += onBeatmapHidden; this.beatmaps.BeatmapRestored += onBeatmapRestored; @@ -214,11 +214,7 @@ namespace osu.Game.Screens.Select Beatmap.DisabledChanged += disabled => Carousel.AllowSelection = !disabled; Beatmap.TriggerChange(); - Beatmap.ValueChanged += b => - { - if (IsCurrentScreen) - Carousel.SelectBeatmap(b?.BeatmapInfo); - }; + Beatmap.ValueChanged += workingBeatmapChanged; } public void Edit(BeatmapInfo beatmap) @@ -261,6 +257,19 @@ namespace osu.Game.Screens.Select // We need to keep track of the last selected beatmap ignoring debounce to play the correct selection sounds. private BeatmapInfo beatmapNoDebounce; + private void workingBeatmapChanged(WorkingBeatmap beatmap) + { + if (beatmap is DummyWorkingBeatmap) return; + + if (IsCurrentScreen && !Carousel.SelectBeatmap(beatmap?.BeatmapInfo, false)) + // If selecting new beatmap without bypassing filters failed, there's possibly a ruleset mismatch + if (beatmap?.BeatmapInfo?.Ruleset != null && beatmap.BeatmapInfo.Ruleset != Ruleset.Value) + { + Ruleset.Value = beatmap.BeatmapInfo.Ruleset; + Carousel.SelectBeatmap(beatmap.BeatmapInfo); + } + } + /// /// selection has been changed as the result of interaction with the carousel. /// @@ -386,6 +395,8 @@ namespace osu.Game.Screens.Select protected override bool OnExiting(Screen next) { + FinaliseSelection(); + beatmapInfoWedge.State = Visibility.Hidden; Content.FadeOut(100); @@ -401,8 +412,8 @@ namespace osu.Game.Screens.Select if (beatmaps != null) { - beatmaps.BeatmapSetAdded -= onBeatmapSetAdded; - beatmaps.BeatmapSetRemoved -= onBeatmapSetRemoved; + beatmaps.ItemAdded -= onBeatmapSetAdded; + beatmaps.ItemRemoved -= onBeatmapSetRemoved; beatmaps.BeatmapHidden -= onBeatmapHidden; beatmaps.BeatmapRestored -= onBeatmapRestored; } @@ -448,16 +459,14 @@ namespace osu.Game.Screens.Select private void carouselBeatmapsLoaded() { - if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false) + if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false && Beatmap.Value.BeatmapSetInfo?.Protected == false && Carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo, false)) + return; + + if (Carousel.SelectedBeatmapSet == null && !Carousel.SelectNextRandom()) { - Carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo); - } - else if (Carousel.SelectedBeatmapSet == null) - { - if (!Carousel.SelectNextRandom()) - // in the case random selection failed, we want to trigger selectionChanged - // to show the dummy beatmap (we have nothing else to display). - carouselSelectionChanged(null); + // in the case random selection failed, we want to trigger selectionChanged + // to show the dummy beatmap (we have nothing else to display). + carouselSelectionChanged(null); } } diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs new file mode 100644 index 0000000000..7422ae2e47 --- /dev/null +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -0,0 +1,34 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Textures; +using OpenTK.Graphics; + +namespace osu.Game.Skinning +{ + public class DefaultSkin : Skin + { + public DefaultSkin() + : base(SkinInfo.Default) + { + Configuration = new SkinConfiguration + { + ComboColours = + { + new Color4(17, 136, 170, 255), + new Color4(102, 136, 0, 255), + new Color4(204, 102, 0, 255), + new Color4(121, 9, 13, 255) + } + }; + } + + public override Drawable GetDrawableComponent(string componentName) => null; + + public override Texture GetTexture(string componentName) => null; + + public override SampleChannel GetSample(string sampleName) => null; + } +} diff --git a/osu.Game/Skinning/ISkinSource.cs b/osu.Game/Skinning/ISkinSource.cs new file mode 100644 index 0000000000..d8f259b4ea --- /dev/null +++ b/osu.Game/Skinning/ISkinSource.cs @@ -0,0 +1,28 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Skinning +{ + /// + /// Provides access to skinnable elements. + /// + public interface ISkinSource + { + event Action SourceChanged; + + Drawable GetDrawableComponent(string componentName); + + Texture GetTexture(string componentName); + + SampleChannel GetSample(string sampleName); + + TValue GetValue(Func query) where TConfiguration : SkinConfiguration where TValue : class; + + TValue? GetValue(Func query) where TConfiguration : SkinConfiguration where TValue : struct; + } +} diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs new file mode 100644 index 0000000000..01beb8db32 --- /dev/null +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -0,0 +1,20 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Audio; +using osu.Framework.IO.Stores; +using osu.Game.Beatmaps; + +namespace osu.Game.Skinning +{ + public class LegacyBeatmapSkin : LegacySkin + { + public LegacyBeatmapSkin(BeatmapInfo beatmap, IResourceStore storage, AudioManager audioManager) + : base(createSkinInfo(beatmap), new LegacySkinResourceStore(beatmap.BeatmapSet, storage), audioManager, beatmap.Path) + { + } + + private static SkinInfo createSkinInfo(BeatmapInfo beatmap) => + new SkinInfo { Name = beatmap.ToString(), Creator = beatmap.Metadata.Author.ToString() }; + } +} diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs new file mode 100644 index 0000000000..1b52507688 --- /dev/null +++ b/osu.Game/Skinning/LegacySkin.cs @@ -0,0 +1,118 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.IO; +using System.Linq; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; +using osu.Game.Database; +using OpenTK; + +namespace osu.Game.Skinning +{ + public class LegacySkin : Skin + { + protected TextureStore Textures; + + protected SampleManager Samples; + + public LegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager) + : this(skin, new LegacySkinResourceStore(skin, storage), audioManager, "skin.ini") + { + } + + protected LegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager, string filename) : base(skin) + { + Stream stream = storage.GetStream(filename); + if (stream != null) + using (StreamReader reader = new StreamReader(stream)) + Configuration = new LegacySkinDecoder().Decode(reader); + else + Configuration = new SkinConfiguration(); + + Samples = audioManager.GetSampleManager(storage); + Textures = new TextureStore(new RawTextureLoaderStore(storage)); + } + + public override Drawable GetDrawableComponent(string componentName) + { + switch (componentName) + { + case "Play/Miss": + componentName = "hit0"; + break; + case "Play/Meh": + componentName = "hit50"; + break; + case "Play/Good": + componentName = "hit100"; + break; + case "Play/Great": + componentName = "hit300"; + break; + } + + float ratio = 0.72f; // brings sizing roughly in-line with stable + + var texture = GetTexture($"{componentName}@2x"); + if (texture == null) + { + ratio *= 2; + texture = GetTexture(componentName); + } + + if (texture == null) return null; + + return new Sprite + { + Texture = texture, + Scale = new Vector2(ratio), + }; + } + + public override Texture GetTexture(string componentName) => Textures.Get(componentName); + + public override SampleChannel GetSample(string sampleName) => Samples.Get(sampleName); + + protected class LegacySkinResourceStore : IResourceStore + where T : INamedFileInfo + { + private readonly IHasFiles source; + private readonly IResourceStore underlyingStore; + + private string getPathForFile(string filename) + { + bool hasExtension = filename.Contains('.'); + + string lastPiece = filename.Split('/').Last(); + + var file = source.Files.FirstOrDefault(f => + string.Equals(hasExtension ? f.Filename : Path.GetFileNameWithoutExtension(f.Filename), lastPiece, StringComparison.InvariantCultureIgnoreCase)); + return file?.FileInfo.StoragePath; + } + + public LegacySkinResourceStore(IHasFiles source, IResourceStore underlyingStore) + { + this.source = source; + this.underlyingStore = underlyingStore; + } + + public Stream GetStream(string name) + { + string path = getPathForFile(name); + return path == null ? null : underlyingStore.GetStream(path); + } + + byte[] IResourceStore.Get(string name) + { + string path = getPathForFile(name); + return path == null ? null : underlyingStore.Get(path); + } + } + } +} diff --git a/osu.Game/Skinning/LegacySkinDecoder.cs b/osu.Game/Skinning/LegacySkinDecoder.cs new file mode 100644 index 0000000000..853abceddf --- /dev/null +++ b/osu.Game/Skinning/LegacySkinDecoder.cs @@ -0,0 +1,38 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Beatmaps.Formats; + +namespace osu.Game.Skinning +{ + public class LegacySkinDecoder : LegacyDecoder + { + public LegacySkinDecoder() + : base(1) + { + } + + protected override void ParseLine(SkinConfiguration output, Section section, string line) + { + switch (section) + { + case Section.General: + var pair = SplitKeyVal(line); + + switch (pair.Key) + { + case @"Name": + output.SkinInfo.Name = pair.Value; + break; + case @"Author": + output.SkinInfo.Creator = pair.Value; + break; + } + + break; + } + + base.ParseLine(output, section, line); + } + } +} diff --git a/osu.Game/Skinning/LocalSkinOverrideContainer.cs b/osu.Game/Skinning/LocalSkinOverrideContainer.cs new file mode 100644 index 0000000000..95cdde2f48 --- /dev/null +++ b/osu.Game/Skinning/LocalSkinOverrideContainer.cs @@ -0,0 +1,79 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Allocation; +using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Skinning +{ + public class LocalSkinOverrideContainer : Container, ISkinSource + { + public event Action SourceChanged; + + public Drawable GetDrawableComponent(string componentName) => source.GetDrawableComponent(componentName) ?? fallbackSource?.GetDrawableComponent(componentName); + + public Texture GetTexture(string componentName) => source.GetTexture(componentName) ?? fallbackSource.GetTexture(componentName); + + public SampleChannel GetSample(string sampleName) => source.GetSample(sampleName) ?? fallbackSource?.GetSample(sampleName); + + public TValue? GetValue(Func query) where TConfiguration : SkinConfiguration where TValue : struct + { + TValue? val = null; + var conf = (source as Skin)?.Configuration as TConfiguration; + if (conf != null) + val = query?.Invoke(conf); + + return val ?? fallbackSource?.GetValue(query); + } + + public TValue GetValue(Func query) where TConfiguration : SkinConfiguration where TValue : class + { + TValue val = null; + var conf = (source as Skin)?.Configuration as TConfiguration; + if (conf != null) + val = query?.Invoke(conf); + + return val ?? fallbackSource?.GetValue(query); + } + + private readonly ISkinSource source; + private ISkinSource fallbackSource; + + public LocalSkinOverrideContainer(ISkinSource source) + { + this.source = source; + } + + private void onSourceChanged() => SourceChanged?.Invoke(); + + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); + + fallbackSource = dependencies.Get(); + dependencies.CacheAs(this); + + return dependencies; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (fallbackSource != null) + fallbackSource.SourceChanged += onSourceChanged; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (fallbackSource != null) + fallbackSource.SourceChanged -= onSourceChanged; + } + } +} diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs new file mode 100644 index 0000000000..02fb84a4a2 --- /dev/null +++ b/osu.Game/Skinning/Skin.cs @@ -0,0 +1,60 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Skinning +{ + public abstract class Skin : IDisposable, ISkinSource + { + public readonly SkinInfo SkinInfo; + + public virtual SkinConfiguration Configuration { get; protected set; } + + public event Action SourceChanged; + + public abstract Drawable GetDrawableComponent(string componentName); + + public abstract SampleChannel GetSample(string sampleName); + + public abstract Texture GetTexture(string componentName); + + public TValue GetValue(Func query) where TConfiguration : SkinConfiguration where TValue : class + => Configuration is TConfiguration conf ? query?.Invoke(conf) : null; + + public TValue? GetValue(Func query) where TConfiguration : SkinConfiguration where TValue : struct + => Configuration is TConfiguration conf ? query?.Invoke(conf) : null; + + protected Skin(SkinInfo skin) + { + SkinInfo = skin; + } + + #region Disposal + + ~Skin() + { + 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/Skinning/SkinConfiguration.cs b/osu.Game/Skinning/SkinConfiguration.cs new file mode 100644 index 0000000000..eac77ae753 --- /dev/null +++ b/osu.Game/Skinning/SkinConfiguration.cs @@ -0,0 +1,18 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Game.Beatmaps.Formats; +using OpenTK.Graphics; + +namespace osu.Game.Skinning +{ + public class SkinConfiguration : IHasComboColours, IHasCustomColours + { + public readonly SkinInfo SkinInfo = new SkinInfo(); + + public List ComboColours { get; set; } = new List(); + + public Dictionary CustomColours { get; set; } = new Dictionary(); + } +} diff --git a/osu.Game/Skinning/SkinFileInfo.cs b/osu.Game/Skinning/SkinFileInfo.cs new file mode 100644 index 0000000000..e8caf8f44a --- /dev/null +++ b/osu.Game/Skinning/SkinFileInfo.cs @@ -0,0 +1,25 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using osu.Game.Database; +using osu.Game.IO; + +namespace osu.Game.Skinning +{ + public class SkinFileInfo : INamedFileInfo + { + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int ID { get; set; } + + public int SkinInfoID { get; set; } + + public int FileInfoID { get; set; } + + public FileInfo FileInfo { get; set; } + + [Required] + public string Filename { get; set; } + } +} diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs new file mode 100644 index 0000000000..5080b65a37 --- /dev/null +++ b/osu.Game/Skinning/SkinInfo.cs @@ -0,0 +1,30 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations.Schema; +using osu.Game.Database; + +namespace osu.Game.Skinning +{ + public class SkinInfo : IHasFiles, IEquatable, IHasPrimaryKey, ISoftDelete + { + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public int ID { get; set; } + + public string Name { get; set; } + + public string Creator { get; set; } + + public List Files { get; set; } + + public bool DeletePending { get; set; } + + public static SkinInfo Default { get; } = new SkinInfo { Name = "osu!lazer", Creator = "team osu!" }; + + public bool Equals(SkinInfo other) => other != null && ID == other.ID; + + public override string ToString() => $"\"{Name}\" by {Creator}"; + } +} diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs new file mode 100644 index 0000000000..f965a77cce --- /dev/null +++ b/osu.Game/Skinning/SkinManager.cs @@ -0,0 +1,129 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Textures; +using osu.Framework.Platform; +using osu.Game.Database; +using osu.Game.IO.Archives; + +namespace osu.Game.Skinning +{ + public class SkinManager : ArchiveModelManager, ISkinSource + { + private readonly AudioManager audio; + + public readonly Bindable CurrentSkin = new Bindable(new DefaultSkin()); + public readonly Bindable CurrentSkinInfo = new Bindable(SkinInfo.Default) { Default = SkinInfo.Default }; + + public override string[] HandledExtensions => new[] { ".osk" }; + + /// + /// Returns a list of all usable s. + /// + /// A list of available . + public List GetAllUsableSkins() + { + var userSkins = ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList(); + userSkins.Insert(0, SkinInfo.Default); + return userSkins; + } + + protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo + { + Name = archive.Name + }; + + protected override void Populate(SkinInfo model, ArchiveReader archive) + { + base.Populate(model, archive); + populate(model); + } + + /// + /// Populate a from its (if possible). + /// + /// + private void populate(SkinInfo model) + { + Skin reference = GetSkin(model); + if (!string.IsNullOrEmpty(reference.Configuration.SkinInfo.Name)) + { + model.Name = reference.Configuration.SkinInfo.Name; + model.Creator = reference.Configuration.SkinInfo.Creator; + } + else + { + model.Name = model.Name.Replace(".osk", ""); + model.Creator = "Unknown"; + } + } + + /// + /// Retrieve a instance for the provided + /// + /// The skin to lookup. + /// A instance correlating to the provided . + public Skin GetSkin(SkinInfo skinInfo) + { + if (skinInfo == SkinInfo.Default) + return new DefaultSkin(); + + return new LegacySkin(skinInfo, Files.Store, audio); + } + + private SkinStore store; + + public SkinManager(Storage storage, DatabaseContextFactory contextFactory, IIpcHost importHost, AudioManager audio) + : base(storage, contextFactory, new SkinStore(contextFactory, storage), importHost) + { + this.audio = audio; + + CurrentSkinInfo.ValueChanged += info => CurrentSkin.Value = GetSkin(info); + CurrentSkin.ValueChanged += skin => + { + if (skin.SkinInfo != CurrentSkinInfo.Value) + throw new InvalidOperationException($"Setting {nameof(CurrentSkin)}'s value directly is not supported. Use {nameof(CurrentSkinInfo)} instead."); + + SourceChanged?.Invoke(); + }; + + // migrate older imports which didn't have access to skin.ini + using (ContextFactory.GetForWrite()) + { + foreach (var skinInfo in ModelStore.ConsumableItems.Where(s => s.Name.EndsWith(".osk"))) + { + populate(skinInfo); + Update(skinInfo); + } + } + } + + /// + /// Perform a lookup query on available s. + /// + /// The query. + /// The first result for the provided query, or null if no results were found. + public SkinInfo Query(Expression> query) => ModelStore.ConsumableItems.AsNoTracking().FirstOrDefault(query); + + public event Action SourceChanged; + + public Drawable GetDrawableComponent(string componentName) => CurrentSkin.Value.GetDrawableComponent(componentName); + + public Texture GetTexture(string componentName) => CurrentSkin.Value.GetTexture(componentName); + + public SampleChannel GetSample(string sampleName) => CurrentSkin.Value.GetSample(sampleName); + + public TValue GetValue(Func query) where TConfiguration : SkinConfiguration where TValue : class => CurrentSkin.Value.GetValue(query); + + public TValue? GetValue(Func query) where TConfiguration : SkinConfiguration where TValue : struct => CurrentSkin.Value.GetValue(query); + } +} diff --git a/osu.Game/Skinning/SkinReloadableDrawable.cs b/osu.Game/Skinning/SkinReloadableDrawable.cs new file mode 100644 index 0000000000..36f33e746a --- /dev/null +++ b/osu.Game/Skinning/SkinReloadableDrawable.cs @@ -0,0 +1,56 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Skinning +{ + /// + /// A drawable which has a callback when the skin changes. + /// + public abstract class SkinReloadableDrawable : CompositeDrawable + { + private readonly Func allowFallback; + private ISkinSource skin; + + /// + /// Whether fallback to default skin should be allowed if the custom skin is missing this resource. + /// + private bool allowDefaultFallback => allowFallback == null || allowFallback.Invoke(skin); + + /// + /// Create a new + /// + /// Whether fallback to default skin should be allowed if the custom skin is missing this resource. + protected SkinReloadableDrawable(Func allowFallback = null) + { + this.allowFallback = allowFallback; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource source) + { + skin = source; + skin.SourceChanged += onChange; + } + + private void onChange() => SkinChanged(skin, allowDefaultFallback); + + protected override void LoadAsyncComplete() + { + base.LoadAsyncComplete(); + onChange(); + } + + /// + /// Called when a change is made to the skin. + /// + /// The new skin. + /// Whether fallback to default skin should be allowed if the custom skin is missing this resource. + protected virtual void SkinChanged(ISkinSource skin, bool allowFallback) + { + } + } +} diff --git a/osu.Game/Skinning/SkinStore.cs b/osu.Game/Skinning/SkinStore.cs new file mode 100644 index 0000000000..ffd9873901 --- /dev/null +++ b/osu.Game/Skinning/SkinStore.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Linq; +using Microsoft.EntityFrameworkCore; +using osu.Framework.Platform; +using osu.Game.Database; + +namespace osu.Game.Skinning +{ + public class SkinStore : MutableDatabaseBackedStore + { + public SkinStore(DatabaseContextFactory contextFactory, Storage storage = null) + : base(contextFactory, storage) + { + } + + protected override IQueryable AddIncludesForConsumption(IQueryable query) => + base.AddIncludesForConsumption(query) + .Include(s => s.Files).ThenInclude(f => f.FileInfo); + } +} diff --git a/osu.Game/Skinning/SkinnableDrawable.cs b/osu.Game/Skinning/SkinnableDrawable.cs new file mode 100644 index 0000000000..09d2e6a3ed --- /dev/null +++ b/osu.Game/Skinning/SkinnableDrawable.cs @@ -0,0 +1,70 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Graphics; +using OpenTK; + +namespace osu.Game.Skinning +{ + public class SkinnableDrawable : SkinnableDrawable + { + public SkinnableDrawable(string name, Func defaultImplementation, Func allowFallback = null, bool restrictSize = true) + : base(name, defaultImplementation, allowFallback, restrictSize) + { + } + } + + public class SkinnableDrawable : SkinReloadableDrawable + where T : Drawable + { + private readonly Func createDefault; + + private readonly string componentName; + + private readonly bool restrictSize; + + /// + /// + /// + /// The namespace-complete resource name for this skinnable element. + /// A function to create the default skin implementation of this element. + /// Whther to fallback to the default implementation when a custom skin is specified but not implementation is present. + /// Whether a user-skin drawable should be limited to the size of our parent. + public SkinnableDrawable(string name, Func defaultImplementation, Func allowFallback = null, bool restrictSize = true) : base(allowFallback) + { + componentName = name; + createDefault = defaultImplementation; + this.restrictSize = restrictSize; + + RelativeSizeAxes = Axes.Both; + } + + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + var drawable = skin.GetDrawableComponent(componentName); + if (drawable != null) + { + if (restrictSize) + { + drawable.RelativeSizeAxes = Axes.Both; + drawable.Size = Vector2.One; + drawable.Scale = Vector2.One; + drawable.FillMode = FillMode.Fit; + } + } + else if (allowFallback) + drawable = createDefault(componentName); + + if (drawable != null) + { + drawable.Origin = Anchor.Centre; + drawable.Anchor = Anchor.Centre; + + InternalChild = drawable; + } + else + ClearInternal(); + } + } +} diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs new file mode 100644 index 0000000000..07c8fd3735 --- /dev/null +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -0,0 +1,62 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Audio; + +namespace osu.Game.Skinning +{ + public class SkinnableSound : SkinReloadableDrawable + { + private readonly SampleInfo[] samples; + private SampleChannel[] channels; + + private AudioManager audio; + + public SkinnableSound(params SampleInfo[] samples) + { + this.samples = samples; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + this.audio = audio; + } + + public void Play() => channels?.ForEach(c => c.Play()); + + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + channels = samples.Select(s => + { + var ch = loadChannel(s, skin.GetSample); + if (ch == null && allowFallback) + ch = loadChannel(s, audio.Sample.Get); + return ch; + }).Where(c => c != null).ToArray(); + } + + private SampleChannel loadChannel(SampleInfo info, Func getSampleFunction) + { + SampleChannel ch = null; + + if (info.Namespace != null) + ch = getSampleFunction($"Gameplay/{info.Namespace}/{info.Bank}-{info.Name}"); + + // try without namespace as a fallback. + if (ch == null) + ch = getSampleFunction($"Gameplay/{info.Bank}-{info.Name}"); + + if (ch != null) + ch.Volume.Value = info.Volume / 100.0; + + return ch; + } + } +} diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index 2489369493..9da92d8cb4 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -5,7 +5,6 @@ using OpenTK; 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.Game.IO; @@ -15,13 +14,6 @@ namespace osu.Game.Storyboards.Drawables { public Storyboard Storyboard { get; private set; } - private readonly Background background; - public Texture BackgroundTexture - { - get { return background.Texture; } - set { background.Texture = value; } - } - private readonly Container content; protected override Container Content => content; @@ -41,6 +33,8 @@ namespace osu.Game.Storyboards.Drawables } } + public override bool RemoveCompletedTransforms => false; + private DependencyContainer dependencies; protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); @@ -52,11 +46,6 @@ namespace osu.Game.Storyboards.Drawables Anchor = Anchor.Centre; Origin = Anchor.Centre; - AddInternal(background = new Background - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); AddInternal(content = new Container { Size = new Vector2(640, 480), @@ -79,10 +68,5 @@ namespace osu.Game.Storyboards.Drawables foreach (var layer in Children) layer.Enabled = passing ? layer.Layer.EnabledWhenPassing : layer.Layer.EnabledWhenFailing; } - - private class Background : Sprite - { - protected override Vector2 DrawScale => Texture != null ? new Vector2(Parent.DrawHeight / Texture.DisplayHeight) : base.DrawScale; - } } } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index ef782abbe5..0b84ff3297 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -17,6 +17,8 @@ namespace osu.Game.Storyboards.Drawables public bool FlipH { get; set; } public bool FlipV { get; set; } + public override bool RemoveWhenNotAlive => false; + protected override Vector2 DrawScale => new Vector2(FlipH ? -base.DrawScale.X : base.DrawScale.X, FlipV ? -base.DrawScale.Y : base.DrawScale.Y); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index a39805f74e..c4b9a3d47e 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -17,6 +17,8 @@ namespace osu.Game.Storyboards.Drawables public bool FlipH { get; set; } public bool FlipV { get; set; } + public override bool RemoveWhenNotAlive => false; + protected override Vector2 DrawScale => new Vector2(FlipH ? -base.DrawScale.X : base.DrawScale.X, FlipV ? -base.DrawScale.Y : base.DrawScale.Y); diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index e2587debc9..9d4efadc81 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -14,6 +14,8 @@ namespace osu.Game.Storyboards private readonly Dictionary layers = new Dictionary(); public IEnumerable Layers => layers.Values; + public BeatmapInfo BeatmapInfo = new BeatmapInfo(); + public bool HasDrawable => Layers.Any(l => l.Elements.Any(e => e.IsDrawable)); public Storyboard() @@ -36,28 +38,22 @@ namespace osu.Game.Storyboards /// /// Whether the beatmap's background should be hidden while this storyboard is being displayed. /// - public bool ReplacesBackground(BeatmapInfo beatmapInfo) + public bool ReplacesBackground { - var backgroundPath = beatmapInfo.BeatmapSet?.Metadata?.BackgroundFile?.ToLowerInvariant(); - if (backgroundPath == null) - return false; + get + { + var backgroundPath = BeatmapInfo.BeatmapSet?.Metadata?.BackgroundFile?.ToLowerInvariant(); + if (backgroundPath == null) + return false; - return GetLayer("Background").Elements.Any(e => e.Path.ToLowerInvariant() == backgroundPath); + return GetLayer("Background").Elements.Any(e => e.Path.ToLowerInvariant() == backgroundPath); + } } - public float AspectRatio(BeatmapInfo beatmapInfo) - => beatmapInfo.WidescreenStoryboard ? 16 / 9f : 4 / 3f; - public DrawableStoryboard CreateDrawable(WorkingBeatmap working = null) { var drawable = new DrawableStoryboard(this); - if (working != null) - { - var beatmapInfo = working.Beatmap.BeatmapInfo; - drawable.Width = drawable.Height * AspectRatio(beatmapInfo); - if (!ReplacesBackground(beatmapInfo)) - drawable.BackgroundTexture = working.Background; - } + drawable.Width = drawable.Height * (BeatmapInfo.WidescreenStoryboard ? 16 / 9f : 4 / 3f); return drawable; } diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs new file mode 100644 index 0000000000..8505498e4f --- /dev/null +++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs @@ -0,0 +1,144 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; +using Newtonsoft.Json; +using NUnit.Framework; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Tests.Beatmaps +{ + [TestFixture] + public abstract class BeatmapConversionTest + where TConvertValue : IEquatable + { + private const string resource_namespace = "Testing.Beatmaps"; + private const string expected_conversion_suffix = "-expected-conversion"; + + protected abstract string ResourceAssembly { get; } + + protected void Test(string name) + { + var ourResult = convert(name); + var expectedResult = read(name); + + Assert.Multiple(() => + { + int mappingCounter = 0; + while (true) + { + if (mappingCounter >= ourResult.Mappings.Count && mappingCounter >= expectedResult.Mappings.Count) + break; + if (mappingCounter >= ourResult.Mappings.Count) + Assert.Fail($"A conversion did not generate any hitobjects, but should have, for hitobject at time: {expectedResult.Mappings[mappingCounter].StartTime}\n"); + else if (mappingCounter >= expectedResult.Mappings.Count) + Assert.Fail($"A conversion generated hitobjects, but should not have, for hitobject at time: {ourResult.Mappings[mappingCounter].StartTime}\n"); + else + { + var counter = mappingCounter; + Assert.Multiple(() => + { + var ourMapping = ourResult.Mappings[counter]; + var expectedMapping = expectedResult.Mappings[counter]; + + int objectCounter = 0; + while (true) + { + if (objectCounter >= ourMapping.Objects.Count && objectCounter >= expectedMapping.Objects.Count) + break; + if (objectCounter >= ourMapping.Objects.Count) + Assert.Fail($"The conversion did not generate a hitobject, but should have, for hitobject at time: {expectedMapping.StartTime}:\n" + + $"Expected: {JsonConvert.SerializeObject(expectedMapping.Objects[objectCounter])}\n"); + else if (objectCounter >= expectedMapping.Objects.Count) + Assert.Fail($"The conversion generated a hitobject, but should not have, for hitobject at time: {ourMapping.StartTime}:\n" + + $"Received: {JsonConvert.SerializeObject(ourMapping.Objects[objectCounter])}\n"); + else if (!EqualityComparer.Default.Equals(expectedMapping.Objects[objectCounter], ourMapping.Objects[objectCounter])) + { + Assert.Fail($"The conversion generated differing hitobjects for object at time: {expectedMapping.StartTime}\n" + + $"Expected: {JsonConvert.SerializeObject(expectedMapping.Objects[objectCounter])}\n" + + $"Received: {JsonConvert.SerializeObject(ourMapping.Objects[objectCounter])}\n"); + } + + objectCounter++; + } + }); + } + + mappingCounter++; + } + }); + } + + private ConvertResult convert(string name) + { + var beatmap = getBeatmap(name); + + var result = new ConvertResult(); + + var converter = CreateConverter(beatmap); + converter.ObjectConverted += (orig, converted) => + { + converted.ForEach(h => h.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty)); + + var mapping = new ConvertMapping { StartTime = orig.StartTime }; + foreach (var obj in converted) + mapping.Objects.AddRange(CreateConvertValue(obj)); + result.Mappings.Add(mapping); + }; + + converter.Convert(beatmap); + + return result; + } + + private ConvertResult read(string name) + { + using (var resStream = openResource($"{resource_namespace}.{name}{expected_conversion_suffix}.json")) + using (var reader = new StreamReader(resStream)) + { + var contents = reader.ReadToEnd(); + return JsonConvert.DeserializeObject(contents); + } + } + + private Beatmap getBeatmap(string name) + { + using (var resStream = openResource($"{resource_namespace}.{name}.osu")) + using (var stream = new StreamReader(resStream)) + { + var decoder = Decoder.GetDecoder(stream); + ((LegacyBeatmapDecoder)decoder).ApplyOffsets = false; + return decoder.Decode(stream); + } + } + + private Stream openResource(string name) + { + var localPath = Path.GetDirectoryName(Uri.UnescapeDataString(new UriBuilder(Assembly.GetExecutingAssembly().CodeBase).Path)); + return Assembly.LoadFrom(Path.Combine(localPath, $"{ResourceAssembly}.dll")).GetManifestResourceStream($@"{ResourceAssembly}.Resources.{name}"); + } + + protected abstract IEnumerable CreateConvertValue(HitObject hitObject); + protected abstract IBeatmapConverter CreateConverter(Beatmap beatmap); + + private class ConvertMapping + { + [JsonProperty] + public double StartTime; + [JsonProperty] + public List Objects = new List(); + } + + private class ConvertResult + { + [JsonProperty] + public List Mappings = new List(); + } + } +} diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs new file mode 100644 index 0000000000..7dc6079959 --- /dev/null +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs @@ -0,0 +1,720 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.IO; +using System.Text; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using Decoder = osu.Game.Beatmaps.Formats.Decoder; + +namespace osu.Game.Tests.Beatmaps +{ + public class TestBeatmap : Beatmap + { + public TestBeatmap(RulesetInfo ruleset) + : base(createTestBeatmap()) + { + BeatmapInfo.Ruleset = ruleset; + } + + private static Beatmap createTestBeatmap() + { + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(test_beatmap_data))) + using (var reader = new StreamReader(stream)) + return Decoder.GetDecoder(reader).Decode(reader); + } + + private const string test_beatmap_data = +@"osu file format v14 + +[General] +AudioLeadIn: 500 +PreviewTime: 53498 +Countdown: 0 +SampleSet: Soft +StackLeniency: 0.7 +Mode: 0 +LetterboxInBreaks: 0 +WidescreenStoryboard: 1 + +[Editor] +DistanceSpacing: 1.2 +BeatDivisor: 4 +GridSize: 4 +TimelineZoom: 1 + +[Metadata] +Title:My Love +TitleUnicode:My Love +Artist:Kuba Oms +ArtistUnicode:Kuba Oms +Creator:W h i t e +Version:Hard +Source:ADHD +Tags:Monthly Beatmapping Contest Electronic folk pop w_h_i_t_e +BeatmapID:397534 +BeatmapSetID:163112 + +[Difficulty] +HPDrainRate:5 +CircleSize:4 +OverallDifficulty:6 +ApproachRate:7 +SliderMultiplier:1.44 +SliderTickRate:2 + +[Events] +//Break Periods +2,69870,83770 +2,152170,158770 +//Storyboard Layer 0 (Background) +//Storyboard Layer 1 (Fail) +//Storyboard Layer 2 (Pass) +//Storyboard Layer 3 (Foreground) +//Storyboard Sound Samples + +[TimingPoints] +2170,468.75,4,2,0,40,1,0 +4045,-100,4,2,0,30,0,0 +4162,-100,4,2,0,40,0,0 +5920,-100,4,2,0,30,0,0 +6037,-100,4,2,0,40,0,0 +7795,-100,4,2,0,30,0,0 +7912,-100,4,2,0,40,0,0 +9670,-100,4,2,0,40,0,0 +9787,-100,4,2,0,50,0,0 +11545,-100,4,2,0,40,0,0 +11662,-100,4,2,0,50,0,0 +13420,-100,4,2,0,40,0,0 +13537,-100,4,2,0,50,0,0 +15295,-100,4,2,0,40,0,0 +15412,-100,4,2,0,50,0,0 +17170,-100,4,2,0,40,0,0 +17287,-100,4,2,0,50,0,0 +19045,-100,4,2,0,40,0,0 +19162,-100,4,2,0,50,0,0 +20920,-100,4,2,0,40,0,0 +21037,-100,4,2,0,50,0,0 +22795,-100,4,2,0,40,0,0 +22912,-100,4,2,0,50,0,0 +24670,-100,4,2,0,70,0,0 +37560,-200,4,2,0,30,0,0 +38263,-200,4,2,0,5,0,0 +38966,-100,4,2,0,30,0,0 +39670,-100,4,2,0,70,0,0 +53732,-100,4,2,0,40,0,0 +54670,-100,4,2,0,80,0,1 +55138,-100,4,2,0,60,0,1 +55255,-100,4,2,0,80,0,1 +56076,-100,4,2,0,60,0,1 +56193,-100,4,2,0,80,0,1 +57013,-100,4,2,0,60,0,1 +57130,-100,4,2,0,80,0,1 +57951,-100,4,2,0,60,0,1 +58068,-100,4,2,0,80,0,1 +58888,-100,4,2,0,60,0,1 +59005,-100,4,2,0,80,0,1 +59826,-100,4,2,0,60,0,1 +59943,-100,4,2,0,80,0,1 +60763,-100,4,2,0,60,0,1 +60880,-100,4,2,0,80,0,1 +61701,-100,4,2,0,60,0,1 +61818,-100,4,2,0,80,0,1 +62638,-100,4,2,0,60,0,1 +62755,-100,4,2,0,80,0,1 +63576,-100,4,2,0,60,0,1 +63693,-100,4,2,0,80,0,1 +64513,-100,4,2,0,60,0,1 +64630,-100,4,2,0,80,0,1 +65451,-100,4,2,0,60,0,1 +65568,-100,4,2,0,80,0,1 +66388,-100,4,2,0,60,0,1 +66505,-100,4,2,0,80,0,1 +67326,-100,4,2,0,60,0,1 +67443,-100,4,2,0,80,0,1 +68263,-100,4,2,0,60,0,1 +68380,-100,4,2,0,80,0,1 +69201,-100,4,2,0,60,0,1 +69318,-100,4,2,0,80,0,1 +69670,-100,4,2,0,70,0,0 +84670,-100,4,2,0,70,0,0 +97560,-200,4,2,0,70,0,0 +97795,-200,4,2,0,30,0,0 +98966,-100,4,2,0,30,0,0 +99670,-100,4,2,0,70,0,0 +113732,-100,4,2,0,40,0,0 +114670,-100,4,2,0,80,0,1 +115138,-100,4,2,0,60,0,1 +115255,-100,4,2,0,80,0,1 +116076,-100,4,2,0,60,0,1 +116193,-100,4,2,0,80,0,1 +117013,-100,4,2,0,60,0,1 +117130,-100,4,2,0,80,0,1 +117951,-100,4,2,0,60,0,1 +118068,-100,4,2,0,80,0,1 +118888,-100,4,2,0,60,0,1 +119005,-100,4,2,0,80,0,1 +119826,-100,4,2,0,60,0,1 +119943,-100,4,2,0,80,0,1 +120763,-100,4,2,0,60,0,1 +120880,-100,4,2,0,80,0,1 +121701,-100,4,2,0,60,0,1 +121818,-100,4,2,0,80,0,1 +122638,-100,4,2,0,60,0,1 +122755,-100,4,2,0,80,0,1 +123576,-100,4,2,0,60,0,1 +123693,-100,4,2,0,80,0,1 +124513,-100,4,2,0,60,0,1 +124630,-100,4,2,0,80,0,1 +125451,-100,4,2,0,60,0,1 +125568,-100,4,2,0,80,0,1 +126388,-100,4,2,0,60,0,1 +126505,-100,4,2,0,80,0,1 +127326,-100,4,2,0,60,0,1 +127443,-100,4,2,0,80,0,1 +128263,-100,4,2,0,60,0,1 +128380,-100,4,2,0,80,0,1 +129201,-100,4,2,0,60,0,1 +129318,-100,4,2,0,80,0,1 +129670,-200,4,2,0,40,0,0 +144670,-133.333333333333,4,2,0,40,0,0 +159670,-133.333333333333,4,2,0,40,0,0 +163420,-133.333333333333,4,2,0,45,0,0 +163888,-125,4,2,0,50,0,0 +164357,-117.647058823529,4,2,0,55,0,0 +164826,-111.111111111111,4,2,0,60,0,0 +165295,-105.263157894737,4,2,0,65,0,0 +165763,-100,4,2,0,70,0,0 +166232,-100,4,2,0,40,0,0 +167170,-100,4,2,0,80,0,1 +167638,-100,4,2,0,60,0,1 +167755,-100,4,2,0,80,0,1 +168576,-100,4,2,0,60,0,1 +168693,-100,4,2,0,80,0,1 +169513,-100,4,2,0,60,0,1 +169630,-100,4,2,0,80,0,1 +170451,-100,4,2,0,60,0,1 +170568,-100,4,2,0,80,0,1 +171388,-100,4,2,0,60,0,1 +171505,-100,4,2,0,80,0,1 +172326,-100,4,2,0,60,0,1 +172443,-100,4,2,0,80,0,1 +173263,-100,4,2,0,60,0,1 +173380,-100,4,2,0,80,0,1 +174201,-100,4,2,0,60,0,1 +174318,-100,4,2,0,80,0,1 +175138,-100,4,2,0,60,0,1 +175255,-100,4,2,0,80,0,1 +176076,-100,4,2,0,60,0,1 +176193,-100,4,2,0,80,0,1 +177013,-100,4,2,0,60,0,1 +177130,-100,4,2,0,80,0,1 +177951,-100,4,2,0,60,0,1 +178068,-100,4,2,0,80,0,1 +178888,-100,4,2,0,60,0,1 +179005,-100,4,2,0,80,0,1 +179826,-100,4,2,0,60,0,1 +179943,-100,4,2,0,80,0,1 +180763,-100,4,2,0,60,0,1 +180880,-100,4,2,0,80,0,1 +180998,-100,4,2,0,80,0,0 +181466,-100,4,2,0,60,0,0 +181584,-100,4,2,0,80,0,0 +181935,-100,4,2,0,80,0,0 +182170,-100,4,2,0,80,0,1 +182638,-100,4,2,0,60,0,1 +182755,-100,4,2,0,80,0,1 +183576,-100,4,2,0,60,0,1 +183693,-100,4,2,0,80,0,1 +184513,-100,4,2,0,60,0,1 +184630,-100,4,2,0,80,0,1 +185451,-100,4,2,0,60,0,1 +185568,-100,4,2,0,80,0,1 +186388,-100,4,2,0,60,0,1 +186505,-100,4,2,0,80,0,1 +187326,-100,4,2,0,60,0,1 +187443,-100,4,2,0,80,0,1 +188263,-100,4,2,0,60,0,1 +188380,-100,4,2,0,80,0,1 +189201,-100,4,2,0,60,0,1 +189318,-100,4,2,0,80,0,1 +190138,-100,4,2,0,60,0,1 +190255,-100,4,2,0,80,0,1 +191076,-100,4,2,0,60,0,1 +191193,-100,4,2,0,80,0,1 +192013,-100,4,2,0,60,0,1 +192130,-100,4,2,0,80,0,1 +192951,-100,4,2,0,60,0,1 +193068,-100,4,2,0,80,0,1 +193888,-100,4,2,0,60,0,1 +194005,-100,4,2,0,80,0,1 +194826,-100,4,2,0,60,0,1 +194943,-100,4,2,0,80,0,1 +195295,-100,4,2,0,50,0,1 +195529,-100,4,2,0,52,0,1 +195646,-100,4,2,0,54,0,1 +195763,-100,4,2,0,56,0,1 +195880,-100,4,2,0,58,0,1 +195998,-100,4,2,0,60,0,1 +196115,-100,4,2,0,62,0,1 +196232,-100,4,2,0,64,0,1 +196349,-100,4,2,0,68,0,1 +196466,-100,4,2,0,70,0,1 +196584,-100,4,2,0,72,0,1 +196701,-100,4,2,0,74,0,1 +196818,-100,4,2,0,76,0,1 +196935,-100,4,2,0,78,0,1 +197052,-100,4,2,0,80,0,1 +197170,-100,4,2,0,80,0,0 +197873,-100,4,2,0,60,0,0 +197990,-100,4,2,0,80,0,0 +198341,-100,4,2,0,60,0,0 +199045,-100,4,2,0,80,0,0 +199279,-100,4,2,0,60,0,0 +199630,-100,4,2,0,80,0,0 +200216,-100,4,2,0,60,0,0 +200334,-100,4,2,0,80,0,0 +201623,-100,4,2,0,60,0,0 +201740,-100,4,2,0,80,0,0 +202326,-100,4,2,0,60,0,0 +202443,-100,4,2,0,80,0,0 +203029,-100,4,2,0,60,0,0 +203498,-100,4,2,0,80,0,0 +203966,-100,4,2,0,60,0,0 +204201,-100,4,2,0,80,0,0 +205373,-100,4,2,0,60,0,0 +205490,-100,4,2,0,80,0,0 +205841,-100,4,2,0,60,0,0 +206076,-100,4,2,0,60,0,0 +206545,-100,4,2,0,80,0,0 +206779,-100,4,2,0,60,0,0 +207130,-100,4,2,0,80,0,0 +207716,-100,4,2,0,60,0,0 +207951,-100,4,2,0,80,0,0 +209123,-100,4,2,0,60,0,0 +209240,-100,4,2,0,80,0,0 +209826,-100,4,2,0,60,0,0 +209943,-100,4,2,0,80,0,0 +210529,-100,4,2,0,60,0,0 +210880,-100,4,2,0,80,0,0 +211232,-100,4,2,0,60,0,0 +211701,-100,4,2,0,70,0,0 +212170,-100,4,2,0,80,0,0 +212873,-100,4,2,0,60,0,0 +212990,-100,4,2,0,80,0,0 +213341,-100,4,2,0,60,0,0 +213576,-100,4,2,0,60,0,0 +214045,-100,4,2,0,80,0,0 +214279,-100,4,2,0,60,0,0 +214630,-100,4,2,0,80,0,0 +215216,-100,4,2,0,60,0,0 +215451,-100,4,2,0,80,0,0 +216623,-100,4,2,0,60,0,0 +216740,-100,4,2,0,80,0,0 +217326,-100,4,2,0,60,0,0 +217443,-100,4,2,0,80,0,0 +218029,-100,4,2,0,60,0,0 +218498,-100,4,2,0,80,0,0 +218732,-100,4,2,0,50,0,0 +219670,-100,4,2,0,70,0,0 +220138,-100,4,2,0,65,0,0 +220373,-100,4,2,0,45,0,0 +220490,-100,4,2,0,65,0,0 +220607,-100,4,2,0,60,0,0 +220841,-100,4,2,0,35,0,0 +221076,-100,4,2,0,35,0,0 +221545,-100,4,2,0,50,0,0 +221779,-100,4,2,0,30,0,0 +222013,-111.111111111111,4,2,0,25,0,0 +222130,-111.111111111111,4,2,0,40,0,0 +222482,-125,4,2,0,40,0,0 +222716,-125,4,2,0,20,0,0 +222951,-100,4,2,0,15,0,0 +223420,-100,4,2,0,30,0,0 +224357,-100,4,2,0,25,0,0 +225295,-100,4,2,0,20,0,0 +226232,-100,4,2,0,15,0,0 +226701,-100,4,2,0,10,0,0 +227170,-100,4,2,0,5,0,0 + +[Colours] +Combo1 : 17,254,176 +Combo2 : 173,255,95 +Combo3 : 255,88,100 +Combo4 : 255,94,55 + +[HitObjects] +320,256,2170,6,0,P|256:284|192:256,1,144,4|0,0:0|0:0,0:0:0:0: +144,184,2873,1,0,0:0:0:0: +108,260,3107,2,0,P|112:296|100:336,1,72 +28,288,3576,2,0,P|24:252|36:212,1,72,0|0,0:0|0:0,0:0:0:0: +76,140,4045,6,0,L|220:136,1,144,4|0,0:0|0:0,0:0:0:0: +292,88,4748,1,0,0:0:0:0: +292,88,4982,2,0,P|304:120|300:168,1,72 +388,168,5451,2,0,P|396:133|416:103,1,72,0|0,0:0|0:0,0:0:0:0: +472,172,5920,6,0,B|470:200|457:222|457:222|488:256|476:308,1,144,4|0,0:0|0:0,0:0:0:0: +396,280,6623,1,0,0:0:0:0: +324,328,6857,2,0,P|288:332|252:324,1,72 +180,280,7326,2,0,L|108:284,1,72,0|0,0:0|0:0,0:0:0:0: +256,192,7795,12,0,9670,0:0:0:0: +428,212,10138,1,0,0:0:0:0: +292,320,10607,1,0,0:0:0:0: +184,184,11076,2,0,L|112:180,1,72,0|0,0:0|0:0,0:0:0:0: +24,172,11545,5,6,0:0:0:0: +160,280,12013,1,0,0:0:0:0: +268,144,12482,1,0,0:0:0:0: +132,36,12951,2,0,L|204:32,1,72,0|0,0:0|0:0,0:0:0:0: +284,60,13420,6,0,P|340:100|344:180,2,144,6|0|0,0:0|0:0|0:0,0:0:0:0: +268,144,14591,1,0,0:0:0:0: +284,228,14826,2,0,P|316:248|364:252,1,72,0|0,0:0|0:0,0:0:0:0: +436,248,15295,6,0,P|372:272|344:340,1,144,6|2,0:0|0:0,0:0:0:0: +168,338,16232,2,0,P|141:273|76:248,1,144,2|2,0:0|0:0,0:0:0:0: +4,296,16935,1,0,0:0:0:0: +80,336,17170,5,6,0:0:0:0: +44,168,17638,1,0,0:0:0:0: +212,128,18107,1,0,0:0:0:0: +248,296,18576,2,0,P|284:288|320:292,1,72,0|0,0:0|0:0,0:0:0:0: +400,324,19045,5,6,0:0:0:0: +280,200,19513,1,0,0:0:0:0: +368,52,19982,1,0,0:0:0:0: +488,176,20451,2,0,P|452:168|416:172,1,72,0|0,0:0|0:0,0:0:0:0: +336,200,20920,6,0,P|284:216|200:192,1,144,6|0,0:0|0:0,0:0:0:0: +200,192,21857,2,0,L|204:264,1,72,0|0,0:3|0:0,0:0:0:0: +117,244,22326,2,0,L|120:172,1,72,0|0,0:0|0:0,0:0:0:0: +40,152,22795,6,0,L|28:296,2,144,6|0|0,0:0|0:0|0:0,0:0:0:0: +152,24,24201,1,0,0:0:0:0: +220,76,24435,1,0,3:0:0:0: +304,56,24670,6,0,P|288:120|296:196,1,144,4|2,0:3|0:3,0:0:0:0: +344,268,25373,1,0,0:0:0:0: +416,316,25607,2,0,P|452:312|508:316,2,72,0|0|2,0:0|0:0|0:3,0:0:0:0: +244,344,26545,6,0,P|176:356|108:328,1,144,4|2,0:3|0:3,0:0:0:0: +60,256,27248,1,0,0:0:0:0: +36,172,27482,2,0,L|40:100,2,72,0|0|2,0:0|0:0|0:3,0:0:0:0: +188,252,28420,6,0,P|192:184|196:100,1,144,4|2,0:3|0:3,0:0:0:0: +140,40,29123,1,0,0:0:0:0: +140,40,29357,2,0,B|172:16|220:24|220:24|288:36,1,144,0|2,0:0|0:3,0:0:0:0: +364,52,30060,1,0,0:0:0:0: +308,116,30295,6,0,B|300:168|300:168|328:256,1,144,4|2,0:3|0:3,0:0:0:0: +340,340,30998,1,0,0:0:0:0: +260,308,31232,2,0,L|188:304,1,72,0|2,0:0|0:3,0:0:0:0: +100,296,31701,1,2,0:3:0:0: +136,374,31935,1,0,0:0:0:0: +152,224,32170,6,0,P|160:152|132:88,1,144,4|2,0:3|0:3,0:0:0:0: +56,48,32873,1,0,0:0:0:0: +60,136,33107,2,0,L|56:208,2,72,0|0|2,0:0|0:0|0:3,0:0:0:0: +224,76,34045,6,0,P|289:104|360:96,1,144,4|2,0:3|0:3,0:0:0:0: +432,48,34748,1,0,0:0:0:0: +440,132,34982,2,0,B|432:156|432:156|436:204,2,72,0|0|2,0:0|0:0|0:3,0:0:0:0: +448,304,35920,6,0,B|412:315|380:292|380:292|348:269|312:280,1,144,4|2,0:3|0:3,0:0:0:0: +332,364,36623,1,0,0:0:0:0: +247,339,36857,2,0,P|230:308|225:273,2,72,0|0|2,0:0|0:0|0:3,0:0:0:0: +312,280,37560,6,0,L|316:172,1,108 +134,35,38966,5,0,0:0:0:0: +72,96,39201,2,0,P|119:119|171:111,1,108,0|0,0:0|0:0,0:0:0:0: +192,100,39670,6,0,L|200:172,1,72,4|2,0:0|0:0,0:0:0:0: +147,240,40138,2,0,P|133:272|132:308,1,72,0|2,1:0|0:0,0:0:0:0: +216,292,40607,2,0,B|260:308|260:308|356:292,1,144,4|0,2:3|1:0,1:0:0:0: +356,292,41310,1,2,0:0:0:0: +436,327,41545,6,0,P|441:292|435:257,1,72,4|2,0:3|0:0,0:0:0:0: +364,204,42013,2,0,P|336:144|352:68,1,144,0|4,1:0|2:3,1:0:0:0: +404,0,42716,1,2,0:0:0:0: +440,80,42951,2,0,B|464:84|464:84|512:80,1,72,0|2,1:0|0:0,0:0:0:0: +351,71,43420,6,0,B|296:68|296:68|268:76|268:76|196:72,1,144,4|0,2:3|1:0,1:0:0:0: +120,68,44123,1,2,0:0:0:0: +160,144,44357,2,0,P|172:180|168:232,1,72,4|2,0:3|0:0,0:0:0:0: +76,264,44826,2,0,P|76:228|88:194,1,72,0|2,1:0|0:0,0:0:0:0: +160,144,45295,5,4,0:3:0:0: +244,164,45529,1,2,0:0:0:0: +268,248,45763,2,0,L|344:252,1,72,0|2,1:0|0:0,0:0:0:0: +408,156,46232,2,0,L|336:159,1,72,4|2,0:3|0:0,0:0:0:0: +212,72,46701,2,0,L|288:76,1,72,0|2,1:0|0:0,0:0:0:0: +400,72,47170,6,0,P|464:96|488:172,1,144,4|0,2:0|1:0,1:0:0:0: +476,248,47873,1,2,0:0:0:0: +436,324,48107,2,0,L|284:320,1,144,4|0,2:3|1:0,1:0:0:0: +204,316,48810,1,2,0:0:0:0: +127,355,49045,6,0,P|120:321|124:285,1,72,4|2,0:3|0:0,0:0:0:0: +192,232,49513,2,0,L|335:228,1,144,0|4,1:0|2:3,1:0:0:0: +412,188,50216,1,2,0:0:0:0: +444,108,50451,2,0,P|452:72|448:36,1,72,0|2,1:0|0:0,0:0:0:0: +368,68,50920,6,0,B|332:79|300:56|300:56|268:33|232:44,1,144,4|0,2:3|1:0,1:0:0:0: +152,76,51623,1,2,0:0:0:0: +76,116,51857,2,0,L|80:268,1,144,4|0,2:3|1:0,1:0:0:0: +80,260,52560,1,2,0:0:0:0: +8,308,52795,6,0,P|34:334|69:346,1,72,4|2,0:3|0:0,0:0:0:0: +148,312,53263,2,0,P|163:278|162:241,1,72,0|2,1:0|0:0,0:0:0:0: +156,156,53732,5,0,3:0:0:0: +156,156,53966,1,2,0:0:0:0: +236,196,54201,2,0,L|312:192,1,72,8|0,0:3|0:0,0:0:0:0: +368,256,54670,6,0,P|392:216|352:116,1,144,4|2,0:0|1:2,0:0:0:0: +288,92,55373,1,0,0:0:0:0: +360,40,55607,2,0,L|432:36,1,72,4|0,0:3|3:0,0:0:0:0: +288,92,56076,2,0,L|216:88,1,72,2|0,1:2|0:0,0:0:0:0: +132,72,56545,6,0,P|172:88|200:184,1,144,4|2,0:3|1:2,0:0:0:0: +143,241,57248,1,0,0:0:0:0: +65,202,57482,2,0,P|87:174|119:157,1,72,4|0,0:3|3:0,0:0:0:0: +132,324,57951,2,0,P|98:312|72:288,1,72,2|0,1:2|0:0,0:0:0:0: +143,241,58420,6,0,L|288:240,1,144,4|2,0:3|1:2,0:0:0:0: +372,240,59123,1,0,0:0:0:0: +330,314,59357,2,0,P|318:350|322:390,1,72,4|0,0:3|3:0,0:0:0:0: +452,264,59826,2,0,P|453:228|442:194,1,72,2|0,1:2|0:0,0:0:0:0: +384,128,60295,6,0,B|336:144|336:144|244:128,1,144,4|2,0:3|1:2,0:0:0:0: +164,160,60998,2,0,P|160:116|168:88,1,72,0|4,0:0|0:3,0:0:0:0: +244,128,61466,2,0,P|248:172|240:200,1,72,0|2,3:0|1:2,0:0:0:0: +168,248,61935,1,0,0:0:0:0: +120,320,62170,6,0,P|196:328|252:272,2,144,4|2|4,0:3|1:2|0:3,0:0:0:0: +80,244,63341,1,0,3:0:0:0: +100,160,63576,2,0,L|24:156,1,72,2|0,1:2|0:0,0:0:0:0: +180,128,64045,6,0,P|249:138|304:94,1,144,4|2,0:3|1:2,0:0:0:0: +226,57,64748,1,0,0:0:0:0: +304,94,64982,2,0,L|300:166,1,72,4|0,0:3|3:0,0:0:0:0: +377,203,65451,2,0,L|388:132,1,72,2|0,1:2|0:0,0:0:0:0: +468,180,65920,6,0,L|432:328,1,144,4|2,0:3|1:2,0:0:0:0: +276,252,66857,2,0,P|208:248|140:280,1,144,4|2,0:3|1:2,0:0:0:0: +84,344,67560,1,0,0:0:0:0: +56,260,67795,6,0,L|52:188,2,72,4|2|2,0:3|0:0|1:2,0:0:0:0: +168,128,68732,2,0,L|172:56,2,72,4|2|2,0:3|0:0|1:2,0:0:0:0: +244,168,69435,1,0,0:0:0:0: +332,164,69670,1,4,0:3:0:0: +208,328,84670,6,0,P|224:264|216:188,1,144,4|2,0:3|0:3,0:0:0:0: +168,116,85373,1,0,0:0:0:0: +96,68,85607,2,0,P|60:72|4:68,2,72,0|0|2,0:0|0:0|0:3,0:0:0:0: +268,40,86545,6,0,P|336:28|404:56,1,144,4|2,0:3|0:3,0:0:0:0: +452,128,87248,1,0,0:0:0:0: +476,212,87482,2,0,L|472:284,2,72,0|0|2,0:0|0:0|0:3,0:0:0:0: +324,132,88420,6,0,P|320:200|316:284,1,144,4|2,0:3|0:3,0:0:0:0: +372,344,89123,1,0,0:0:0:0: +372,344,89357,2,0,B|340:368|292:360|292:360|224:348,1,144,0|2,0:0|0:3,0:0:0:0: +148,332,90060,1,0,0:0:0:0: +204,268,90295,6,0,B|212:216|212:216|184:128,1,144,4|2,0:3|0:3,0:0:0:0: +172,44,90998,1,0,0:0:0:0: +252,76,91232,2,0,L|324:80,1,72,0|2,0:0|0:3,0:0:0:0: +412,88,91701,1,2,0:3:0:0: +377,9,91935,1,0,0:0:0:0: +360,160,92170,6,0,P|352:232|380:296,1,144,4|2,0:3|0:3,0:0:0:0: +456,336,92873,1,0,0:0:0:0: +452,248,93107,2,0,L|456:176,2,72,0|0|2,0:0|0:0|0:3,0:0:0:0: +288,308,94045,6,0,P|223:280|152:288,1,144,4|2,0:3|0:3,0:0:0:0: +80,336,94748,1,0,0:0:0:0: +72,252,94982,2,0,B|80:228|80:228|76:180,2,72,0|0|2,0:0|0:0|0:3,0:0:0:0: +64,80,95920,6,0,B|100:69|132:92|132:92|164:115|200:104,1,144,4|2,0:3|0:3,0:0:0:0: +180,20,96623,1,0,0:0:0:0: +265,45,96857,2,0,P|282:76|287:111,2,72,0|0|2,0:0|0:0|0:3,0:0:0:0: +200,104,97560,1,0,0:0:0:0: +200,104,97677,1,0,0:0:0:0: +200,104,97795,6,0,B|196:142|217:166|217:166|176:180|160:220,1,144,4|0,0:3|0:0,0:0:0:0: +240,248,98966,5,0,0:0:0:0: +202,325,99201,2,0,P|254:333|301:309,1,108,0|0,0:0|0:0,0:0:0:0: +315,292,99670,6,0,L|323:220,1,72,4|2,0:0|0:0,0:0:0:0: +365,144,100138,2,0,P|379:112|380:76,1,72,0|2,1:0|0:0,0:0:0:0: +296,92,100607,2,0,B|252:76|252:76|156:92,1,144,4|0,2:3|1:0,1:0:0:0: +156,92,101310,1,2,0:0:0:0: +76,57,101545,6,0,P|71:92|77:127,1,72,4|2,0:3|0:0,0:0:0:0: +148,180,102013,2,0,P|176:240|160:316,1,144,0|4,1:0|2:3,1:0:0:0: +108,384,102716,1,2,0:0:0:0: +72,304,102951,2,0,B|48:300|48:300|0:304,1,72,0|2,1:0|0:0,0:0:0:0: +161,313,103420,6,0,B|216:316|216:316|244:308|244:308|316:312,1,144,4|0,2:3|1:0,1:0:0:0: +392,316,104123,1,2,0:0:0:0: +352,240,104357,2,0,P|340:204|344:152,1,72,4|2,0:3|0:0,0:0:0:0: +436,120,104826,2,0,P|436:156|424:190,1,72,0|2,1:0|0:0,0:0:0:0: +352,240,105295,5,4,0:3:0:0: +268,220,105529,1,2,0:0:0:0: +244,136,105763,2,0,L|168:132,1,72,0|2,1:0|0:0,0:0:0:0: +104,228,106232,2,0,L|176:225,1,72,4|2,0:3|0:0,0:0:0:0: +300,312,106701,2,0,L|224:308,1,72,0|2,1:0|0:0,0:0:0:0: +112,312,107170,6,0,P|48:288|24:212,1,144,4|0,2:0|1:0,1:0:0:0: +36,136,107873,1,2,0:0:0:0: +76,60,108107,2,0,L|228:64,1,144,4|0,2:3|1:0,1:0:0:0: +308,68,108810,1,2,0:0:0:0: +385,29,109045,6,0,P|392:63|388:99,1,72,4|2,0:3|0:0,0:0:0:0: +320,152,109513,2,0,L|177:156,1,144,0|4,1:0|2:3,1:0:0:0: +100,196,110216,1,2,0:0:0:0: +68,276,110451,2,0,P|60:312|64:348,1,72,0|2,1:0|0:0,0:0:0:0: +144,316,110920,6,0,B|180:305|212:328|212:328|244:351|280:340,1,144,4|0,2:3|1:0,1:0:0:0: +360,308,111623,1,2,0:0:0:0: +436,268,111857,2,0,L|432:116,1,144,4|0,2:3|1:0,1:0:0:0: +432,124,112560,1,2,0:0:0:0: +504,76,112795,6,0,P|478:50|443:38,1,72,4|2,0:3|0:0,0:0:0:0: +364,72,113263,2,0,P|349:106|350:143,1,72,0|2,1:0|0:0,0:0:0:0: +356,228,113732,5,0,3:0:0:0: +356,228,113966,1,2,0:0:0:0: +276,188,114201,2,0,L|200:192,1,72,8|0,0:3|0:0,0:0:0:0: +144,128,114670,6,0,P|120:168|160:268,1,144,4|2,0:0|1:2,0:0:0:0: +224,292,115373,1,0,0:0:0:0: +152,344,115607,2,0,L|80:348,1,72,4|0,0:3|3:0,0:0:0:0: +224,292,116076,2,0,L|296:296,1,72,2|0,1:2|0:0,0:0:0:0: +380,312,116545,6,0,P|340:296|312:200,1,144,4|2,0:3|1:2,0:0:0:0: +369,143,117248,1,0,0:0:0:0: +447,182,117482,2,0,P|425:210|393:227,1,72,4|0,0:3|3:0,0:0:0:0: +380,60,117951,2,0,P|414:72|440:96,1,72,2|0,1:2|0:0,0:0:0:0: +369,143,118420,6,0,L|224:144,1,144,4|2,0:3|1:2,0:0:0:0: +140,144,119123,1,0,0:0:0:0: +182,70,119357,2,0,P|194:34|190:-6,1,72,4|0,0:3|3:0,0:0:0:0: +60,120,119826,2,0,P|59:156|70:190,1,72,2|0,1:2|0:0,0:0:0:0: +128,256,120295,6,0,B|176:240|176:240|268:256,1,144,4|2,0:3|1:2,0:0:0:0: +348,224,120998,2,0,P|352:268|344:296,1,72,0|4,0:0|0:3,0:0:0:0: +268,256,121466,2,0,P|264:212|272:184,1,72,0|2,3:0|1:2,0:0:0:0: +344,136,121935,1,0,0:0:0:0: +392,64,122170,6,0,P|316:56|260:112,2,144,4|2|4,0:3|1:2|0:3,0:0:0:0: +432,140,123341,1,0,3:0:0:0: +412,224,123576,2,0,L|488:228,1,72,2|0,1:2|0:0,0:0:0:0: +332,256,124045,6,0,P|263:246|208:290,1,144,4|2,0:3|1:2,0:0:0:0: +286,327,124748,1,0,0:0:0:0: +208,290,124982,2,0,L|212:218,1,72,4|0,0:3|3:0,0:0:0:0: +135,181,125451,2,0,L|124:252,1,72,2|0,1:2|0:0,0:0:0:0: +44,204,125920,6,0,L|80:56,1,144,4|2,0:3|1:2,0:0:0:0: +236,132,126857,2,0,P|304:136|372:104,1,144,4|2,0:3|1:2,0:0:0:0: +428,40,127560,1,0,0:0:0:0: +456,124,127795,6,0,L|460:196,2,72,4|2|2,0:3|0:0|1:2,0:0:0:0: +344,256,128732,2,0,L|340:328,2,72,4|2|2,0:3|0:0|1:2,0:0:0:0: +268,216,129435,1,0,0:0:0:0: +180,220,129670,5,4,2:0:0:0: +256,40,130373,1,2,0:0:0:0: +64,68,131076,1,2,0:0:0:0: +92,136,131310,1,0,0:0:0:0: +64,204,131545,6,0,L|60:288,1,72 +31,343,132248,2,0,P|86:345|127:309,1,108 +332,220,133420,5,2,0:0:0:0: +256,40,134123,1,2,0:0:0:0: +448,68,134826,1,2,0:0:0:0: +420,136,135060,1,0,0:0:0:0: +448,204,135295,6,0,L|452:288,1,72,2|0,0:0|0:0,0:0:0:0: +480,343,135998,2,0,P|426:345|385:309,1,108 +256,192,137170,5,2,0:0:0:0: +156,360,137873,1,2,0:0:0:0: +356,360,138576,2,0,L|352:308,1,36,2|0,0:0|0:0,0:0:0:0: +304,268,139045,6,0,P|336:253|372:252,1,72 +448,260,139748,2,0,L|444:152,1,108 +256,192,140920,5,2,0:0:0:0: +356,24,141623,1,2,0:0:0:0: +156,24,142326,2,0,L|160:72,1,36,2|0,0:0|0:0,0:0:0:0: +208,116,142795,6,0,P|176:131|140:132,1,72,2|0,0:0|0:0,0:0:0:0: +64,124,143498,2,0,L|68:232,1,108 +68,232,144670,5,4,0:3:0:0: +216,320,145138,1,4,0:3:0:0: +304,172,145607,1,4,0:3:0:0: +156,84,146075,1,4,0:3:0:0: +296,320,146545,5,4,0:3:0:0: +208,172,147013,1,4,0:3:0:0: +356,84,147482,1,4,0:3:0:0: +444,232,147950,1,4,0:3:0:0: +296,320,148420,6,0,P|252:328|192:296,2,108.000004119873,4|4|4,0:3|0:3|0:3,0:0:0:0: +260,248,149591,1,0,0:0:0:0: +320,196,149826,2,0,L|316:140,1,54.0000020599366,4|0,0:3|0:0,0:0:0:0: +120,236,159670,6,0,L|176:232,1,54.0000020599366,4|0,0:3|0:0,0:0:0:0: +160,152,160138,2,0,L|104:156,1,54.0000020599366,2|0,0:0|0:0,0:0:0:0: +240,180,160607,2,0,P|292:188|344:172,1,108.000004119873,4|2,0:3|0:0,3:0:0:0: +408,120,161310,1,0,3:0:0:0: +424,200,161545,6,0,L|420:256,1,54.0000020599366,4|0,0:3|0:0,0:0:0:0: +376,320,162013,2,0,P|396:328|480:304,2,108.000004119873,2|6|2,2:0|0:3|2:0,3:0:0:0: +312,268,163185,1,0,0:0:0:0: +296,348,163420,6,0,L|240:344,1,54.0000020599366,4|0,3:0|3:0,0:0:0:0: +160,320,163888,2,0,L|100:316,1,57.6,4|0,3:0|3:0,0:0:0:0: +64,232,164357,6,0,L|128:228,1,61.2000011672974,4|0,3:0|3:0,0:0:0:0: +204,200,164825,2,0,L|268:196,1,61.2000011672974,4|0,3:0|3:0,0:0:0:0: +232,108,165295,6,0,L|164:104,1,68.399998173523,4|0,3:0|3:0,0:0:0:0: +80,84,165763,2,0,L|4:80,1,72,4|0,3:0|3:0,0:0:0:0: +324,120,167170,6,0,P|388:128|456:92,1,144,4|2,0:0|1:2,0:0:0:0: +496,168,167873,1,0,0:0:0:0: +496,168,168107,2,0,P|484:204|488:256,1,72,4|0,0:3|3:0,0:0:0:0: +408,296,168576,2,0,P|398:261|378:231,1,72,2|0,1:2|0:0,0:0:0:0: +296,200,169045,6,0,B|228:228|156:204,1,144,4|2,0:3|1:2,0:0:0:0: +84,156,169748,1,0,0:0:0:0: +80,244,169982,2,0,L|76:316,1,72,4|0,0:3|3:0,0:0:0:0: +170,274,170451,2,0,L|156:204,1,72,2|0,1:2|0:0,0:0:0:0: +216,140,170920,6,0,L|284:276,1,144,4|2,0:3|1:2,0:0:0:0: +320,344,171623,1,0,0:0:0:0: +372,276,171857,2,0,P|366:240|349:207,1,72,4|0,0:3|3:0,0:0:0:0: +312,132,172326,2,0,L|276:60,1,72,2|0,1:2|0:0,0:0:0:0: +208,20,172795,6,0,P|272:36|348:12,1,144,4|2,0:3|1:2,0:0:0:0: +424,48,173498,2,0,L|412:132,1,72,0|4,0:0|0:3,0:0:0:0: +484,168,173966,2,0,L|472:252,1,72,0|2,3:0|1:2,0:0:0:0: +400,280,174435,1,0,0:0:0:0: +346,348,174670,6,0,P|414:363|472:324,2,144,4|2|4,0:3|1:2|0:3,0:0:0:0: +312,268,175841,1,0,3:0:0:0: +256,336,176076,2,0,L|184:332,1,72,2|0,1:2|0:0,0:0:0:0: +80,244,176545,6,0,B|140:248|140:248|164:244|164:244|223:247,1,144,4|2,0:3|1:2,0:0:0:0: +312,268,177248,1,0,0:0:0:0: +224,247,177482,2,0,P|240:215|272:187,1,72,4|0,0:3|3:0,0:0:0:0: +204,131,177951,2,0,P|233:111|275:103,1,72,2|0,1:2|0:0,0:0:0:0: +240,23,178420,6,0,B|280:15|316:35|316:35|376:71,1,144,4|2,0:3|1:2,0:0:0:0: +399,236,179357,2,0,B|359:244|323:224|323:224|263:188,1,144,4|2,0:3|1:2,0:0:0:0: +204,132,180060,1,0,0:0:0:0: +184,216,180295,6,0,L|188:288,2,72,4|2|2,0:3|0:0|1:2,0:0:0:0: +120,156,180998,1,0,0:0:0:0: +56,96,181232,2,0,L|60:24,2,72,4|2|0,0:3|0:0|1:0,0:0:0:0: +36,180,181935,1,0,0:0:0:0: +100,240,182170,6,0,P|144:300|116:380,2,144,4|2|4,0:0|1:2|0:3,0:0:0:0: +60,316,183341,1,0,0:0:0:0: +220,352,183576,2,0,L|308:348,1,72,2|0,1:2|0:0,0:0:0:0: +396,264,184045,6,0,B|336:268|336:268|312:264|312:264|253:267,1,144,4|2,0:3|1:2,0:0:0:0: +253,267,184748,1,0,0:0:0:0: +268,180,184982,2,0,L|339:177,1,72,4|0,0:3|0:0,0:0:0:0: +164,280,185451,2,0,L|92:282,1,72,2|0,1:2|0:0,0:0:0:0: +52,208,185920,6,0,P|8:268|32:344,2,144,4|2|4,0:3|1:2|0:3,0:0:0:0: +140,212,187091,1,0,0:0:0:0: +92,284,187326,2,0,P|104:316|100:368,1,72,2|0,1:2|0:0,0:0:0:0: +52,208,187795,6,0,P|48:136|76:72,1,144,4|2,0:3|1:2,0:0:0:0: +160,52,188498,2,0,P|188:28|220:16,1,72,0|4,0:0|0:3,0:0:0:0: +232,100,188966,2,0,P|268:93|301:98,1,72,0|2,0:0|1:2,0:0:0:0: +372,152,189435,1,0,0:0:0:0: +420,224,189670,6,0,P|428:296|400:360,2,144,4|2|4,0:3|1:2|0:3,0:0:0:0: +372,152,190841,1,0,0:0:0:0: +392,68,191076,2,0,L|465:64,1,72,2|0,1:2|0:0,0:0:0:0: +304,92,191545,6,0,P|236:104|168:76,1,144,4|2,0:3|1:2,0:0:0:0: +108,12,192248,1,0,0:0:0:0: +168,76,192482,2,0,L|172:152,1,72,4|0,0:3|0:0,0:0:0:0: +80,136,192951,2,0,L|101:204,1,72,2|0,1:2|0:0,0:0:0:0: +12,220,193420,6,0,B|50:279|50:279|80:300|120:292,1,144,4|2,0:3|1:2,0:0:0:0: +284,232,194357,2,0,B|320:221|352:244|352:244|384:267|420:256,1,144,4|2,0:3|1:2,0:0:0:0: +488,200,195060,1,0,0:0:0:0: +507,284,195295,6,0,P|492:315|464:338,1,72,4|0,0:0|0:0,0:0:0:0: +380,356,195763,2,0,L|236:352,1,144,0|4,1:0|0:3,0:0:0:0: +152,328,196466,1,0,3:0:0:0: +64,336,196701,2,0,P|29:325|4:300,1,72,0|0,1:0|0:0,0:0:0:0: +76,252,197170,6,0,P|108:188|96:116,1,144,4|0,0:0|1:0,0:0:0:0: +36,56,197873,1,2,0:0:0:0: +120,32,198107,2,0,L|192:28,2,72,4|2|2,0:3|0:0|1:2,0:0:0:0: +248,152,199045,6,0,P|280:168|304:196,1,72,4|2,0:3|0:0,0:0:0:0: +336,277,199513,2,0,P|306:296|269:303,1,72,2|0,1:2|0:0,0:0:0:0: +183,290,199982,2,0,P|180:254|193:219,2,72,4|2|0,0:3|0:0|1:0,0:0:0:0: +436,252,200920,6,0,P|404:188|416:116,1,144,4|0,0:3|1:0,0:0:0:0: +476,56,201623,1,2,0:0:0:0: +392,32,201857,2,0,L|320:28,2,72,4|0|2,0:3|0:0|1:2,0:0:0:0: +264,152,202795,6,0,P|232:168|208:196,1,72,4|2,0:3|0:0,0:0:0:0: +176,277,203263,2,0,P|205:296|242:303,1,72,2|0,1:2|0:0,0:0:0:0: +329,290,203732,2,0,P|331:254|318:219,2,72,4|2|0,0:3|0:0|1:0,0:0:0:0: +72,324,204670,6,0,B|60:272|60:272|76:180,1,144,4|0,0:0|1:0,0:0:0:0: +92,96,205373,1,2,0:0:0:0: +8,124,205607,2,0,P|5:88|14:53,2,72,4|2|2,0:3|0:0|1:2,0:0:0:0: +168,192,206545,6,0,P|200:174|237:173,1,72,4|2,0:3|0:0,0:0:0:0: +320,160,207013,2,0,P|318:196|301:229,1,72,2|0,1:2|0:0,0:0:0:0: +272,307,207482,2,0,P|240:287|221:256,2,72,4|2|0,0:3|0:0|1:0,0:0:0:0: +440,324,208420,6,0,B|452:272|452:272|436:180,1,144,4|0,0:3|1:0,0:0:0:0: +420,96,209123,1,2,0:0:0:0: +504,124,209357,2,0,P|507:88|498:53,2,72,4|0|2,0:3|0:0|1:2,0:0:0:0: +344,192,210295,6,0,P|311:174|274:173,1,72,4|2,0:3|0:0,0:0:0:0: +190,156,210763,2,0,P|191:192|208:225,1,72,2|0,1:2|0:0,0:0:0:0: +288,256,211232,1,4,0:3:0:0: +132,332,211701,1,0,1:0:0:0: +28,192,212170,6,0,P|16:120|44:56,1,144,4|0,0:0|1:0,0:0:0:0: +120,16,212873,1,2,0:0:0:0: +204,32,213107,2,0,L|304:28,2,72,4|2|2,0:3|0:0|1:2,0:0:0:0: +192,204,214045,6,0,P|196:240|216:272,1,72,4|2,0:3|0:0,0:0:0:0: +298,241,214513,2,0,P|327:219|345:186,1,72,6|0,1:2|0:0,0:0:0:0: +280,132,214982,2,0,P|246:117|209:118,2,72,4|2|0,0:3|0:0|1:0,0:0:0:0: +484,192,215920,6,0,P|496:120|468:56,1,144,4|0,0:3|1:0,0:0:0:0: +392,16,216623,1,2,0:0:0:0: +308,32,216857,2,0,L|208:28,2,72,4|0|2,0:3|0:0|1:2,0:0:0:0: +320,204,217795,6,0,P|316:240|296:272,1,72,4|2,0:3|0:0,0:0:0:0: +213,241,218263,2,0,P|184:219|166:186,1,72,2|0,1:2|0:0,0:0:0:0: +232,132,218732,2,0,B|260:112|300:116|300:116|384:128,1,144,4|0,0:3|1:0,0:0:0:0: +348,336,219670,6,0,B|320:356|280:352|280:352|196:340,1,144,4|0,0:0|1:0,0:0:0:0: +124,328,220373,1,2,0:0:0:0: +54,276,220607,2,0,P|41:308|39:345,2,72,4|2|2,0:3|0:0|1:2,0:0:0:0: +156,80,221545,6,0,L|251:94,1,72,4|2,0:3|0:0,0:0:0:0: +212,169,222013,2,0,L|148:160,1,64.799998022461,2|0,1:2|0:0,0:0:0:0: +140,240,222482,2,0,L|216:252,2,57.6,4|2|0,0:3|0:0|1:0,0:0:0:0: +256,192,223420,12,0,227170,0:0:0:0: +"; + } +} diff --git a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs index af482dc250..02a5c8a5fc 100644 --- a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs @@ -1,14 +1,22 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System.Linq; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Tests.Beatmaps { public class TestWorkingBeatmap : WorkingBeatmap { + public TestWorkingBeatmap(RulesetInfo ruleset) + : this(new TestBeatmap(ruleset)) + { + } + public TestWorkingBeatmap(Beatmap beatmap) : base(beatmap.BeatmapInfo) { @@ -16,9 +24,23 @@ namespace osu.Game.Tests.Beatmaps } private readonly Beatmap beatmap; - protected override Beatmap GetBeatmap() => beatmap; protected override Texture GetBackground() => null; - protected override Track GetTrack() => new TrackVirtual(); + + protected override Track GetTrack() + { + var lastObject = beatmap.HitObjects.LastOrDefault(); + if (lastObject != null) + return new TestTrack(((lastObject as IHasEndTime)?.EndTime ?? lastObject.StartTime) + 1000); + return new TrackVirtual(); + } + + private class TestTrack : TrackVirtual + { + public TestTrack(double length) + { + Length = length; + } + } } } diff --git a/osu.Game/Tests/TestTestCase.cs b/osu.Game/Tests/TestTestCase.cs new file mode 100644 index 0000000000..4efd57095e --- /dev/null +++ b/osu.Game/Tests/TestTestCase.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using NUnit.Framework; +using osu.Framework.Testing; + +namespace osu.Game.Tests +{ + [TestFixture] + internal class TestTestCase : TestCase + { + // This TestCase is required for nunit to not throw errors + // See: https://github.com/nunit/nunit/issues/1118 + } +} diff --git a/osu.Game/Tests/Visual/EditorTestCase.cs b/osu.Game/Tests/Visual/EditorTestCase.cs new file mode 100644 index 0000000000..982a3c5d73 --- /dev/null +++ b/osu.Game/Tests/Visual/EditorTestCase.cs @@ -0,0 +1,33 @@ +// Copyright (c) 2007-2018 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Game.Rulesets; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Screens; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.Visual +{ + public abstract class EditorTestCase : ScreenTestCase + { + public override IReadOnlyList RequiredTypes => new[] { typeof(Editor), typeof(EditorScreen) }; + + private readonly Ruleset ruleset; + + protected EditorTestCase(Ruleset ruleset) + { + this.ruleset = ruleset; + } + + [BackgroundDependencyLoader] + private void load(OsuGameBase osuGame) + { + osuGame.Beatmap.Value = new TestWorkingBeatmap(ruleset.RulesetInfo); + + LoadComponentAsync(new Editor(), LoadScreen); + } + } +} diff --git a/osu.Game/Tests/Visual/OsuTestCase.cs b/osu.Game/Tests/Visual/OsuTestCase.cs index 97aada2971..453e94b6bd 100644 --- a/osu.Game/Tests/Visual/OsuTestCase.cs +++ b/osu.Game/Tests/Visual/OsuTestCase.cs @@ -2,6 +2,8 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.IO; +using System.Reflection; using osu.Framework.Testing; namespace osu.Game.Tests.Visual @@ -18,6 +20,8 @@ namespace osu.Game.Tests.Visual { private readonly OsuTestCase testCase; + protected override string MainResourceFile => File.Exists(base.MainResourceFile) ? base.MainResourceFile : Assembly.GetExecutingAssembly().Location; + public OsuTestCaseTestRunner(OsuTestCase testCase) { this.testCase = testCase; diff --git a/osu.Game/Tests/Visual/TestCasePerformancePoints.cs b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs index c531edb893..7ca69c14b8 100644 --- a/osu.Game/Tests/Visual/TestCasePerformancePoints.cs +++ b/osu.Game/Tests/Visual/TestCasePerformancePoints.cs @@ -128,7 +128,7 @@ namespace osu.Game.Tests.Visual 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); + var allBeatmaps = sets.SelectMany(s => s.Beatmaps).Where(b => ruleset.LegacyID == null || b.RulesetID == ruleset.LegacyID); allBeatmaps.ForEach(b => beatmapDisplays.Add(new BeatmapDisplay(b))); } @@ -230,7 +230,7 @@ namespace osu.Game.Tests.Visual { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = "Please login to see online scores", + Text = "Please sign in to see online scores", }; } diff --git a/osu.Game/Tests/Visual/TestCasePlayer.cs b/osu.Game/Tests/Visual/TestCasePlayer.cs index 181ed5e0e6..b7c66a37e5 100644 --- a/osu.Game/Tests/Visual/TestCasePlayer.cs +++ b/osu.Game/Tests/Visual/TestCasePlayer.cs @@ -1,9 +1,7 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System.IO; using System.Linq; -using System.Text; using osu.Framework.Allocation; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; @@ -59,24 +57,13 @@ namespace osu.Game.Tests.Visual } } - protected virtual Beatmap CreateBeatmap() - { - Beatmap beatmap; - - using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(test_beatmap_data))) - using (var reader = new StreamReader(stream)) - beatmap = Game.Beatmaps.Formats.Decoder.GetDecoder(reader).DecodeBeatmap(reader); - - return beatmap; - } + protected virtual Beatmap CreateBeatmap(Ruleset ruleset) => new TestBeatmap(ruleset.RulesetInfo); private Player loadPlayerFor(RulesetInfo ri) => loadPlayerFor(ri.CreateInstance()); private Player loadPlayerFor(Ruleset r) { - var beatmap = CreateBeatmap(); - - beatmap.BeatmapInfo.Ruleset = r.RulesetInfo; + var beatmap = CreateBeatmap(r); working = new TestWorkingBeatmap(beatmap); working.Mods.Value = new[] { r.GetAllMods().First(m => m is ModNoFail) }; @@ -107,699 +94,5 @@ namespace osu.Game.Tests.Visual AllowLeadIn = false, AllowResults = false, }; - - private const string test_beatmap_data = - @"osu file format v14 - -[General] -AudioLeadIn: 500 -PreviewTime: 53498 -Countdown: 0 -SampleSet: Soft -StackLeniency: 0.7 -Mode: 0 -LetterboxInBreaks: 0 -WidescreenStoryboard: 1 - -[Editor] -DistanceSpacing: 1.2 -BeatDivisor: 4 -GridSize: 4 -TimelineZoom: 1 - -[Metadata] -Title:My Love -TitleUnicode:My Love -Artist:Kuba Oms -ArtistUnicode:Kuba Oms -Creator:W h i t e -Version:Hard -Source:ADHD -Tags:Monthly Beatmapping Contest Electronic folk pop w_h_i_t_e -BeatmapID:397534 -BeatmapSetID:163112 - -[Difficulty] -HPDrainRate:5 -CircleSize:4 -OverallDifficulty:6 -ApproachRate:7 -SliderMultiplier:1.44 -SliderTickRate:2 - -[Events] -//Break Periods -2,69870,83770 -2,152170,158770 -//Storyboard Layer 0 (Background) -//Storyboard Layer 1 (Fail) -//Storyboard Layer 2 (Pass) -//Storyboard Layer 3 (Foreground) -//Storyboard Sound Samples - -[TimingPoints] -2170,468.75,4,2,0,40,1,0 -4045,-100,4,2,0,30,0,0 -4162,-100,4,2,0,40,0,0 -5920,-100,4,2,0,30,0,0 -6037,-100,4,2,0,40,0,0 -7795,-100,4,2,0,30,0,0 -7912,-100,4,2,0,40,0,0 -9670,-100,4,2,0,40,0,0 -9787,-100,4,2,0,50,0,0 -11545,-100,4,2,0,40,0,0 -11662,-100,4,2,0,50,0,0 -13420,-100,4,2,0,40,0,0 -13537,-100,4,2,0,50,0,0 -15295,-100,4,2,0,40,0,0 -15412,-100,4,2,0,50,0,0 -17170,-100,4,2,0,40,0,0 -17287,-100,4,2,0,50,0,0 -19045,-100,4,2,0,40,0,0 -19162,-100,4,2,0,50,0,0 -20920,-100,4,2,0,40,0,0 -21037,-100,4,2,0,50,0,0 -22795,-100,4,2,0,40,0,0 -22912,-100,4,2,0,50,0,0 -24670,-100,4,2,0,70,0,0 -37560,-200,4,2,0,30,0,0 -38263,-200,4,2,0,5,0,0 -38966,-100,4,2,0,30,0,0 -39670,-100,4,2,0,70,0,0 -53732,-100,4,2,0,40,0,0 -54670,-100,4,2,0,80,0,1 -55138,-100,4,2,0,60,0,1 -55255,-100,4,2,0,80,0,1 -56076,-100,4,2,0,60,0,1 -56193,-100,4,2,0,80,0,1 -57013,-100,4,2,0,60,0,1 -57130,-100,4,2,0,80,0,1 -57951,-100,4,2,0,60,0,1 -58068,-100,4,2,0,80,0,1 -58888,-100,4,2,0,60,0,1 -59005,-100,4,2,0,80,0,1 -59826,-100,4,2,0,60,0,1 -59943,-100,4,2,0,80,0,1 -60763,-100,4,2,0,60,0,1 -60880,-100,4,2,0,80,0,1 -61701,-100,4,2,0,60,0,1 -61818,-100,4,2,0,80,0,1 -62638,-100,4,2,0,60,0,1 -62755,-100,4,2,0,80,0,1 -63576,-100,4,2,0,60,0,1 -63693,-100,4,2,0,80,0,1 -64513,-100,4,2,0,60,0,1 -64630,-100,4,2,0,80,0,1 -65451,-100,4,2,0,60,0,1 -65568,-100,4,2,0,80,0,1 -66388,-100,4,2,0,60,0,1 -66505,-100,4,2,0,80,0,1 -67326,-100,4,2,0,60,0,1 -67443,-100,4,2,0,80,0,1 -68263,-100,4,2,0,60,0,1 -68380,-100,4,2,0,80,0,1 -69201,-100,4,2,0,60,0,1 -69318,-100,4,2,0,80,0,1 -69670,-100,4,2,0,70,0,0 -84670,-100,4,2,0,70,0,0 -97560,-200,4,2,0,70,0,0 -97795,-200,4,2,0,30,0,0 -98966,-100,4,2,0,30,0,0 -99670,-100,4,2,0,70,0,0 -113732,-100,4,2,0,40,0,0 -114670,-100,4,2,0,80,0,1 -115138,-100,4,2,0,60,0,1 -115255,-100,4,2,0,80,0,1 -116076,-100,4,2,0,60,0,1 -116193,-100,4,2,0,80,0,1 -117013,-100,4,2,0,60,0,1 -117130,-100,4,2,0,80,0,1 -117951,-100,4,2,0,60,0,1 -118068,-100,4,2,0,80,0,1 -118888,-100,4,2,0,60,0,1 -119005,-100,4,2,0,80,0,1 -119826,-100,4,2,0,60,0,1 -119943,-100,4,2,0,80,0,1 -120763,-100,4,2,0,60,0,1 -120880,-100,4,2,0,80,0,1 -121701,-100,4,2,0,60,0,1 -121818,-100,4,2,0,80,0,1 -122638,-100,4,2,0,60,0,1 -122755,-100,4,2,0,80,0,1 -123576,-100,4,2,0,60,0,1 -123693,-100,4,2,0,80,0,1 -124513,-100,4,2,0,60,0,1 -124630,-100,4,2,0,80,0,1 -125451,-100,4,2,0,60,0,1 -125568,-100,4,2,0,80,0,1 -126388,-100,4,2,0,60,0,1 -126505,-100,4,2,0,80,0,1 -127326,-100,4,2,0,60,0,1 -127443,-100,4,2,0,80,0,1 -128263,-100,4,2,0,60,0,1 -128380,-100,4,2,0,80,0,1 -129201,-100,4,2,0,60,0,1 -129318,-100,4,2,0,80,0,1 -129670,-200,4,2,0,40,0,0 -144670,-133.333333333333,4,2,0,40,0,0 -159670,-133.333333333333,4,2,0,40,0,0 -163420,-133.333333333333,4,2,0,45,0,0 -163888,-125,4,2,0,50,0,0 -164357,-117.647058823529,4,2,0,55,0,0 -164826,-111.111111111111,4,2,0,60,0,0 -165295,-105.263157894737,4,2,0,65,0,0 -165763,-100,4,2,0,70,0,0 -166232,-100,4,2,0,40,0,0 -167170,-100,4,2,0,80,0,1 -167638,-100,4,2,0,60,0,1 -167755,-100,4,2,0,80,0,1 -168576,-100,4,2,0,60,0,1 -168693,-100,4,2,0,80,0,1 -169513,-100,4,2,0,60,0,1 -169630,-100,4,2,0,80,0,1 -170451,-100,4,2,0,60,0,1 -170568,-100,4,2,0,80,0,1 -171388,-100,4,2,0,60,0,1 -171505,-100,4,2,0,80,0,1 -172326,-100,4,2,0,60,0,1 -172443,-100,4,2,0,80,0,1 -173263,-100,4,2,0,60,0,1 -173380,-100,4,2,0,80,0,1 -174201,-100,4,2,0,60,0,1 -174318,-100,4,2,0,80,0,1 -175138,-100,4,2,0,60,0,1 -175255,-100,4,2,0,80,0,1 -176076,-100,4,2,0,60,0,1 -176193,-100,4,2,0,80,0,1 -177013,-100,4,2,0,60,0,1 -177130,-100,4,2,0,80,0,1 -177951,-100,4,2,0,60,0,1 -178068,-100,4,2,0,80,0,1 -178888,-100,4,2,0,60,0,1 -179005,-100,4,2,0,80,0,1 -179826,-100,4,2,0,60,0,1 -179943,-100,4,2,0,80,0,1 -180763,-100,4,2,0,60,0,1 -180880,-100,4,2,0,80,0,1 -180998,-100,4,2,0,80,0,0 -181466,-100,4,2,0,60,0,0 -181584,-100,4,2,0,80,0,0 -181935,-100,4,2,0,80,0,0 -182170,-100,4,2,0,80,0,1 -182638,-100,4,2,0,60,0,1 -182755,-100,4,2,0,80,0,1 -183576,-100,4,2,0,60,0,1 -183693,-100,4,2,0,80,0,1 -184513,-100,4,2,0,60,0,1 -184630,-100,4,2,0,80,0,1 -185451,-100,4,2,0,60,0,1 -185568,-100,4,2,0,80,0,1 -186388,-100,4,2,0,60,0,1 -186505,-100,4,2,0,80,0,1 -187326,-100,4,2,0,60,0,1 -187443,-100,4,2,0,80,0,1 -188263,-100,4,2,0,60,0,1 -188380,-100,4,2,0,80,0,1 -189201,-100,4,2,0,60,0,1 -189318,-100,4,2,0,80,0,1 -190138,-100,4,2,0,60,0,1 -190255,-100,4,2,0,80,0,1 -191076,-100,4,2,0,60,0,1 -191193,-100,4,2,0,80,0,1 -192013,-100,4,2,0,60,0,1 -192130,-100,4,2,0,80,0,1 -192951,-100,4,2,0,60,0,1 -193068,-100,4,2,0,80,0,1 -193888,-100,4,2,0,60,0,1 -194005,-100,4,2,0,80,0,1 -194826,-100,4,2,0,60,0,1 -194943,-100,4,2,0,80,0,1 -195295,-100,4,2,0,50,0,1 -195529,-100,4,2,0,52,0,1 -195646,-100,4,2,0,54,0,1 -195763,-100,4,2,0,56,0,1 -195880,-100,4,2,0,58,0,1 -195998,-100,4,2,0,60,0,1 -196115,-100,4,2,0,62,0,1 -196232,-100,4,2,0,64,0,1 -196349,-100,4,2,0,68,0,1 -196466,-100,4,2,0,70,0,1 -196584,-100,4,2,0,72,0,1 -196701,-100,4,2,0,74,0,1 -196818,-100,4,2,0,76,0,1 -196935,-100,4,2,0,78,0,1 -197052,-100,4,2,0,80,0,1 -197170,-100,4,2,0,80,0,0 -197873,-100,4,2,0,60,0,0 -197990,-100,4,2,0,80,0,0 -198341,-100,4,2,0,60,0,0 -199045,-100,4,2,0,80,0,0 -199279,-100,4,2,0,60,0,0 -199630,-100,4,2,0,80,0,0 -200216,-100,4,2,0,60,0,0 -200334,-100,4,2,0,80,0,0 -201623,-100,4,2,0,60,0,0 -201740,-100,4,2,0,80,0,0 -202326,-100,4,2,0,60,0,0 -202443,-100,4,2,0,80,0,0 -203029,-100,4,2,0,60,0,0 -203498,-100,4,2,0,80,0,0 -203966,-100,4,2,0,60,0,0 -204201,-100,4,2,0,80,0,0 -205373,-100,4,2,0,60,0,0 -205490,-100,4,2,0,80,0,0 -205841,-100,4,2,0,60,0,0 -206076,-100,4,2,0,60,0,0 -206545,-100,4,2,0,80,0,0 -206779,-100,4,2,0,60,0,0 -207130,-100,4,2,0,80,0,0 -207716,-100,4,2,0,60,0,0 -207951,-100,4,2,0,80,0,0 -209123,-100,4,2,0,60,0,0 -209240,-100,4,2,0,80,0,0 -209826,-100,4,2,0,60,0,0 -209943,-100,4,2,0,80,0,0 -210529,-100,4,2,0,60,0,0 -210880,-100,4,2,0,80,0,0 -211232,-100,4,2,0,60,0,0 -211701,-100,4,2,0,70,0,0 -212170,-100,4,2,0,80,0,0 -212873,-100,4,2,0,60,0,0 -212990,-100,4,2,0,80,0,0 -213341,-100,4,2,0,60,0,0 -213576,-100,4,2,0,60,0,0 -214045,-100,4,2,0,80,0,0 -214279,-100,4,2,0,60,0,0 -214630,-100,4,2,0,80,0,0 -215216,-100,4,2,0,60,0,0 -215451,-100,4,2,0,80,0,0 -216623,-100,4,2,0,60,0,0 -216740,-100,4,2,0,80,0,0 -217326,-100,4,2,0,60,0,0 -217443,-100,4,2,0,80,0,0 -218029,-100,4,2,0,60,0,0 -218498,-100,4,2,0,80,0,0 -218732,-100,4,2,0,50,0,0 -219670,-100,4,2,0,70,0,0 -220138,-100,4,2,0,65,0,0 -220373,-100,4,2,0,45,0,0 -220490,-100,4,2,0,65,0,0 -220607,-100,4,2,0,60,0,0 -220841,-100,4,2,0,35,0,0 -221076,-100,4,2,0,35,0,0 -221545,-100,4,2,0,50,0,0 -221779,-100,4,2,0,30,0,0 -222013,-111.111111111111,4,2,0,25,0,0 -222130,-111.111111111111,4,2,0,40,0,0 -222482,-125,4,2,0,40,0,0 -222716,-125,4,2,0,20,0,0 -222951,-100,4,2,0,15,0,0 -223420,-100,4,2,0,30,0,0 -224357,-100,4,2,0,25,0,0 -225295,-100,4,2,0,20,0,0 -226232,-100,4,2,0,15,0,0 -226701,-100,4,2,0,10,0,0 -227170,-100,4,2,0,5,0,0 - - -[Colours] - Combo1 : 17,254,176 -Combo2 : 173,255,95 -Combo3 : 255,88,100 -Combo4 : 255,94,55 - -[HitObjects] -320,256,2170,6,0,P|256:284|192:256,1,144,4|0,0:0|0:0,0:0:0:0: -144,184,2873,1,0,0:0:0:0: -108,260,3107,2,0,P|112:296|100:336,1,72 -28,288,3576,2,0,P|24:252|36:212,1,72,0|0,0:0|0:0,0:0:0:0: -76,140,4045,6,0,L|220:136,1,144,4|0,0:0|0:0,0:0:0:0: -292,88,4748,1,0,0:0:0:0: -292,88,4982,2,0,P|304:120|300:168,1,72 -388,168,5451,2,0,P|396:133|416:103,1,72,0|0,0:0|0:0,0:0:0:0: -472,172,5920,6,0,B|470:200|457:222|457:222|488:256|476:308,1,144,4|0,0:0|0:0,0:0:0:0: -396,280,6623,1,0,0:0:0:0: -324,328,6857,2,0,P|288:332|252:324,1,72 -180,280,7326,2,0,L|108:284,1,72,0|0,0:0|0:0,0:0:0:0: -256,192,7795,12,0,9670,0:0:0:0: -428,212,10138,1,0,0:0:0:0: -292,320,10607,1,0,0:0:0:0: -184,184,11076,2,0,L|112:180,1,72,0|0,0:0|0:0,0:0:0:0: -24,172,11545,5,6,0:0:0:0: -160,280,12013,1,0,0:0:0:0: -268,144,12482,1,0,0:0:0:0: -132,36,12951,2,0,L|204:32,1,72,0|0,0:0|0:0,0:0:0:0: -284,60,13420,6,0,P|340:100|344:180,2,144,6|0|0,0:0|0:0|0:0,0:0:0:0: -268,144,14591,1,0,0:0:0:0: -284,228,14826,2,0,P|316:248|364:252,1,72,0|0,0:0|0:0,0:0:0:0: -436,248,15295,6,0,P|372:272|344:340,1,144,6|2,0:0|0:0,0:0:0:0: -168,338,16232,2,0,P|141:273|76:248,1,144,2|2,0:0|0:0,0:0:0:0: -4,296,16935,1,0,0:0:0:0: -80,336,17170,5,6,0:0:0:0: -44,168,17638,1,0,0:0:0:0: -212,128,18107,1,0,0:0:0:0: -248,296,18576,2,0,P|284:288|320:292,1,72,0|0,0:0|0:0,0:0:0:0: -400,324,19045,5,6,0:0:0:0: -280,200,19513,1,0,0:0:0:0: -368,52,19982,1,0,0:0:0:0: -488,176,20451,2,0,P|452:168|416:172,1,72,0|0,0:0|0:0,0:0:0:0: -336,200,20920,6,0,P|284:216|200:192,1,144,6|0,0:0|0:0,0:0:0:0: -200,192,21857,2,0,L|204:264,1,72,0|0,0:3|0:0,0:0:0:0: -117,244,22326,2,0,L|120:172,1,72,0|0,0:0|0:0,0:0:0:0: -40,152,22795,6,0,L|28:296,2,144,6|0|0,0:0|0:0|0:0,0:0:0:0: -152,24,24201,1,0,0:0:0:0: -220,76,24435,1,0,3:0:0:0: -304,56,24670,6,0,P|288:120|296:196,1,144,4|2,0:3|0:3,0:0:0:0: -344,268,25373,1,0,0:0:0:0: -416,316,25607,2,0,P|452:312|508:316,2,72,0|0|2,0:0|0:0|0:3,0:0:0:0: -244,344,26545,6,0,P|176:356|108:328,1,144,4|2,0:3|0:3,0:0:0:0: -60,256,27248,1,0,0:0:0:0: -36,172,27482,2,0,L|40:100,2,72,0|0|2,0:0|0:0|0:3,0:0:0:0: -188,252,28420,6,0,P|192:184|196:100,1,144,4|2,0:3|0:3,0:0:0:0: -140,40,29123,1,0,0:0:0:0: -140,40,29357,2,0,B|172:16|220:24|220:24|288:36,1,144,0|2,0:0|0:3,0:0:0:0: -364,52,30060,1,0,0:0:0:0: -308,116,30295,6,0,B|300:168|300:168|328:256,1,144,4|2,0:3|0:3,0:0:0:0: -340,340,30998,1,0,0:0:0:0: -260,308,31232,2,0,L|188:304,1,72,0|2,0:0|0:3,0:0:0:0: -100,296,31701,1,2,0:3:0:0: -136,374,31935,1,0,0:0:0:0: -152,224,32170,6,0,P|160:152|132:88,1,144,4|2,0:3|0:3,0:0:0:0: -56,48,32873,1,0,0:0:0:0: -60,136,33107,2,0,L|56:208,2,72,0|0|2,0:0|0:0|0:3,0:0:0:0: -224,76,34045,6,0,P|289:104|360:96,1,144,4|2,0:3|0:3,0:0:0:0: -432,48,34748,1,0,0:0:0:0: -440,132,34982,2,0,B|432:156|432:156|436:204,2,72,0|0|2,0:0|0:0|0:3,0:0:0:0: -448,304,35920,6,0,B|412:315|380:292|380:292|348:269|312:280,1,144,4|2,0:3|0:3,0:0:0:0: -332,364,36623,1,0,0:0:0:0: -247,339,36857,2,0,P|230:308|225:273,2,72,0|0|2,0:0|0:0|0:3,0:0:0:0: -312,280,37560,6,0,L|316:172,1,108 -134,35,38966,5,0,0:0:0:0: -72,96,39201,2,0,P|119:119|171:111,1,108,0|0,0:0|0:0,0:0:0:0: -192,100,39670,6,0,L|200:172,1,72,4|2,0:0|0:0,0:0:0:0: -147,240,40138,2,0,P|133:272|132:308,1,72,0|2,1:0|0:0,0:0:0:0: -216,292,40607,2,0,B|260:308|260:308|356:292,1,144,4|0,2:3|1:0,1:0:0:0: -356,292,41310,1,2,0:0:0:0: -436,327,41545,6,0,P|441:292|435:257,1,72,4|2,0:3|0:0,0:0:0:0: -364,204,42013,2,0,P|336:144|352:68,1,144,0|4,1:0|2:3,1:0:0:0: -404,0,42716,1,2,0:0:0:0: -440,80,42951,2,0,B|464:84|464:84|512:80,1,72,0|2,1:0|0:0,0:0:0:0: -351,71,43420,6,0,B|296:68|296:68|268:76|268:76|196:72,1,144,4|0,2:3|1:0,1:0:0:0: -120,68,44123,1,2,0:0:0:0: -160,144,44357,2,0,P|172:180|168:232,1,72,4|2,0:3|0:0,0:0:0:0: -76,264,44826,2,0,P|76:228|88:194,1,72,0|2,1:0|0:0,0:0:0:0: -160,144,45295,5,4,0:3:0:0: -244,164,45529,1,2,0:0:0:0: -268,248,45763,2,0,L|344:252,1,72,0|2,1:0|0:0,0:0:0:0: -408,156,46232,2,0,L|336:159,1,72,4|2,0:3|0:0,0:0:0:0: -212,72,46701,2,0,L|288:76,1,72,0|2,1:0|0:0,0:0:0:0: -400,72,47170,6,0,P|464:96|488:172,1,144,4|0,2:0|1:0,1:0:0:0: -476,248,47873,1,2,0:0:0:0: -436,324,48107,2,0,L|284:320,1,144,4|0,2:3|1:0,1:0:0:0: -204,316,48810,1,2,0:0:0:0: -127,355,49045,6,0,P|120:321|124:285,1,72,4|2,0:3|0:0,0:0:0:0: -192,232,49513,2,0,L|335:228,1,144,0|4,1:0|2:3,1:0:0:0: -412,188,50216,1,2,0:0:0:0: -444,108,50451,2,0,P|452:72|448:36,1,72,0|2,1:0|0:0,0:0:0:0: -368,68,50920,6,0,B|332:79|300:56|300:56|268:33|232:44,1,144,4|0,2:3|1:0,1:0:0:0: -152,76,51623,1,2,0:0:0:0: -76,116,51857,2,0,L|80:268,1,144,4|0,2:3|1:0,1:0:0:0: -80,260,52560,1,2,0:0:0:0: -8,308,52795,6,0,P|34:334|69:346,1,72,4|2,0:3|0:0,0:0:0:0: -148,312,53263,2,0,P|163:278|162:241,1,72,0|2,1:0|0:0,0:0:0:0: -156,156,53732,5,0,3:0:0:0: -156,156,53966,1,2,0:0:0:0: -236,196,54201,2,0,L|312:192,1,72,8|0,0:3|0:0,0:0:0:0: -368,256,54670,6,0,P|392:216|352:116,1,144,4|2,0:0|1:2,0:0:0:0: -288,92,55373,1,0,0:0:0:0: -360,40,55607,2,0,L|432:36,1,72,4|0,0:3|3:0,0:0:0:0: -288,92,56076,2,0,L|216:88,1,72,2|0,1:2|0:0,0:0:0:0: -132,72,56545,6,0,P|172:88|200:184,1,144,4|2,0:3|1:2,0:0:0:0: -143,241,57248,1,0,0:0:0:0: -65,202,57482,2,0,P|87:174|119:157,1,72,4|0,0:3|3:0,0:0:0:0: -132,324,57951,2,0,P|98:312|72:288,1,72,2|0,1:2|0:0,0:0:0:0: -143,241,58420,6,0,L|288:240,1,144,4|2,0:3|1:2,0:0:0:0: -372,240,59123,1,0,0:0:0:0: -330,314,59357,2,0,P|318:350|322:390,1,72,4|0,0:3|3:0,0:0:0:0: -452,264,59826,2,0,P|453:228|442:194,1,72,2|0,1:2|0:0,0:0:0:0: -384,128,60295,6,0,B|336:144|336:144|244:128,1,144,4|2,0:3|1:2,0:0:0:0: -164,160,60998,2,0,P|160:116|168:88,1,72,0|4,0:0|0:3,0:0:0:0: -244,128,61466,2,0,P|248:172|240:200,1,72,0|2,3:0|1:2,0:0:0:0: -168,248,61935,1,0,0:0:0:0: -120,320,62170,6,0,P|196:328|252:272,2,144,4|2|4,0:3|1:2|0:3,0:0:0:0: -80,244,63341,1,0,3:0:0:0: -100,160,63576,2,0,L|24:156,1,72,2|0,1:2|0:0,0:0:0:0: -180,128,64045,6,0,P|249:138|304:94,1,144,4|2,0:3|1:2,0:0:0:0: -226,57,64748,1,0,0:0:0:0: -304,94,64982,2,0,L|300:166,1,72,4|0,0:3|3:0,0:0:0:0: -377,203,65451,2,0,L|388:132,1,72,2|0,1:2|0:0,0:0:0:0: -468,180,65920,6,0,L|432:328,1,144,4|2,0:3|1:2,0:0:0:0: -276,252,66857,2,0,P|208:248|140:280,1,144,4|2,0:3|1:2,0:0:0:0: -84,344,67560,1,0,0:0:0:0: -56,260,67795,6,0,L|52:188,2,72,4|2|2,0:3|0:0|1:2,0:0:0:0: -168,128,68732,2,0,L|172:56,2,72,4|2|2,0:3|0:0|1:2,0:0:0:0: -244,168,69435,1,0,0:0:0:0: -332,164,69670,1,4,0:3:0:0: -208,328,84670,6,0,P|224:264|216:188,1,144,4|2,0:3|0:3,0:0:0:0: -168,116,85373,1,0,0:0:0:0: -96,68,85607,2,0,P|60:72|4:68,2,72,0|0|2,0:0|0:0|0:3,0:0:0:0: -268,40,86545,6,0,P|336:28|404:56,1,144,4|2,0:3|0:3,0:0:0:0: -452,128,87248,1,0,0:0:0:0: -476,212,87482,2,0,L|472:284,2,72,0|0|2,0:0|0:0|0:3,0:0:0:0: -324,132,88420,6,0,P|320:200|316:284,1,144,4|2,0:3|0:3,0:0:0:0: -372,344,89123,1,0,0:0:0:0: -372,344,89357,2,0,B|340:368|292:360|292:360|224:348,1,144,0|2,0:0|0:3,0:0:0:0: -148,332,90060,1,0,0:0:0:0: -204,268,90295,6,0,B|212:216|212:216|184:128,1,144,4|2,0:3|0:3,0:0:0:0: -172,44,90998,1,0,0:0:0:0: -252,76,91232,2,0,L|324:80,1,72,0|2,0:0|0:3,0:0:0:0: -412,88,91701,1,2,0:3:0:0: -377,9,91935,1,0,0:0:0:0: -360,160,92170,6,0,P|352:232|380:296,1,144,4|2,0:3|0:3,0:0:0:0: -456,336,92873,1,0,0:0:0:0: -452,248,93107,2,0,L|456:176,2,72,0|0|2,0:0|0:0|0:3,0:0:0:0: -288,308,94045,6,0,P|223:280|152:288,1,144,4|2,0:3|0:3,0:0:0:0: -80,336,94748,1,0,0:0:0:0: -72,252,94982,2,0,B|80:228|80:228|76:180,2,72,0|0|2,0:0|0:0|0:3,0:0:0:0: -64,80,95920,6,0,B|100:69|132:92|132:92|164:115|200:104,1,144,4|2,0:3|0:3,0:0:0:0: -180,20,96623,1,0,0:0:0:0: -265,45,96857,2,0,P|282:76|287:111,2,72,0|0|2,0:0|0:0|0:3,0:0:0:0: -200,104,97560,1,0,0:0:0:0: -200,104,97677,1,0,0:0:0:0: -200,104,97795,6,0,B|196:142|217:166|217:166|176:180|160:220,1,144,4|0,0:3|0:0,0:0:0:0: -240,248,98966,5,0,0:0:0:0: -202,325,99201,2,0,P|254:333|301:309,1,108,0|0,0:0|0:0,0:0:0:0: -315,292,99670,6,0,L|323:220,1,72,4|2,0:0|0:0,0:0:0:0: -365,144,100138,2,0,P|379:112|380:76,1,72,0|2,1:0|0:0,0:0:0:0: -296,92,100607,2,0,B|252:76|252:76|156:92,1,144,4|0,2:3|1:0,1:0:0:0: -156,92,101310,1,2,0:0:0:0: -76,57,101545,6,0,P|71:92|77:127,1,72,4|2,0:3|0:0,0:0:0:0: -148,180,102013,2,0,P|176:240|160:316,1,144,0|4,1:0|2:3,1:0:0:0: -108,384,102716,1,2,0:0:0:0: -72,304,102951,2,0,B|48:300|48:300|0:304,1,72,0|2,1:0|0:0,0:0:0:0: -161,313,103420,6,0,B|216:316|216:316|244:308|244:308|316:312,1,144,4|0,2:3|1:0,1:0:0:0: -392,316,104123,1,2,0:0:0:0: -352,240,104357,2,0,P|340:204|344:152,1,72,4|2,0:3|0:0,0:0:0:0: -436,120,104826,2,0,P|436:156|424:190,1,72,0|2,1:0|0:0,0:0:0:0: -352,240,105295,5,4,0:3:0:0: -268,220,105529,1,2,0:0:0:0: -244,136,105763,2,0,L|168:132,1,72,0|2,1:0|0:0,0:0:0:0: -104,228,106232,2,0,L|176:225,1,72,4|2,0:3|0:0,0:0:0:0: -300,312,106701,2,0,L|224:308,1,72,0|2,1:0|0:0,0:0:0:0: -112,312,107170,6,0,P|48:288|24:212,1,144,4|0,2:0|1:0,1:0:0:0: -36,136,107873,1,2,0:0:0:0: -76,60,108107,2,0,L|228:64,1,144,4|0,2:3|1:0,1:0:0:0: -308,68,108810,1,2,0:0:0:0: -385,29,109045,6,0,P|392:63|388:99,1,72,4|2,0:3|0:0,0:0:0:0: -320,152,109513,2,0,L|177:156,1,144,0|4,1:0|2:3,1:0:0:0: -100,196,110216,1,2,0:0:0:0: -68,276,110451,2,0,P|60:312|64:348,1,72,0|2,1:0|0:0,0:0:0:0: -144,316,110920,6,0,B|180:305|212:328|212:328|244:351|280:340,1,144,4|0,2:3|1:0,1:0:0:0: -360,308,111623,1,2,0:0:0:0: -436,268,111857,2,0,L|432:116,1,144,4|0,2:3|1:0,1:0:0:0: -432,124,112560,1,2,0:0:0:0: -504,76,112795,6,0,P|478:50|443:38,1,72,4|2,0:3|0:0,0:0:0:0: -364,72,113263,2,0,P|349:106|350:143,1,72,0|2,1:0|0:0,0:0:0:0: -356,228,113732,5,0,3:0:0:0: -356,228,113966,1,2,0:0:0:0: -276,188,114201,2,0,L|200:192,1,72,8|0,0:3|0:0,0:0:0:0: -144,128,114670,6,0,P|120:168|160:268,1,144,4|2,0:0|1:2,0:0:0:0: -224,292,115373,1,0,0:0:0:0: -152,344,115607,2,0,L|80:348,1,72,4|0,0:3|3:0,0:0:0:0: -224,292,116076,2,0,L|296:296,1,72,2|0,1:2|0:0,0:0:0:0: -380,312,116545,6,0,P|340:296|312:200,1,144,4|2,0:3|1:2,0:0:0:0: -369,143,117248,1,0,0:0:0:0: -447,182,117482,2,0,P|425:210|393:227,1,72,4|0,0:3|3:0,0:0:0:0: -380,60,117951,2,0,P|414:72|440:96,1,72,2|0,1:2|0:0,0:0:0:0: -369,143,118420,6,0,L|224:144,1,144,4|2,0:3|1:2,0:0:0:0: -140,144,119123,1,0,0:0:0:0: -182,70,119357,2,0,P|194:34|190:-6,1,72,4|0,0:3|3:0,0:0:0:0: -60,120,119826,2,0,P|59:156|70:190,1,72,2|0,1:2|0:0,0:0:0:0: -128,256,120295,6,0,B|176:240|176:240|268:256,1,144,4|2,0:3|1:2,0:0:0:0: -348,224,120998,2,0,P|352:268|344:296,1,72,0|4,0:0|0:3,0:0:0:0: -268,256,121466,2,0,P|264:212|272:184,1,72,0|2,3:0|1:2,0:0:0:0: -344,136,121935,1,0,0:0:0:0: -392,64,122170,6,0,P|316:56|260:112,2,144,4|2|4,0:3|1:2|0:3,0:0:0:0: -432,140,123341,1,0,3:0:0:0: -412,224,123576,2,0,L|488:228,1,72,2|0,1:2|0:0,0:0:0:0: -332,256,124045,6,0,P|263:246|208:290,1,144,4|2,0:3|1:2,0:0:0:0: -286,327,124748,1,0,0:0:0:0: -208,290,124982,2,0,L|212:218,1,72,4|0,0:3|3:0,0:0:0:0: -135,181,125451,2,0,L|124:252,1,72,2|0,1:2|0:0,0:0:0:0: -44,204,125920,6,0,L|80:56,1,144,4|2,0:3|1:2,0:0:0:0: -236,132,126857,2,0,P|304:136|372:104,1,144,4|2,0:3|1:2,0:0:0:0: -428,40,127560,1,0,0:0:0:0: -456,124,127795,6,0,L|460:196,2,72,4|2|2,0:3|0:0|1:2,0:0:0:0: -344,256,128732,2,0,L|340:328,2,72,4|2|2,0:3|0:0|1:2,0:0:0:0: -268,216,129435,1,0,0:0:0:0: -180,220,129670,5,4,2:0:0:0: -256,40,130373,1,2,0:0:0:0: -64,68,131076,1,2,0:0:0:0: -92,136,131310,1,0,0:0:0:0: -64,204,131545,6,0,L|60:288,1,72 -31,343,132248,2,0,P|86:345|127:309,1,108 -332,220,133420,5,2,0:0:0:0: -256,40,134123,1,2,0:0:0:0: -448,68,134826,1,2,0:0:0:0: -420,136,135060,1,0,0:0:0:0: -448,204,135295,6,0,L|452:288,1,72,2|0,0:0|0:0,0:0:0:0: -480,343,135998,2,0,P|426:345|385:309,1,108 -256,192,137170,5,2,0:0:0:0: -156,360,137873,1,2,0:0:0:0: -356,360,138576,2,0,L|352:308,1,36,2|0,0:0|0:0,0:0:0:0: -304,268,139045,6,0,P|336:253|372:252,1,72 -448,260,139748,2,0,L|444:152,1,108 -256,192,140920,5,2,0:0:0:0: -356,24,141623,1,2,0:0:0:0: -156,24,142326,2,0,L|160:72,1,36,2|0,0:0|0:0,0:0:0:0: -208,116,142795,6,0,P|176:131|140:132,1,72,2|0,0:0|0:0,0:0:0:0: -64,124,143498,2,0,L|68:232,1,108 -68,232,144670,5,4,0:3:0:0: -216,320,145138,1,4,0:3:0:0: -304,172,145607,1,4,0:3:0:0: -156,84,146075,1,4,0:3:0:0: -296,320,146545,5,4,0:3:0:0: -208,172,147013,1,4,0:3:0:0: -356,84,147482,1,4,0:3:0:0: -444,232,147950,1,4,0:3:0:0: -296,320,148420,6,0,P|252:328|192:296,2,108.000004119873,4|4|4,0:3|0:3|0:3,0:0:0:0: -260,248,149591,1,0,0:0:0:0: -320,196,149826,2,0,L|316:140,1,54.0000020599366,4|0,0:3|0:0,0:0:0:0: -120,236,159670,6,0,L|176:232,1,54.0000020599366,4|0,0:3|0:0,0:0:0:0: -160,152,160138,2,0,L|104:156,1,54.0000020599366,2|0,0:0|0:0,0:0:0:0: -240,180,160607,2,0,P|292:188|344:172,1,108.000004119873,4|2,0:3|0:0,3:0:0:0: -408,120,161310,1,0,3:0:0:0: -424,200,161545,6,0,L|420:256,1,54.0000020599366,4|0,0:3|0:0,0:0:0:0: -376,320,162013,2,0,P|396:328|480:304,2,108.000004119873,2|6|2,2:0|0:3|2:0,3:0:0:0: -312,268,163185,1,0,0:0:0:0: -296,348,163420,6,0,L|240:344,1,54.0000020599366,4|0,3:0|3:0,0:0:0:0: -160,320,163888,2,0,L|100:316,1,57.6,4|0,3:0|3:0,0:0:0:0: -64,232,164357,6,0,L|128:228,1,61.2000011672974,4|0,3:0|3:0,0:0:0:0: -204,200,164825,2,0,L|268:196,1,61.2000011672974,4|0,3:0|3:0,0:0:0:0: -232,108,165295,6,0,L|164:104,1,68.399998173523,4|0,3:0|3:0,0:0:0:0: -80,84,165763,2,0,L|4:80,1,72,4|0,3:0|3:0,0:0:0:0: -324,120,167170,6,0,P|388:128|456:92,1,144,4|2,0:0|1:2,0:0:0:0: -496,168,167873,1,0,0:0:0:0: -496,168,168107,2,0,P|484:204|488:256,1,72,4|0,0:3|3:0,0:0:0:0: -408,296,168576,2,0,P|398:261|378:231,1,72,2|0,1:2|0:0,0:0:0:0: -296,200,169045,6,0,B|228:228|156:204,1,144,4|2,0:3|1:2,0:0:0:0: -84,156,169748,1,0,0:0:0:0: -80,244,169982,2,0,L|76:316,1,72,4|0,0:3|3:0,0:0:0:0: -170,274,170451,2,0,L|156:204,1,72,2|0,1:2|0:0,0:0:0:0: -216,140,170920,6,0,L|284:276,1,144,4|2,0:3|1:2,0:0:0:0: -320,344,171623,1,0,0:0:0:0: -372,276,171857,2,0,P|366:240|349:207,1,72,4|0,0:3|3:0,0:0:0:0: -312,132,172326,2,0,L|276:60,1,72,2|0,1:2|0:0,0:0:0:0: -208,20,172795,6,0,P|272:36|348:12,1,144,4|2,0:3|1:2,0:0:0:0: -424,48,173498,2,0,L|412:132,1,72,0|4,0:0|0:3,0:0:0:0: -484,168,173966,2,0,L|472:252,1,72,0|2,3:0|1:2,0:0:0:0: -400,280,174435,1,0,0:0:0:0: -346,348,174670,6,0,P|414:363|472:324,2,144,4|2|4,0:3|1:2|0:3,0:0:0:0: -312,268,175841,1,0,3:0:0:0: -256,336,176076,2,0,L|184:332,1,72,2|0,1:2|0:0,0:0:0:0: -80,244,176545,6,0,B|140:248|140:248|164:244|164:244|223:247,1,144,4|2,0:3|1:2,0:0:0:0: -312,268,177248,1,0,0:0:0:0: -224,247,177482,2,0,P|240:215|272:187,1,72,4|0,0:3|3:0,0:0:0:0: -204,131,177951,2,0,P|233:111|275:103,1,72,2|0,1:2|0:0,0:0:0:0: -240,23,178420,6,0,B|280:15|316:35|316:35|376:71,1,144,4|2,0:3|1:2,0:0:0:0: -399,236,179357,2,0,B|359:244|323:224|323:224|263:188,1,144,4|2,0:3|1:2,0:0:0:0: -204,132,180060,1,0,0:0:0:0: -184,216,180295,6,0,L|188:288,2,72,4|2|2,0:3|0:0|1:2,0:0:0:0: -120,156,180998,1,0,0:0:0:0: -56,96,181232,2,0,L|60:24,2,72,4|2|0,0:3|0:0|1:0,0:0:0:0: -36,180,181935,1,0,0:0:0:0: -100,240,182170,6,0,P|144:300|116:380,2,144,4|2|4,0:0|1:2|0:3,0:0:0:0: -60,316,183341,1,0,0:0:0:0: -220,352,183576,2,0,L|308:348,1,72,2|0,1:2|0:0,0:0:0:0: -396,264,184045,6,0,B|336:268|336:268|312:264|312:264|253:267,1,144,4|2,0:3|1:2,0:0:0:0: -253,267,184748,1,0,0:0:0:0: -268,180,184982,2,0,L|339:177,1,72,4|0,0:3|0:0,0:0:0:0: -164,280,185451,2,0,L|92:282,1,72,2|0,1:2|0:0,0:0:0:0: -52,208,185920,6,0,P|8:268|32:344,2,144,4|2|4,0:3|1:2|0:3,0:0:0:0: -140,212,187091,1,0,0:0:0:0: -92,284,187326,2,0,P|104:316|100:368,1,72,2|0,1:2|0:0,0:0:0:0: -52,208,187795,6,0,P|48:136|76:72,1,144,4|2,0:3|1:2,0:0:0:0: -160,52,188498,2,0,P|188:28|220:16,1,72,0|4,0:0|0:3,0:0:0:0: -232,100,188966,2,0,P|268:93|301:98,1,72,0|2,0:0|1:2,0:0:0:0: -372,152,189435,1,0,0:0:0:0: -420,224,189670,6,0,P|428:296|400:360,2,144,4|2|4,0:3|1:2|0:3,0:0:0:0: -372,152,190841,1,0,0:0:0:0: -392,68,191076,2,0,L|465:64,1,72,2|0,1:2|0:0,0:0:0:0: -304,92,191545,6,0,P|236:104|168:76,1,144,4|2,0:3|1:2,0:0:0:0: -108,12,192248,1,0,0:0:0:0: -168,76,192482,2,0,L|172:152,1,72,4|0,0:3|0:0,0:0:0:0: -80,136,192951,2,0,L|101:204,1,72,2|0,1:2|0:0,0:0:0:0: -12,220,193420,6,0,B|50:279|50:279|80:300|120:292,1,144,4|2,0:3|1:2,0:0:0:0: -284,232,194357,2,0,B|320:221|352:244|352:244|384:267|420:256,1,144,4|2,0:3|1:2,0:0:0:0: -488,200,195060,1,0,0:0:0:0: -507,284,195295,6,0,P|492:315|464:338,1,72,4|0,0:0|0:0,0:0:0:0: -380,356,195763,2,0,L|236:352,1,144,0|4,1:0|0:3,0:0:0:0: -152,328,196466,1,0,3:0:0:0: -64,336,196701,2,0,P|29:325|4:300,1,72,0|0,1:0|0:0,0:0:0:0: -76,252,197170,6,0,P|108:188|96:116,1,144,4|0,0:0|1:0,0:0:0:0: -36,56,197873,1,2,0:0:0:0: -120,32,198107,2,0,L|192:28,2,72,4|2|2,0:3|0:0|1:2,0:0:0:0: -248,152,199045,6,0,P|280:168|304:196,1,72,4|2,0:3|0:0,0:0:0:0: -336,277,199513,2,0,P|306:296|269:303,1,72,2|0,1:2|0:0,0:0:0:0: -183,290,199982,2,0,P|180:254|193:219,2,72,4|2|0,0:3|0:0|1:0,0:0:0:0: -436,252,200920,6,0,P|404:188|416:116,1,144,4|0,0:3|1:0,0:0:0:0: -476,56,201623,1,2,0:0:0:0: -392,32,201857,2,0,L|320:28,2,72,4|0|2,0:3|0:0|1:2,0:0:0:0: -264,152,202795,6,0,P|232:168|208:196,1,72,4|2,0:3|0:0,0:0:0:0: -176,277,203263,2,0,P|205:296|242:303,1,72,2|0,1:2|0:0,0:0:0:0: -329,290,203732,2,0,P|331:254|318:219,2,72,4|2|0,0:3|0:0|1:0,0:0:0:0: -72,324,204670,6,0,B|60:272|60:272|76:180,1,144,4|0,0:0|1:0,0:0:0:0: -92,96,205373,1,2,0:0:0:0: -8,124,205607,2,0,P|5:88|14:53,2,72,4|2|2,0:3|0:0|1:2,0:0:0:0: -168,192,206545,6,0,P|200:174|237:173,1,72,4|2,0:3|0:0,0:0:0:0: -320,160,207013,2,0,P|318:196|301:229,1,72,2|0,1:2|0:0,0:0:0:0: -272,307,207482,2,0,P|240:287|221:256,2,72,4|2|0,0:3|0:0|1:0,0:0:0:0: -440,324,208420,6,0,B|452:272|452:272|436:180,1,144,4|0,0:3|1:0,0:0:0:0: -420,96,209123,1,2,0:0:0:0: -504,124,209357,2,0,P|507:88|498:53,2,72,4|0|2,0:3|0:0|1:2,0:0:0:0: -344,192,210295,6,0,P|311:174|274:173,1,72,4|2,0:3|0:0,0:0:0:0: -190,156,210763,2,0,P|191:192|208:225,1,72,2|0,1:2|0:0,0:0:0:0: -288,256,211232,1,4,0:3:0:0: -132,332,211701,1,0,1:0:0:0: -28,192,212170,6,0,P|16:120|44:56,1,144,4|0,0:0|1:0,0:0:0:0: -120,16,212873,1,2,0:0:0:0: -204,32,213107,2,0,L|304:28,2,72,4|2|2,0:3|0:0|1:2,0:0:0:0: -192,204,214045,6,0,P|196:240|216:272,1,72,4|2,0:3|0:0,0:0:0:0: -298,241,214513,2,0,P|327:219|345:186,1,72,6|0,1:2|0:0,0:0:0:0: -280,132,214982,2,0,P|246:117|209:118,2,72,4|2|0,0:3|0:0|1:0,0:0:0:0: -484,192,215920,6,0,P|496:120|468:56,1,144,4|0,0:3|1:0,0:0:0:0: -392,16,216623,1,2,0:0:0:0: -308,32,216857,2,0,L|208:28,2,72,4|0|2,0:3|0:0|1:2,0:0:0:0: -320,204,217795,6,0,P|316:240|296:272,1,72,4|2,0:3|0:0,0:0:0:0: -213,241,218263,2,0,P|184:219|166:186,1,72,2|0,1:2|0:0,0:0:0:0: -232,132,218732,2,0,B|260:112|300:116|300:116|384:128,1,144,4|0,0:3|1:0,0:0:0:0: -348,336,219670,6,0,B|320:356|280:352|280:352|196:340,1,144,4|0,0:0|1:0,0:0:0:0: -124,328,220373,1,2,0:0:0:0: -54,276,220607,2,0,P|41:308|39:345,2,72,4|2|2,0:3|0:0|1:2,0:0:0:0: -156,80,221545,6,0,L|251:94,1,72,4|2,0:3|0:0,0:0:0:0: -212,169,222013,2,0,L|148:160,1,64.799998022461,2|0,1:2|0:0,0:0:0:0: -140,240,222482,2,0,L|216:252,2,57.6,4|2|0,0:3|0:0|1:0,0:0:0:0: -256,192,223420,12,0,227170,0:0:0:0: -"; } } diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index c29bc91d17..2504c9c62c 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -73,10 +73,10 @@ namespace osu.Game.Users public struct UserRanks { [JsonProperty(@"global")] - public int Global; + public int? Global; [JsonProperty(@"country")] - public int Country; + public int? Country; } }