diff --git a/.github/workflows/report-nunit.yml b/.github/workflows/report-nunit.yml index e0ccd50989..358cbda17a 100644 --- a/.github/workflows/report-nunit.yml +++ b/.github/workflows/report-nunit.yml @@ -30,3 +30,5 @@ jobs: name: Test Results (${{matrix.os.prettyname}}, ${{matrix.threadingMode}}) path: "*.trx" reporter: dotnet-trx + list-suites: 'failed' + list-tests: 'failed' diff --git a/osu.Android.props b/osu.Android.props index db62667fc2..f552aff2f2 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -51,8 +51,8 @@ - - + + diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 0d6925a83d..6d5a960f06 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -42,9 +42,8 @@ namespace osu.Game.Rulesets.Catch.Objects base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); - DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime); - double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier; + double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; Velocity = scoringDistance / timingPoint.BeatLength; TickDistance = scoringDistance / difficulty.SliderTickRate; diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs index 538a51db5f..5ccb191a9b 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Edit; using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; @@ -101,27 +102,27 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor throw new System.NotImplementedException(); } - public override float GetBeatSnapDistanceAt(double referenceTime) + public override float GetBeatSnapDistanceAt(HitObject referenceObject) { throw new System.NotImplementedException(); } - public override float DurationToDistance(double referenceTime, double duration) + public override float DurationToDistance(HitObject referenceObject, double duration) { throw new System.NotImplementedException(); } - public override double DistanceToDuration(double referenceTime, float distance) + public override double DistanceToDuration(HitObject referenceObject, float distance) { throw new System.NotImplementedException(); } - public override double GetSnappedDurationFromDistance(double referenceTime, float distance) + public override double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) { throw new System.NotImplementedException(); } - public override float GetSnappedDistanceFromDistance(double referenceTime, float distance) + public override float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) { throw new System.NotImplementedException(); } diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 471dad87d5..4387bc6b3b 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -388,7 +388,7 @@ namespace osu.Game.Rulesets.Mania.Tests }, }; - beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f }); + beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f }); } AddStep("load player", () => diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs index 18891f8c58..89e13acad6 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs @@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Mania.Tests }, }); - Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f }); + Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new EffectControlPoint { ScrollSpeed = 0.1f }); var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index 380efff69f..1ed045f7e0 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy Debug.Assert(distanceData != null); TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime); - DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime); + DifficultyControlPoint difficultyPoint = hitObject.DifficultyControlPoint; double beatLength; #pragma warning disable 618 @@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy #pragma warning restore 618 beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier; else - beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier; + beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity; SpanCount = repeatsData?.SpanCount() ?? 1; StartTime = (int)Math.Round(hitObject.StartTime); diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu index 7c75b45e5f..ca9e5b0b85 100644 --- a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/mania-samples.osu @@ -13,6 +13,7 @@ SliderTickRate:1 [TimingPoints] 0,500,4,1,0,100,1,0 +10000,-150,4,1,0,100,1,0 [HitObjects] 51,192,500,128,0,1500:1:0:0:0: diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 3b7da8d9ba..28e970f397 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Mania.UI // For non-mania beatmap, speed changes should only happen through timing points if (!isForCurrentRuleset) - p.DifficultyPoint = new DifficultyControlPoint(); + p.EffectPoint = new EffectControlPoint(); } BarLines.ForEach(Playfield.Add); diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs index 851be2b2f2..ef43c3a696 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs @@ -11,6 +11,7 @@ using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Objects; @@ -179,15 +180,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0); - public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length; + public float GetBeatSnapDistanceAt(HitObject referenceObject) => (float)beat_length; - public float DurationToDistance(double referenceTime, double duration) => (float)duration; + public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration; - public double DistanceToDuration(double referenceTime, float distance) => distance; + public double DistanceToDuration(HitObject referenceObject, float distance) => distance; - public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0; + public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => 0; - public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => 0; + public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => 0; } } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs index f09aad8b49..1f01ba601b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneGameplayCursor.cs @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Tests { config.SetValue(OsuSetting.AutoCursorSize, true); gameplayState.Beatmap.Difficulty.CircleSize = val; - Scheduler.AddOnce(() => loadContent(false)); + Scheduler.AddOnce(loadContent); }); AddStep("test cursor container", () => loadContent(false)); @@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep($"adjust cs to {circleSize}", () => gameplayState.Beatmap.Difficulty.CircleSize = circleSize); AddStep("turn on autosizing", () => config.SetValue(OsuSetting.AutoCursorSize, true)); - AddStep("load content", () => loadContent()); + AddStep("load content", loadContent); AddUntilStep("cursor size correct", () => lastContainer.ActiveCursor.Scale.X == OsuCursorContainer.GetScaleForCircleSize(circleSize) * userScale); @@ -98,7 +98,9 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("load content", () => loadContent(false, () => new SkinProvidingContainer(new TopLeftCursorSkin()))); } - private void loadContent(bool automated = true, Func skinProvider = null) + private void loadContent() => loadContent(false); + + private void loadContent(bool automated, Func skinProvider = null) { SetContents(_ => { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs index ececfb0586..d31e7a31f5 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs @@ -407,8 +407,6 @@ namespace osu.Game.Rulesets.Osu.Tests }, }); - Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f }); - SelectedMods.Value = new[] { new OsuModClassic() }; var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); @@ -439,6 +437,8 @@ namespace osu.Game.Rulesets.Osu.Tests { public TestSlider() { + DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f }; + DefaultsApplied += _ => { HeadCircle.HitWindows = new TestHitWindows(); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index 81902c25af..03b4254eed 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -13,6 +13,7 @@ using osuTK.Graphics; using osu.Game.Rulesets.Mods; using System.Linq; using NUnit.Framework; +using osu.Game.Beatmaps.Legacy; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Judgements; @@ -328,10 +329,14 @@ namespace osu.Game.Rulesets.Osu.Tests private Drawable createDrawable(Slider slider, float circleSize, double speedMultiplier) { - var cpi = new ControlPointInfo(); - cpi.Add(0, new DifficultyControlPoint { SpeedMultiplier = speedMultiplier }); + var cpi = new LegacyControlPointInfo(); + cpi.Add(0, new DifficultyControlPoint { SliderVelocity = speedMultiplier }); - slider.ApplyDefaults(cpi, new BeatmapDifficulty { CircleSize = circleSize, SliderTickRate = 3 }); + slider.ApplyDefaults(cpi, new BeatmapDifficulty + { + CircleSize = circleSize, + SliderTickRate = 3 + }); var drawable = CreateDrawableSlider(slider); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 590d159300..f3392724ec 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -348,6 +348,7 @@ namespace osu.Game.Rulesets.Osu.Tests { StartTime = time_slider_start, Position = new Vector2(0, 0), + DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f }, Path = new SliderPath(PathType.PerfectCurve, new[] { Vector2.Zero, @@ -362,8 +363,6 @@ namespace osu.Game.Rulesets.Osu.Tests }, }); - Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f }); - var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); p.OnLoadComplete += _ => diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs index 1b85e0efde..2d43e1b95e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs @@ -369,8 +369,6 @@ namespace osu.Game.Rulesets.Osu.Tests }, }); - Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f }); - var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); p.OnLoadComplete += _ => @@ -399,6 +397,8 @@ namespace osu.Game.Rulesets.Osu.Tests { public TestSlider() { + DifficultyControlPoint = new DifficultyControlPoint { SliderVelocity = 0.1f }; + DefaultsApplied += _ => { HeadCircle.HitWindows = new TestHitWindows(); diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs index a2fc4848af..d82186fb52 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs @@ -11,6 +11,7 @@ using System.Linq; using System.Threading; using osu.Game.Rulesets.Osu.UI; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Beatmaps.Legacy; namespace osu.Game.Rulesets.Osu.Beatmaps { @@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps LegacyLastTickOffset = (original as IHasLegacyLastTickOffset)?.LegacyLastTickOffset, // prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance. // this results in more (or less) ticks being generated in curr objects is a spinner + if (BaseObject is Spinner || lastObject is Spinner) + return; + // We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps. float scalingFactor = normalized_radius / (float)BaseObject.Radius; @@ -89,14 +93,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing Vector2 lastCursorPosition = getEndCursorPosition(lastObject); - // Don't need to jump to reach spinners - if (!(BaseObject is Spinner)) - { - JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length; - MovementDistance = Math.Min(JumpDistance, MovementDistance); - } + JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length; + MovementDistance = Math.Min(JumpDistance, MovementDistance); - if (lastLastObject != null) + if (lastLastObject != null && !(lastLastObject is Spinner)) { Vector2 lastLastCursorPosition = getEndCursorPosition(lastLastObject); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs index b9e4ed6fcb..07b6a1bdc2 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -8,12 +8,14 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Events; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; +using osu.Game.Screens.Edit; using osuTK; using osuTK.Input; @@ -67,6 +69,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders inputManager = GetContainingInputManager(); } + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } + public override void UpdateTimeAndPosition(SnapResult result) { base.UpdateTimeAndPosition(result); @@ -75,6 +80,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { case SliderPlacementState.Initial: BeginPlacement(); + + var nearestDifficultyPoint = editorBeatmap.HitObjects.LastOrDefault(h => h.GetEndTime() < HitObject.StartTime)?.DifficultyControlPoint?.DeepClone() as DifficultyControlPoint; + + HitObject.DifficultyControlPoint = nearestDifficultyPoint ?? new DifficultyControlPoint(); HitObject.Position = ToLocalSpace(result.ScreenSpacePosition); break; @@ -212,7 +221,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void updateSlider() { - HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; + HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; bodyPiece.UpdateFrom(HitObject); headCirclePiece.UpdateFrom(HitObject.HeadCircle); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 89724876fa..a7fadfb67f 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -230,7 +230,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void updatePath() { - HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; + HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; editorBeatmap?.Update(HitObject); } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs index ff3be97427..8a561f962a 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuDistanceSnapGrid.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Edit public class OsuDistanceSnapGrid : CircularDistanceSnapGrid { public OsuDistanceSnapGrid(OsuHitObject hitObject, [CanBeNull] OsuHitObject nextHitObject = null) - : base(hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime) + : base(hitObject, hitObject.StackedEndPosition, hitObject.GetEndTime(), nextHitObject?.StartTime) { Masking = true; } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs new file mode 100644 index 0000000000..c48cbd9992 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuModNoScope.cs @@ -0,0 +1,76 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.Mods; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Bindables; +using osu.Framework.Localisation; +using osu.Framework.Utils; +using osu.Game.Graphics.UserInterface; +using osu.Game.Configuration; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; + +namespace osu.Game.Rulesets.Osu.Mods +{ + public class OsuModNoScope : Mod, IUpdatableByPlayfield, IApplicableToScoreProcessor + { + /// + /// Slightly higher than the cutoff for . + /// + private const float min_alpha = 0.0002f; + + private const float transition_duration = 100; + + public override string Name => "No Scope"; + public override string Acronym => "NS"; + public override ModType Type => ModType.Fun; + public override IconUsage? Icon => FontAwesome.Solid.EyeSlash; + public override string Description => "Where's the cursor?"; + public override double ScoreMultiplier => 1; + + private BindableNumber currentCombo; + + private float targetAlpha; + + [SettingSource( + "Hidden at combo", + "The combo count at which the cursor becomes completely hidden", + SettingControlType = typeof(SettingsSlider) + )] + public BindableInt HiddenComboCount { get; } = new BindableInt + { + Default = 10, + Value = 10, + MinValue = 0, + MaxValue = 50, + }; + + public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank; + + public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) + { + if (HiddenComboCount.Value == 0) return; + + currentCombo = scoreProcessor.Combo.GetBoundCopy(); + currentCombo.BindValueChanged(combo => + { + targetAlpha = Math.Max(min_alpha, 1 - (float)combo.NewValue / HiddenComboCount.Value); + }, true); + } + + public virtual void Update(Playfield playfield) + { + playfield.Cursor.Alpha = (float)Interpolation.Lerp(playfield.Cursor.Alpha, targetAlpha, Math.Clamp(playfield.Time.Elapsed / transition_duration, 0, 1)); + } + } + + public class HiddenComboSlider : OsuSliderBar + { + public override LocalisableString TooltipText => Current.Value == 0 ? "always hidden" : base.TooltipText; + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 1d494c2917..9e9c75cf31 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -146,9 +146,8 @@ namespace osu.Game.Rulesets.Osu.Objects base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); - DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime); - double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier; + double scoringDistance = BASE_SCORING_DISTANCE * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; Velocity = scoringDistance / timingPoint.BeatLength; TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier; @@ -181,7 +180,6 @@ namespace osu.Game.Rulesets.Osu.Objects StartTime = e.Time, Position = Position, StackHeight = StackHeight, - SampleControlPoint = SampleControlPoint, }); break; diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index f4a93a571d..ee4712c3b8 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -192,6 +192,7 @@ namespace osu.Game.Rulesets.Osu new OsuModBarrelRoll(), new OsuModApproachDifferent(), new OsuModMuted(), + new OsuModNoScope(), }; case ModType.System: diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 50c0ca7f55..32aad6c36a 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps double distance = distanceData.Distance * spans * LegacyBeatmapEncoder.LEGACY_TAIKO_VELOCITY_MULTIPLIER; TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime); - DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime); + DifficultyControlPoint difficultyPoint = obj.DifficultyControlPoint; double beatLength; #pragma warning disable 618 @@ -162,7 +162,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps #pragma warning restore 618 beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier; else - beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier; + beatLength = timingPoint.BeatLength / difficultyPoint.SliderVelocity; double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.Difficulty.SliderMultiplier / beatmap.Difficulty.SliderTickRate; diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 0318e32991..0e93ad7e73 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -63,9 +63,8 @@ namespace osu.Game.Rulesets.Taiko.Objects base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); - DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime); - double scoringDistance = base_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier; + double scoringDistance = base_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; Velocity = scoringDistance / timingPoint.BeatLength; tickSpacing = timingPoint.BeatLength / TickRate; diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index b5e1fa204f..cb12d03620 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -192,15 +192,15 @@ namespace osu.Game.Tests.Beatmaps.Formats var difficultyPoint = controlPoints.DifficultyPointAt(0); Assert.AreEqual(0, difficultyPoint.Time); - Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier); + Assert.AreEqual(1.0, difficultyPoint.SliderVelocity); difficultyPoint = controlPoints.DifficultyPointAt(48428); Assert.AreEqual(0, difficultyPoint.Time); - Assert.AreEqual(1.0, difficultyPoint.SpeedMultiplier); + Assert.AreEqual(1.0, difficultyPoint.SliderVelocity); difficultyPoint = controlPoints.DifficultyPointAt(116999); Assert.AreEqual(116999, difficultyPoint.Time); - Assert.AreEqual(0.75, difficultyPoint.SpeedMultiplier, 0.1); + Assert.AreEqual(0.75, difficultyPoint.SliderVelocity, 0.1); var soundPoint = controlPoints.SamplePointAt(0); Assert.AreEqual(956, soundPoint.Time); @@ -227,7 +227,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.IsTrue(effectPoint.KiaiMode); Assert.IsFalse(effectPoint.OmitFirstBarLine); - effectPoint = controlPoints.EffectPointAt(119637); + effectPoint = controlPoints.EffectPointAt(116637); Assert.AreEqual(95901, effectPoint.Time); Assert.IsFalse(effectPoint.KiaiMode); Assert.IsFalse(effectPoint.OmitFirstBarLine); @@ -249,10 +249,10 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(controlPoints.EffectPoints.Count, Is.EqualTo(3)); Assert.That(controlPoints.SamplePoints.Count, Is.EqualTo(3)); - Assert.That(controlPoints.DifficultyPointAt(500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1)); - Assert.That(controlPoints.DifficultyPointAt(1500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1)); - Assert.That(controlPoints.DifficultyPointAt(2500).SpeedMultiplier, Is.EqualTo(0.75).Within(0.1)); - Assert.That(controlPoints.DifficultyPointAt(3500).SpeedMultiplier, Is.EqualTo(1.5).Within(0.1)); + Assert.That(controlPoints.DifficultyPointAt(500).SliderVelocity, Is.EqualTo(1.5).Within(0.1)); + Assert.That(controlPoints.DifficultyPointAt(1500).SliderVelocity, Is.EqualTo(1.5).Within(0.1)); + Assert.That(controlPoints.DifficultyPointAt(2500).SliderVelocity, Is.EqualTo(0.75).Within(0.1)); + Assert.That(controlPoints.DifficultyPointAt(3500).SliderVelocity, Is.EqualTo(1.5).Within(0.1)); Assert.That(controlPoints.EffectPointAt(500).KiaiMode, Is.True); Assert.That(controlPoints.EffectPointAt(1500).KiaiMode, Is.True); @@ -279,10 +279,10 @@ namespace osu.Game.Tests.Beatmaps.Formats using (var resStream = TestResources.OpenResource("timingpoint-speedmultiplier-reset.osu")) using (var stream = new LineBufferedReader(resStream)) { - var controlPoints = decoder.Decode(stream).ControlPointInfo; + var controlPoints = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo; - Assert.That(controlPoints.DifficultyPointAt(0).SpeedMultiplier, Is.EqualTo(0.5).Within(0.1)); - Assert.That(controlPoints.DifficultyPointAt(2000).SpeedMultiplier, Is.EqualTo(1).Within(0.1)); + Assert.That(controlPoints.DifficultyPointAt(0).SliderVelocity, Is.EqualTo(0.5).Within(0.1)); + Assert.That(controlPoints.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1).Within(0.1)); } } @@ -394,12 +394,12 @@ namespace osu.Game.Tests.Beatmaps.Formats using (var resStream = TestResources.OpenResource("controlpoint-difficulty-multiplier.osu")) using (var stream = new LineBufferedReader(resStream)) { - var controlPointInfo = decoder.Decode(stream).ControlPointInfo; + var controlPointInfo = (LegacyControlPointInfo)decoder.Decode(stream).ControlPointInfo; - Assert.That(controlPointInfo.DifficultyPointAt(5).SpeedMultiplier, Is.EqualTo(1)); - Assert.That(controlPointInfo.DifficultyPointAt(1000).SpeedMultiplier, Is.EqualTo(10)); - Assert.That(controlPointInfo.DifficultyPointAt(2000).SpeedMultiplier, Is.EqualTo(1.8518518518518519d)); - Assert.That(controlPointInfo.DifficultyPointAt(3000).SpeedMultiplier, Is.EqualTo(0.5)); + Assert.That(controlPointInfo.DifficultyPointAt(5).SliderVelocity, Is.EqualTo(1)); + Assert.That(controlPointInfo.DifficultyPointAt(1000).SliderVelocity, Is.EqualTo(10)); + Assert.That(controlPointInfo.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1.8518518518518519d)); + Assert.That(controlPointInfo.DifficultyPointAt(3000).SliderVelocity, Is.EqualTo(0.5)); } } diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index 896aa53f82..d12da1a22f 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -46,8 +46,7 @@ namespace osu.Game.Tests.Beatmaps.Formats sort(decoded.beatmap); sort(decodedAfterEncode.beatmap); - Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize())); - Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration)); + compareBeatmaps(decoded, decodedAfterEncode); } [TestCaseSource(nameof(allBeatmaps))] @@ -62,8 +61,7 @@ namespace osu.Game.Tests.Beatmaps.Formats sort(decoded.beatmap); sort(decodedAfterEncode.beatmap); - Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize())); - Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration)); + compareBeatmaps(decoded, decodedAfterEncode); } [TestCaseSource(nameof(allBeatmaps))] @@ -77,12 +75,7 @@ namespace osu.Game.Tests.Beatmaps.Formats var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded), name); - // in this process, we may lose some detail in the control points section. - // let's focus on only the hitobjects. - var originalHitObjects = decoded.beatmap.HitObjects.Serialize(); - var newHitObjects = decodedAfterEncode.beatmap.HitObjects.Serialize(); - - Assert.That(newHitObjects, Is.EqualTo(originalHitObjects)); + compareBeatmaps(decoded, decodedAfterEncode); ControlPointInfo removeLegacyControlPointTypes(ControlPointInfo controlPointInfo) { @@ -97,7 +90,7 @@ namespace osu.Game.Tests.Beatmaps.Formats // completely ignore "legacy" types, which have been moved to HitObjects. // even though these would mostly be ignored by the Add call, they will still be available in groups, // which isn't what we want to be testing here. - if (point is SampleControlPoint) + if (point is SampleControlPoint || point is DifficultyControlPoint) continue; newControlPoints.Add(point.Time, point.DeepClone()); @@ -107,6 +100,19 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + private void compareBeatmaps((IBeatmap beatmap, TestLegacySkin skin) expected, (IBeatmap beatmap, TestLegacySkin skin) actual) + { + // Check all control points that are still considered to be at a global level. + Assert.That(expected.beatmap.ControlPointInfo.TimingPoints.Serialize(), Is.EqualTo(actual.beatmap.ControlPointInfo.TimingPoints.Serialize())); + Assert.That(expected.beatmap.ControlPointInfo.EffectPoints.Serialize(), Is.EqualTo(actual.beatmap.ControlPointInfo.EffectPoints.Serialize())); + + // Check all hitobjects. + Assert.That(expected.beatmap.HitObjects.Serialize(), Is.EqualTo(actual.beatmap.HitObjects.Serialize())); + + // Check skin. + Assert.IsTrue(areComboColoursEqual(expected.skin.Configuration, actual.skin.Configuration)); + } + [Test] public void TestEncodeMultiSegmentSliderWithFloatingPointError() { @@ -156,7 +162,7 @@ namespace osu.Game.Tests.Beatmaps.Formats } } - private (IBeatmap beatmap, TestLegacySkin beatmapSkin) decodeFromLegacy(Stream stream, string name) + private (IBeatmap beatmap, TestLegacySkin skin) decodeFromLegacy(Stream stream, string name) { using (var reader = new LineBufferedReader(stream)) { @@ -174,7 +180,7 @@ namespace osu.Game.Tests.Beatmaps.Formats } } - private MemoryStream encodeToLegacy((IBeatmap beatmap, ISkin beatmapSkin) fullBeatmap) + private MemoryStream encodeToLegacy((IBeatmap beatmap, ISkin skin) fullBeatmap) { var (beatmap, beatmapSkin) = fullBeatmap; var stream = new MemoryStream(); diff --git a/osu.Game.Tests/Database/RulesetStoreTests.cs b/osu.Game.Tests/Database/RulesetStoreTests.cs new file mode 100644 index 0000000000..f4e0838be1 --- /dev/null +++ b/osu.Game.Tests/Database/RulesetStoreTests.cs @@ -0,0 +1,54 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Game.Models; +using osu.Game.Stores; + +namespace osu.Game.Tests.Database +{ + public class RulesetStoreTests : RealmTest + { + [Test] + public void TestCreateStore() + { + RunTestWithRealm((realmFactory, storage) => + { + var rulesets = new RealmRulesetStore(realmFactory, storage); + + Assert.AreEqual(4, rulesets.AvailableRulesets.Count()); + Assert.AreEqual(4, realmFactory.Context.All().Count()); + }); + } + + [Test] + public void TestCreateStoreTwiceDoesntAddRulesetsAgain() + { + RunTestWithRealm((realmFactory, storage) => + { + var rulesets = new RealmRulesetStore(realmFactory, storage); + var rulesets2 = new RealmRulesetStore(realmFactory, storage); + + Assert.AreEqual(4, rulesets.AvailableRulesets.Count()); + Assert.AreEqual(4, rulesets2.AvailableRulesets.Count()); + + Assert.AreEqual(rulesets.AvailableRulesets.First(), rulesets2.AvailableRulesets.First()); + Assert.AreEqual(4, realmFactory.Context.All().Count()); + }); + } + + [Test] + public void TestRetrievedRulesetsAreDetached() + { + RunTestWithRealm((realmFactory, storage) => + { + var rulesets = new RealmRulesetStore(realmFactory, storage); + + Assert.IsTrue((rulesets.AvailableRulesets.First() as RealmRuleset)?.IsManaged == false); + Assert.IsTrue((rulesets.GetRuleset(0) as RealmRuleset)?.IsManaged == false); + Assert.IsTrue((rulesets.GetRuleset("mania") as RealmRuleset)?.IsManaged == false); + }); + } + } +} diff --git a/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs b/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs new file mode 100644 index 0000000000..f3a4f10210 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckAudioInVideoTest.cs @@ -0,0 +1,104 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Moq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Storyboards; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Resources; +using FileInfo = osu.Game.IO.FileInfo; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public class CheckAudioInVideoTest + { + private CheckAudioInVideo check; + private IBeatmap beatmap; + + [SetUp] + public void Setup() + { + check = new CheckAudioInVideo(); + beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + BeatmapSet = new BeatmapSetInfo + { + Files = new List(new[] + { + new BeatmapSetFileInfo + { + Filename = "abc123.mp4", + FileInfo = new FileInfo { Hash = "abcdef" } + } + }) + } + } + }; + } + + [Test] + public void TestRegularVideoFile() + { + using (var resourceStream = TestResources.OpenResource("Videos/test-video.mp4")) + Assert.IsEmpty(check.Run(getContext(resourceStream))); + } + + [Test] + public void TestVideoFileWithAudio() + { + using (var resourceStream = TestResources.OpenResource("Videos/test-video-with-audio.mp4")) + { + var issues = check.Run(getContext(resourceStream)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateHasAudioTrack); + } + } + + [Test] + public void TestVideoFileWithTrackButNoAudio() + { + using (var resourceStream = TestResources.OpenResource("Videos/test-video-with-track-but-no-audio.mp4")) + { + var issues = check.Run(getContext(resourceStream)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateHasAudioTrack); + } + } + + [Test] + public void TestMissingFile() + { + beatmap.BeatmapInfo.BeatmapSet.Files.Clear(); + + var issues = check.Run(getContext(null)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckAudioInVideo.IssueTemplateMissingFile); + } + + private BeatmapVerifierContext getContext(Stream resourceStream) + { + var storyboard = new Storyboard(); + var layer = storyboard.GetLayer("Video"); + layer.Add(new StoryboardVideo("abc123.mp4", 0)); + + var mockWorkingBeatmap = new Mock(beatmap, null, null); + mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(resourceStream); + mockWorkingBeatmap.As().SetupGet(w => w.Storyboard).Returns(storyboard); + + return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object); + } + } +} diff --git a/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs new file mode 100644 index 0000000000..9b090591bc --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckTooShortAudioFilesTest.cs @@ -0,0 +1,128 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using ManagedBass; +using Moq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Resources; +using osuTK.Audio; +using FileInfo = osu.Game.IO.FileInfo; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public class CheckTooShortAudioFilesTest + { + private CheckTooShortAudioFiles check; + private IBeatmap beatmap; + + [SetUp] + public void Setup() + { + check = new CheckTooShortAudioFiles(); + beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + BeatmapSet = new BeatmapSetInfo + { + Files = new List(new[] + { + new BeatmapSetFileInfo + { + Filename = "abc123.wav", + FileInfo = new FileInfo { Hash = "abcdef" } + } + }) + } + } + }; + + // 0 = No output device. This still allows decoding. + if (!Bass.Init(0) && Bass.LastError != Errors.Already) + throw new AudioException("Could not initialize Bass."); + } + + [Test] + public void TestDifferentExtension() + { + beatmap.BeatmapInfo.BeatmapSet.Files.Clear(); + beatmap.BeatmapInfo.BeatmapSet.Files.Add(new BeatmapSetFileInfo + { + Filename = "abc123.jpg", + FileInfo = new FileInfo { Hash = "abcdef" } + }); + + // Should fail to load, but not produce an error due to the extension not being expected to load. + Assert.IsEmpty(check.Run(getContext(null, allowMissing: true))); + } + + [Test] + public void TestRegularAudioFile() + { + using (var resourceStream = TestResources.OpenResource("Samples/test-sample.mp3")) + { + Assert.IsEmpty(check.Run(getContext(resourceStream))); + } + } + + [Test] + public void TestBlankAudioFile() + { + using (var resourceStream = TestResources.OpenResource("Samples/blank.wav")) + { + // This is a 0 ms duration audio file, commonly used to silence sliderslides/ticks, and so should be fine. + Assert.IsEmpty(check.Run(getContext(resourceStream))); + } + } + + [Test] + public void TestTooShortAudioFile() + { + using (var resourceStream = TestResources.OpenResource("Samples/test-sample-cut.mp3")) + { + var issues = check.Run(getContext(resourceStream)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckTooShortAudioFiles.IssueTemplateTooShort); + } + } + + [Test] + public void TestMissingAudioFile() + { + using (var resourceStream = TestResources.OpenResource("Samples/missing.mp3")) + { + Assert.IsEmpty(check.Run(getContext(resourceStream, allowMissing: true))); + } + } + + [Test] + public void TestCorruptAudioFile() + { + using (var resourceStream = TestResources.OpenResource("Samples/corrupt.wav")) + { + var issues = check.Run(getContext(resourceStream)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckTooShortAudioFiles.IssueTemplateBadFormat); + } + } + + private BeatmapVerifierContext getContext(Stream resourceStream, bool allowMissing = false) + { + var mockWorkingBeatmap = new Mock(beatmap, null, null); + mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(resourceStream); + + return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object); + } + } +} diff --git a/osu.Game.Tests/Editing/Checks/CheckZeroByteFilesTest.cs b/osu.Game.Tests/Editing/Checks/CheckZeroByteFilesTest.cs new file mode 100644 index 0000000000..c9adc030c1 --- /dev/null +++ b/osu.Game.Tests/Editing/Checks/CheckZeroByteFilesTest.cs @@ -0,0 +1,86 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Moq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Edit.Checks; +using osu.Game.Rulesets.Objects; +using FileInfo = osu.Game.IO.FileInfo; + +namespace osu.Game.Tests.Editing.Checks +{ + [TestFixture] + public class CheckZeroByteFilesTest + { + private CheckZeroByteFiles check; + private IBeatmap beatmap; + + [SetUp] + public void Setup() + { + check = new CheckZeroByteFiles(); + beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + BeatmapSet = new BeatmapSetInfo + { + Files = new List(new[] + { + new BeatmapSetFileInfo + { + Filename = "abc123.jpg", + FileInfo = new FileInfo { Hash = "abcdef" } + } + }) + } + } + }; + } + + [Test] + public void TestNonZeroBytes() + { + Assert.IsEmpty(check.Run(getContext(byteLength: 44))); + } + + [Test] + public void TestZeroBytes() + { + var issues = check.Run(getContext(byteLength: 0)).ToList(); + + Assert.That(issues, Has.Count.EqualTo(1)); + Assert.That(issues.Single().Template is CheckZeroByteFiles.IssueTemplateZeroBytes); + } + + [Test] + public void TestMissing() + { + Assert.IsEmpty(check.Run(getContextMissing())); + } + + private BeatmapVerifierContext getContext(long byteLength) + { + var mockStream = new Mock(); + mockStream.Setup(s => s.Length).Returns(byteLength); + + var mockWorkingBeatmap = new Mock(); + mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns(mockStream.Object); + + return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object); + } + + private BeatmapVerifierContext getContextMissing() + { + var mockWorkingBeatmap = new Mock(); + mockWorkingBeatmap.Setup(w => w.GetStream(It.IsAny())).Returns((Stream)null); + + return new BeatmapVerifierContext(beatmap, mockWorkingBeatmap.Object); + } + } +} diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index a40a6dac4c..8eb9452736 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Edit; @@ -55,8 +56,6 @@ namespace osu.Game.Tests.Editing composer.EditorBeatmap.Difficulty.SliderMultiplier = 1; composer.EditorBeatmap.ControlPointInfo.Clear(); - - composer.EditorBeatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 1 }); composer.EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }); }); @@ -73,13 +72,13 @@ namespace osu.Game.Tests.Editing [TestCase(2)] public void TestSpeedMultiplier(float multiplier) { - AddStep($"set multiplier = {multiplier}", () => + assertSnapDistance(100 * multiplier, new HitObject { - composer.EditorBeatmap.ControlPointInfo.Clear(); - composer.EditorBeatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = multiplier }); + DifficultyControlPoint = new DifficultyControlPoint + { + SliderVelocity = multiplier + } }); - - assertSnapDistance(100 * multiplier); } [TestCase(1)] @@ -197,20 +196,20 @@ namespace osu.Game.Tests.Editing assertSnappedDistance(400, 400); } - private void assertSnapDistance(float expectedDistance) - => AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(0) == expectedDistance); + private void assertSnapDistance(float expectedDistance, HitObject hitObject = null) + => AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(hitObject ?? new HitObject()) == expectedDistance); private void assertDurationToDistance(double duration, float expectedDistance) - => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(0, duration) == expectedDistance); + => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(new HitObject(), duration) == expectedDistance); private void assertDistanceToDuration(float distance, double expectedDuration) - => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(0, distance) == expectedDuration); + => AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(new HitObject(), distance) == expectedDuration); private void assertSnappedDuration(float distance, double expectedDuration) - => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.GetSnappedDurationFromDistance(0, distance) == expectedDuration); + => AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.GetSnappedDurationFromDistance(new HitObject(), distance) == expectedDuration); private void assertSnappedDistance(float distance, float expectedDistance) - => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.GetSnappedDistanceFromDistance(0, distance) == expectedDistance); + => AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.GetSnappedDistanceFromDistance(new HitObject(), distance) == expectedDistance); private class TestHitObjectComposer : OsuHitObjectComposer { diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs index fabb016d5f..cfda4f6422 100644 --- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs +++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestAddRedundantDifficulty() { - var cpi = new ControlPointInfo(); + var cpi = new LegacyControlPointInfo(); cpi.Add(0, new DifficultyControlPoint()); // is redundant cpi.Add(1000, new DifficultyControlPoint()); // is redundant @@ -55,7 +55,7 @@ namespace osu.Game.Tests.NonVisual Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(0)); Assert.That(cpi.AllControlPoints.Count(), Is.EqualTo(0)); - cpi.Add(1000, new DifficultyControlPoint { SpeedMultiplier = 2 }); // is not redundant + cpi.Add(1000, new DifficultyControlPoint { SliderVelocity = 2 }); // is not redundant Assert.That(cpi.Groups.Count, Is.EqualTo(1)); Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1)); @@ -159,7 +159,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestAddControlPointToGroup() { - var cpi = new ControlPointInfo(); + var cpi = new LegacyControlPointInfo(); var group = cpi.GroupAt(1000, true); Assert.That(cpi.Groups.Count, Is.EqualTo(1)); @@ -174,23 +174,23 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestAddDuplicateControlPointToGroup() { - var cpi = new ControlPointInfo(); + var cpi = new LegacyControlPointInfo(); var group = cpi.GroupAt(1000, true); Assert.That(cpi.Groups.Count, Is.EqualTo(1)); group.Add(new DifficultyControlPoint()); - group.Add(new DifficultyControlPoint { SpeedMultiplier = 2 }); + group.Add(new DifficultyControlPoint { SliderVelocity = 2 }); Assert.That(group.ControlPoints.Count, Is.EqualTo(1)); Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(1)); - Assert.That(cpi.DifficultyPoints.First().SpeedMultiplier, Is.EqualTo(2)); + Assert.That(cpi.DifficultyPoints.First().SliderVelocity, Is.EqualTo(2)); } [Test] public void TestRemoveControlPointFromGroup() { - var cpi = new ControlPointInfo(); + var cpi = new LegacyControlPointInfo(); var group = cpi.GroupAt(1000, true); Assert.That(cpi.Groups.Count, Is.EqualTo(1)); @@ -208,14 +208,14 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestOrdering() { - var cpi = new ControlPointInfo(); + var cpi = new LegacyControlPointInfo(); cpi.Add(0, new TimingControlPoint()); cpi.Add(1000, new TimingControlPoint { BeatLength = 500 }); cpi.Add(10000, new TimingControlPoint { BeatLength = 200 }); cpi.Add(5000, new TimingControlPoint { BeatLength = 100 }); - cpi.Add(3000, new DifficultyControlPoint { SpeedMultiplier = 2 }); - cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SpeedMultiplier = 4 }); + cpi.Add(3000, new DifficultyControlPoint { SliderVelocity = 2 }); + cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SliderVelocity = 4 }); cpi.GroupAt(1000).Add(new SampleControlPoint { SampleVolume = 0 }); cpi.GroupAt(8000, true).Add(new EffectControlPoint { KiaiMode = true }); @@ -230,14 +230,14 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestClear() { - var cpi = new ControlPointInfo(); + var cpi = new LegacyControlPointInfo(); cpi.Add(0, new TimingControlPoint()); cpi.Add(1000, new TimingControlPoint { BeatLength = 500 }); cpi.Add(10000, new TimingControlPoint { BeatLength = 200 }); cpi.Add(5000, new TimingControlPoint { BeatLength = 100 }); - cpi.Add(3000, new DifficultyControlPoint { SpeedMultiplier = 2 }); - cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SpeedMultiplier = 4 }); + cpi.Add(3000, new DifficultyControlPoint { SliderVelocity = 2 }); + cpi.GroupAt(7000, true).Add(new DifficultyControlPoint { SliderVelocity = 4 }); cpi.GroupAt(1000).Add(new SampleControlPoint { SampleVolume = 0 }); cpi.GroupAt(8000, true).Add(new EffectControlPoint { KiaiMode = true }); diff --git a/osu.Game.Tests/Resources/Samples/blank.wav b/osu.Game.Tests/Resources/Samples/blank.wav new file mode 100644 index 0000000000..878bf23cea Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/blank.wav differ diff --git a/osu.Game.Tests/Resources/Samples/corrupt.wav b/osu.Game.Tests/Resources/Samples/corrupt.wav new file mode 100644 index 0000000000..87c7de4b7b Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/corrupt.wav differ diff --git a/osu.Game.Tests/Resources/Samples/test-sample-cut.mp3 b/osu.Game.Tests/Resources/Samples/test-sample-cut.mp3 new file mode 100644 index 0000000000..003fe23dca Binary files /dev/null and b/osu.Game.Tests/Resources/Samples/test-sample-cut.mp3 differ diff --git a/osu.Game.Tests/Resources/Videos/test-video-with-audio.mp4 b/osu.Game.Tests/Resources/Videos/test-video-with-audio.mp4 new file mode 100644 index 0000000000..5d380ab50c Binary files /dev/null and b/osu.Game.Tests/Resources/Videos/test-video-with-audio.mp4 differ diff --git a/osu.Game.Tests/Resources/Videos/test-video-with-track-but-no-audio.mp4 b/osu.Game.Tests/Resources/Videos/test-video-with-track-but-no-audio.mp4 new file mode 100644 index 0000000000..7cdd1939e9 Binary files /dev/null and b/osu.Game.Tests/Resources/Videos/test-video-with-track-but-no-audio.mp4 differ diff --git a/osu.Game.Tests/Resources/Videos/test-video.mp4 b/osu.Game.Tests/Resources/Videos/test-video.mp4 new file mode 100644 index 0000000000..795483c096 Binary files /dev/null and b/osu.Game.Tests/Resources/Videos/test-video.mp4 differ diff --git a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs index 0107632f6e..99be72e958 100644 --- a/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs +++ b/osu.Game.Tests/Visual/Audio/TestSceneAudioFilter.cs @@ -163,5 +163,11 @@ namespace osu.Game.Tests.Visual.Audio } private void waitTrackPlay() => AddWaitStep("Let track play", 10); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + track?.Dispose(); + } } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index 11830ebe35..d1efd22d6f 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components; @@ -81,7 +82,7 @@ namespace osu.Game.Tests.Visual.Editing public new float DistanceSpacing => base.DistanceSpacing; public TestDistanceSnapGrid(double? endTime = null) - : base(grid_position, 0, endTime) + : base(new HitObject(), grid_position, 0, endTime) { } @@ -158,15 +159,15 @@ namespace osu.Game.Tests.Visual.Editing public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0); - public float GetBeatSnapDistanceAt(double referenceTime) => 10; + public float GetBeatSnapDistanceAt(HitObject referenceObject) => 10; - public float DurationToDistance(double referenceTime, double duration) => (float)duration; + public float DurationToDistance(HitObject referenceObject, double duration) => (float)duration; - public double DistanceToDuration(double referenceTime, float distance) => distance; + public double DistanceToDuration(HitObject referenceObject, float distance) => distance; - public double GetSnappedDurationFromDistance(double referenceTime, float distance) => 0; + public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => 0; - public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => 0; + public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => 0; } } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs index f0aa3e2350..ab2bc4649a 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSaving.cs @@ -8,6 +8,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Menu; using osu.Game.Screens.Select; using osuTK.Input; @@ -30,23 +31,35 @@ namespace osu.Game.Tests.Visual.Editing PushAndConfirm(() => new EditorLoader()); - AddUntilStep("wait for editor load", () => editor != null); + AddUntilStep("wait for editor load", () => editor?.IsLoaded == true); - AddStep("Set overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty = 7); + AddUntilStep("wait for metadata screen load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); - AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint())); + // We intentionally switch away from the metadata screen, else there is a feedback loop with the textbox handling which causes metadata changes below to get overwritten. AddStep("Enter compose mode", () => InputManager.Key(Key.F1)); AddUntilStep("Wait for compose mode load", () => editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + AddStep("Set overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty = 7); + AddStep("Set artist and title", () => + { + editorBeatmap.BeatmapInfo.Metadata.Artist = "artist"; + editorBeatmap.BeatmapInfo.Metadata.Title = "title"; + }); + AddStep("Set difficulty name", () => editorBeatmap.BeatmapInfo.Version = "difficulty"); + + AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint())); + AddStep("Change to placement mode", () => InputManager.Key(Key.Number2)); AddStep("Move to playfield", () => InputManager.MoveMouseTo(Game.ScreenSpaceDrawQuad.Centre)); AddStep("Place single hitcircle", () => InputManager.Click(MouseButton.Left)); - AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1); + checkMutations(); AddStep("Save", () => InputManager.Keys(PlatformAction.Save)); + checkMutations(); + AddStep("Exit", () => InputManager.Key(Key.Escape)); AddUntilStep("Wait for main menu", () => Game.ScreenStack.CurrentScreen is MainMenu); @@ -58,8 +71,16 @@ namespace osu.Game.Tests.Visual.Editing AddStep("Enter editor", () => InputManager.Key(Key.Number5)); AddUntilStep("Wait for editor load", () => editor != null); + + checkMutations(); + } + + private void checkMutations() + { AddAssert("Beatmap contains single hitcircle", () => editorBeatmap.HitObjects.Count == 1); AddAssert("Beatmap has correct overall difficulty", () => editorBeatmap.Difficulty.OverallDifficulty == 7); + AddAssert("Beatmap has correct metadata", () => editorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && editorBeatmap.BeatmapInfo.Metadata.Title == "title"); + AddAssert("Beatmap has correct difficulty name", () => editorBeatmap.BeatmapInfo.Version == "difficulty"); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs index 2f15e549f7..283fe594ea 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs @@ -93,9 +93,9 @@ namespace osu.Game.Tests.Visual.Gameplay private IList testControlPoints => new List { - new MultiplierControlPoint(time_range) { DifficultyPoint = { SpeedMultiplier = 1.25 } }, - new MultiplierControlPoint(1.5 * time_range) { DifficultyPoint = { SpeedMultiplier = 1 } }, - new MultiplierControlPoint(2 * time_range) { DifficultyPoint = { SpeedMultiplier = 1.5 } } + new MultiplierControlPoint(time_range) { EffectPoint = { ScrollSpeed = 1.25 } }, + new MultiplierControlPoint(1.5 * time_range) { EffectPoint = { ScrollSpeed = 1 } }, + new MultiplierControlPoint(2 * time_range) { EffectPoint = { ScrollSpeed = 1.5 } } }; [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 0b70703870..2bb77395ef 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -564,11 +564,18 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); - AddRepeatStep("click spectate button", () => + AddUntilStep("wait for ready button to be enabled", () => readyButton.ChildrenOfType().Single().Enabled.Value); + + AddStep("click ready button", () => { - InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.MoveMouseTo(readyButton); InputManager.Click(MouseButton.Left); - }, 2); + }); + + AddUntilStep("wait for player to be ready", () => client.Room?.Users[0].State == MultiplayerUserState.Ready); + AddUntilStep("wait for ready button to be enabled", () => readyButton.ChildrenOfType().Single().Enabled.Value); + + AddStep("click start button", () => InputManager.Click(MouseButton.Left)); AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); @@ -582,6 +589,8 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for results", () => Stack.CurrentScreen is ResultsScreen); } + private MultiplayerReadyButton readyButton => this.ChildrenOfType().Single(); + private void createRoom(Func room) { AddUntilStep("wait for lounge", () => multiplayerScreen.ChildrenOfType().SingleOrDefault()?.IsLoaded == true); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs b/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs index cb7c334656..bd723eeed6 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneStartupImport.cs @@ -4,7 +4,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; -using osu.Game.Database; +using osu.Game.Overlays.Notifications; using osu.Game.Tests.Resources; namespace osu.Game.Tests.Visual.Navigation @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestImportCreatedNotification() { - AddUntilStep("Import notification was presented", () => Game.Notifications.ChildrenOfType().Count() == 1); + AddUntilStep("Import notification was presented", () => Game.Notifications.ChildrenOfType().Count() == 1); } } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index a5b90e6655..0ae4e0c5dc 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -205,7 +205,7 @@ namespace osu.Game.Tests.Visual.SongSelect private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) => AddAssert($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'", // A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872 - () => shouldContain == (getCollectionDropdownItems().Any(i => i.ChildrenOfType().OfType().First().Text == collectionName))); + () => shouldContain == (getCollectionDropdownItems().Any(i => i.ChildrenOfType().OfType().First().Text == collectionName))); private IconButton getAddOrRemoveButton(int index) => getCollectionDropdownItems().ElementAt(index).ChildrenOfType().Single(); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs index 393420e700..1b7f65f9a0 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs @@ -1,11 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; namespace osu.Game.Tests.Visual.UserInterface { @@ -19,28 +23,62 @@ namespace osu.Game.Tests.Visual.UserInterface { AddStep("create component", () => { - LabelledSliderBar component; + FillFlowContainer flow; - Child = new Container + Child = flow = new FillFlowContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, Width = 500, AutoSizeAxes = Axes.Y, - Child = component = new LabelledSliderBar + Direction = FillDirection.Vertical, + Children = new Drawable[] { - Current = new BindableDouble(5) + new LabelledSliderBar { - MinValue = 0, - MaxValue = 10, - Precision = 1, - } - } + Current = new BindableDouble(5) + { + MinValue = 0, + MaxValue = 10, + Precision = 1, + }, + Label = "a sample component", + Description = hasDescription ? "this text describes the component" : string.Empty, + }, + }, }; - component.Label = "a sample component"; - component.Description = hasDescription ? "this text describes the component" : string.Empty; + foreach (var colour in Enum.GetValues(typeof(OverlayColourScheme)).OfType()) + { + flow.Add(new OverlayColourContainer(colour) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = new LabelledSliderBar + { + Current = new BindableDouble(5) + { + MinValue = 0, + MaxValue = 10, + Precision = 1, + }, + Label = "a sample component", + Description = hasDescription ? "this text describes the component" : string.Empty, + } + }); + } }); } + + private class OverlayColourContainer : Container + { + [Cached] + private OverlayColourProvider colourProvider; + + public OverlayColourContainer(OverlayColourScheme scheme) + { + colourProvider = new OverlayColourProvider(scheme); + } + } } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsCheckbox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsCheckbox.cs new file mode 100644 index 0000000000..fb04c5bad0 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSettingsCheckbox.cs @@ -0,0 +1,68 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Overlays; +using osu.Game.Overlays.Settings; +using osuTK; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneSettingsCheckbox : OsuTestScene + { + [TestCase] + public void TestCheckbox() + { + AddStep("create component", () => + { + FillFlowContainer flow; + + Child = flow = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 500, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(5), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new SettingsCheckbox + { + LabelText = "a sample component", + }, + }, + }; + + foreach (var colour1 in Enum.GetValues(typeof(OverlayColourScheme)).OfType()) + { + flow.Add(new OverlayColourContainer(colour1) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = new SettingsCheckbox + { + LabelText = "a sample component", + } + }); + } + }); + } + + private class OverlayColourContainer : Container + { + [Cached] + private OverlayColourProvider colourProvider; + + public OverlayColourContainer(OverlayColourScheme scheme) + { + colourProvider = new OverlayColourProvider(scheme); + } + } + } +} diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 14175f251b..562cbfabf0 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -176,11 +176,6 @@ namespace osu.Game.Beatmaps } } - /// - /// Fired when the user requests to view the resulting import. - /// - public Action>> PresentImport { set => beatmapModelManager.PostImport = value; } - /// /// Delete a beatmap difficulty. /// @@ -338,5 +333,14 @@ namespace osu.Game.Beatmaps } #endregion + + #region Implementation of IPostImports + + public Action>> PostImport + { + set => beatmapModelManager.PostImport = value; + } + + #endregion } } diff --git a/osu.Game/Beatmaps/BeatmapModelManager.cs b/osu.Game/Beatmaps/BeatmapModelManager.cs index 9c0fc5ef8a..76019a15ae 100644 --- a/osu.Game/Beatmaps/BeatmapModelManager.cs +++ b/osu.Game/Beatmaps/BeatmapModelManager.cs @@ -192,6 +192,13 @@ namespace osu.Game.Beatmaps { var setInfo = beatmapInfo.BeatmapSet; + // Difficulty settings must be copied first due to the clone in `Beatmap<>.BeatmapInfo_Set`. + // This should hopefully be temporary, assuming said clone is eventually removed. + beatmapInfo.BaseDifficulty.CopyFrom(beatmapContent.Difficulty); + + // All changes to metadata are made in the provided beatmapInfo, so this should be copied to the `IBeatmap` before encoding. + beatmapContent.BeatmapInfo = beatmapInfo; + using (var stream = new MemoryStream()) { using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) @@ -202,7 +209,6 @@ namespace osu.Game.Beatmaps using (ContextFactory.GetForWrite()) { beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == beatmapInfo.ID); - beatmapInfo.BaseDifficulty.CopyFrom(beatmapContent.Difficulty); var metadata = beatmapInfo.Metadata ?? setInfo.Metadata; diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs index 8203f2e968..4079a0cd5f 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs @@ -15,11 +15,9 @@ namespace osu.Game.Beatmaps.ControlPoints /// The time at which the control point takes effect. /// [JsonIgnore] - public double Time => controlPointGroup?.Time ?? 0; + public double Time { get; set; } - private ControlPointGroup controlPointGroup; - - public void AttachGroup(ControlPointGroup pointGroup) => controlPointGroup = pointGroup; + public void AttachGroup(ControlPointGroup pointGroup) => Time = pointGroup.Time; public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time); @@ -46,6 +44,7 @@ namespace osu.Game.Beatmaps.ControlPoints public virtual void CopyFrom(ControlPoint other) { + Time = other.Time; } } } diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index 3ff40fe194..9d738ecbfb 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -33,14 +33,6 @@ namespace osu.Game.Beatmaps.ControlPoints private readonly SortedList timingPoints = new SortedList(Comparer.Default); - /// - /// All difficulty points. - /// - [JsonProperty] - public IReadOnlyList DifficultyPoints => difficultyPoints; - - private readonly SortedList difficultyPoints = new SortedList(Comparer.Default); - /// /// All effect points. /// @@ -55,14 +47,6 @@ namespace osu.Game.Beatmaps.ControlPoints [JsonIgnore] public IEnumerable AllControlPoints => Groups.SelectMany(g => g.ControlPoints).ToArray(); - /// - /// Finds the difficulty control point that is active at . - /// - /// The time to find the difficulty control point at. - /// The difficulty control point. - [NotNull] - public DifficultyControlPoint DifficultyPointAt(double time) => BinarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT); - /// /// Finds the effect control point that is active at . /// @@ -100,7 +84,6 @@ namespace osu.Game.Beatmaps.ControlPoints { groups.Clear(); timingPoints.Clear(); - difficultyPoints.Clear(); effectPoints.Clear(); } @@ -277,10 +260,6 @@ namespace osu.Game.Beatmaps.ControlPoints case EffectControlPoint _: existing = EffectPointAt(time); break; - - case DifficultyControlPoint _: - existing = DifficultyPointAt(time); - break; } return newPoint?.IsRedundant(existing) == true; @@ -298,9 +277,8 @@ namespace osu.Game.Beatmaps.ControlPoints effectPoints.Add(typed); break; - case DifficultyControlPoint typed: - difficultyPoints.Add(typed); - break; + default: + throw new ArgumentException($"A control point of unexpected type {controlPoint.GetType()} was added to this {nameof(ControlPointInfo)}"); } } @@ -315,10 +293,6 @@ namespace osu.Game.Beatmaps.ControlPoints case EffectControlPoint typed: effectPoints.Remove(typed); break; - - case DifficultyControlPoint typed: - difficultyPoints.Remove(typed); - break; } } diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index 8a6cfaf688..bf7ed8e6f5 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -7,17 +7,20 @@ using osuTK.Graphics; namespace osu.Game.Beatmaps.ControlPoints { + /// + /// Note that going forward, this control point type should always be assigned directly to HitObjects. + /// public class DifficultyControlPoint : ControlPoint { public static readonly DifficultyControlPoint DEFAULT = new DifficultyControlPoint { - SpeedMultiplierBindable = { Disabled = true }, + SliderVelocityBindable = { Disabled = true }, }; /// - /// The speed multiplier at this control point. + /// The slider velocity at this control point. /// - public readonly BindableDouble SpeedMultiplierBindable = new BindableDouble(1) + public readonly BindableDouble SliderVelocityBindable = new BindableDouble(1) { Precision = 0.01, Default = 1, @@ -28,21 +31,21 @@ namespace osu.Game.Beatmaps.ControlPoints public override Color4 GetRepresentingColour(OsuColour colours) => colours.Lime1; /// - /// The speed multiplier at this control point. + /// The slider velocity at this control point. /// - public double SpeedMultiplier + public double SliderVelocity { - get => SpeedMultiplierBindable.Value; - set => SpeedMultiplierBindable.Value = value; + get => SliderVelocityBindable.Value; + set => SliderVelocityBindable.Value = value; } public override bool IsRedundant(ControlPoint existing) => existing is DifficultyControlPoint existingDifficulty - && SpeedMultiplier == existingDifficulty.SpeedMultiplier; + && SliderVelocity == existingDifficulty.SliderVelocity; public override void CopyFrom(ControlPoint other) { - SpeedMultiplier = ((DifficultyControlPoint)other).SpeedMultiplier; + SliderVelocity = ((DifficultyControlPoint)other).SliderVelocity; base.CopyFrom(other); } diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs index 79bc88e773..7f550a52fc 100644 --- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs @@ -12,7 +12,8 @@ namespace osu.Game.Beatmaps.ControlPoints public static readonly EffectControlPoint DEFAULT = new EffectControlPoint { KiaiModeBindable = { Disabled = true }, - OmitFirstBarLineBindable = { Disabled = true } + OmitFirstBarLineBindable = { Disabled = true }, + ScrollSpeedBindable = { Disabled = true } }; /// @@ -20,6 +21,26 @@ namespace osu.Game.Beatmaps.ControlPoints /// public readonly BindableBool OmitFirstBarLineBindable = new BindableBool(); + /// + /// The relative scroll speed at this control point. + /// + public readonly BindableDouble ScrollSpeedBindable = new BindableDouble(1) + { + Precision = 0.01, + Default = 1, + MinValue = 0.01, + MaxValue = 10 + }; + + /// + /// The relative scroll speed. + /// + public double ScrollSpeed + { + get => ScrollSpeedBindable.Value; + set => ScrollSpeedBindable.Value = value; + } + public override Color4 GetRepresentingColour(OsuColour colours) => colours.Purple; /// @@ -49,12 +70,14 @@ namespace osu.Game.Beatmaps.ControlPoints => !OmitFirstBarLine && existing is EffectControlPoint existingEffect && KiaiMode == existingEffect.KiaiMode - && OmitFirstBarLine == existingEffect.OmitFirstBarLine; + && OmitFirstBarLine == existingEffect.OmitFirstBarLine + && ScrollSpeed == existingEffect.ScrollSpeed; public override void CopyFrom(ControlPoint other) { KiaiMode = ((EffectControlPoint)other).KiaiMode; OmitFirstBarLine = ((EffectControlPoint)other).OmitFirstBarLine; + ScrollSpeed = ((EffectControlPoint)other).ScrollSpeed; base.CopyFrom(other); } diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs index 4aa6a3d6e9..fb489f73b1 100644 --- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs @@ -8,6 +8,9 @@ using osuTK.Graphics; namespace osu.Game.Beatmaps.ControlPoints { + /// + /// Note that going forward, this control point type should always be assigned directly to HitObjects. + /// public class SampleControlPoint : ControlPoint { public const string DEFAULT_BANK = "normal"; diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index f71b148008..bef2d78f21 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -384,14 +384,21 @@ namespace osu.Game.Beatmaps.Formats addControlPoint(time, new LegacyDifficultyControlPoint(beatLength) #pragma warning restore 618 { - SpeedMultiplier = speedMultiplier, + SliderVelocity = speedMultiplier, }, timingChange); - addControlPoint(time, new EffectControlPoint + var effectPoint = new EffectControlPoint { KiaiMode = kiaiMode, OmitFirstBarLine = omitFirstBarSignature, - }, timingChange); + }; + + bool isOsuRuleset = beatmap.BeatmapInfo.RulesetID == 0; + // scrolling rulesets use effect points rather than difficulty points for scroll speed adjustments. + if (!isOsuRuleset) + effectPoint.ScrollSpeed = speedMultiplier; + + addControlPoint(time, effectPoint, timingChange); addControlPoint(time, new LegacySampleControlPoint { diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 74b3c178cd..1dc270ee63 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -170,33 +170,30 @@ namespace osu.Game.Beatmaps.Formats if (beatmap.ControlPointInfo.Groups.Count == 0) return; + var legacyControlPoints = new LegacyControlPointInfo(); + foreach (var point in beatmap.ControlPointInfo.AllControlPoints) + legacyControlPoints.Add(point.Time, point.DeepClone()); + writer.WriteLine("[TimingPoints]"); - if (!(beatmap.ControlPointInfo is LegacyControlPointInfo)) + SampleControlPoint lastRelevantSamplePoint = null; + DifficultyControlPoint lastRelevantDifficultyPoint = null; + + bool isOsuRuleset = beatmap.BeatmapInfo.RulesetID == 0; + + // iterate over hitobjects and pull out all required sample and difficulty changes + extractDifficultyControlPoints(beatmap.HitObjects); + extractSampleControlPoints(beatmap.HitObjects); + + // handle scroll speed, which is stored as "slider velocity" in legacy formats. + // this is relevant for scrolling ruleset beatmaps. + if (!isOsuRuleset) { - var legacyControlPoints = new LegacyControlPointInfo(); - - foreach (var point in beatmap.ControlPointInfo.AllControlPoints) - legacyControlPoints.Add(point.Time, point.DeepClone()); - - beatmap.ControlPointInfo = legacyControlPoints; - - SampleControlPoint lastRelevantSamplePoint = null; - - // iterate over hitobjects and pull out all required sample changes - foreach (var h in beatmap.HitObjects) - { - var hSamplePoint = h.SampleControlPoint; - - if (!hSamplePoint.IsRedundant(lastRelevantSamplePoint)) - { - legacyControlPoints.Add(hSamplePoint.Time, hSamplePoint); - lastRelevantSamplePoint = hSamplePoint; - } - } + foreach (var point in legacyControlPoints.EffectPoints) + legacyControlPoints.Add(point.Time, new DifficultyControlPoint { SliderVelocity = point.ScrollSpeed }); } - foreach (var group in beatmap.ControlPointInfo.Groups) + foreach (var group in legacyControlPoints.Groups) { var groupTimingPoint = group.ControlPoints.OfType().FirstOrDefault(); @@ -209,16 +206,16 @@ namespace osu.Game.Beatmaps.Formats } // Output any remaining effects as secondary non-timing control point. - var difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(group.Time); + var difficultyPoint = legacyControlPoints.DifficultyPointAt(group.Time); writer.Write(FormattableString.Invariant($"{group.Time},")); - writer.Write(FormattableString.Invariant($"{-100 / difficultyPoint.SpeedMultiplier},")); + writer.Write(FormattableString.Invariant($"{-100 / difficultyPoint.SliderVelocity},")); outputControlPointAt(group.Time, false); } void outputControlPointAt(double time, bool isTimingPoint) { - var samplePoint = ((LegacyControlPointInfo)beatmap.ControlPointInfo).SamplePointAt(time); - var effectPoint = beatmap.ControlPointInfo.EffectPointAt(time); + var samplePoint = legacyControlPoints.SamplePointAt(time); + var effectPoint = legacyControlPoints.EffectPointAt(time); // Apply the control point to a hit sample to uncover legacy properties (e.g. suffix) HitSampleInfo tempHitSample = samplePoint.ApplyTo(new ConvertHitObjectParser.LegacyHitSampleInfo(string.Empty)); @@ -230,7 +227,7 @@ namespace osu.Game.Beatmaps.Formats if (effectPoint.OmitFirstBarLine) effectFlags |= LegacyEffectFlags.OmitFirstBarLine; - writer.Write(FormattableString.Invariant($"{(int)beatmap.ControlPointInfo.TimingPointAt(time).TimeSignature},")); + writer.Write(FormattableString.Invariant($"{(int)legacyControlPoints.TimingPointAt(time).TimeSignature},")); writer.Write(FormattableString.Invariant($"{(int)toLegacySampleBank(tempHitSample.Bank)},")); writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample)},")); writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},")); @@ -238,6 +235,55 @@ namespace osu.Game.Beatmaps.Formats writer.Write(FormattableString.Invariant($"{(int)effectFlags}")); writer.WriteLine(); } + + IEnumerable collectDifficultyControlPoints(IEnumerable hitObjects) + { + if (!isOsuRuleset) + yield break; + + foreach (var hitObject in hitObjects) + { + yield return hitObject.DifficultyControlPoint; + + foreach (var nested in collectDifficultyControlPoints(hitObject.NestedHitObjects)) + yield return nested; + } + } + + void extractDifficultyControlPoints(IEnumerable hitObjects) + { + foreach (var hDifficultyPoint in collectDifficultyControlPoints(hitObjects).OrderBy(dp => dp.Time)) + { + if (!hDifficultyPoint.IsRedundant(lastRelevantDifficultyPoint)) + { + legacyControlPoints.Add(hDifficultyPoint.Time, hDifficultyPoint); + lastRelevantDifficultyPoint = hDifficultyPoint; + } + } + } + + IEnumerable collectSampleControlPoints(IEnumerable hitObjects) + { + foreach (var hitObject in hitObjects) + { + yield return hitObject.SampleControlPoint; + + foreach (var nested in collectSampleControlPoints(hitObject.NestedHitObjects)) + yield return nested; + } + } + + void extractSampleControlPoints(IEnumerable hitObject) + { + foreach (var hSamplePoint in collectSampleControlPoints(hitObject).OrderBy(sp => sp.Time)) + { + if (!hSamplePoint.IsRedundant(lastRelevantSamplePoint)) + { + legacyControlPoints.Add(hSamplePoint.Time, hSamplePoint); + lastRelevantSamplePoint = hSamplePoint; + } + } + } } private void handleColours(TextWriter writer) diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 20080308f9..cf6c827af5 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -181,7 +181,7 @@ namespace osu.Game.Beatmaps.Formats public LegacyDifficultyControlPoint() { - SpeedMultiplierBindable.Precision = double.Epsilon; + SliderVelocityBindable.Precision = double.Epsilon; } public override void CopyFrom(ControlPoint other) diff --git a/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs b/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs index ff0ca5ebe1..2b0a2e7a4d 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyControlPointInfo.cs @@ -1,9 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using JetBrains.Annotations; using Newtonsoft.Json; -using osu.Framework.Bindables; +using osu.Framework.Lists; using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Beatmaps.Legacy @@ -14,9 +15,9 @@ namespace osu.Game.Beatmaps.Legacy /// All sound points. /// [JsonProperty] - public IBindableList SamplePoints => samplePoints; + public IReadOnlyList SamplePoints => samplePoints; - private readonly BindableList samplePoints = new BindableList(); + private readonly SortedList samplePoints = new SortedList(Comparer.Default); /// /// Finds the sound control point that is active at . @@ -26,35 +27,76 @@ namespace osu.Game.Beatmaps.Legacy [NotNull] public SampleControlPoint SamplePointAt(double time) => BinarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : SampleControlPoint.DEFAULT); + /// + /// All difficulty points. + /// + [JsonProperty] + public IReadOnlyList DifficultyPoints => difficultyPoints; + + private readonly SortedList difficultyPoints = new SortedList(Comparer.Default); + + /// + /// Finds the difficulty control point that is active at . + /// + /// The time to find the difficulty control point at. + /// The difficulty control point. + [NotNull] + public DifficultyControlPoint DifficultyPointAt(double time) => BinarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT); + public override void Clear() { base.Clear(); samplePoints.Clear(); + difficultyPoints.Clear(); } protected override bool CheckAlreadyExisting(double time, ControlPoint newPoint) { - if (newPoint is SampleControlPoint) + switch (newPoint) { - var existing = BinarySearch(SamplePoints, time); - return newPoint.IsRedundant(existing); - } + case SampleControlPoint _: + // intentionally don't use SamplePointAt (we always need to consider the first sample point). + var existing = BinarySearch(SamplePoints, time); + return newPoint.IsRedundant(existing); - return base.CheckAlreadyExisting(time, newPoint); + case DifficultyControlPoint _: + return newPoint.IsRedundant(DifficultyPointAt(time)); + + default: + return base.CheckAlreadyExisting(time, newPoint); + } } protected override void GroupItemAdded(ControlPoint controlPoint) { - if (controlPoint is SampleControlPoint typed) - samplePoints.Add(typed); + switch (controlPoint) + { + case SampleControlPoint typed: + samplePoints.Add(typed); + return; - base.GroupItemAdded(controlPoint); + case DifficultyControlPoint typed: + difficultyPoints.Add(typed); + return; + + default: + base.GroupItemAdded(controlPoint); + break; + } } protected override void GroupItemRemoved(ControlPoint controlPoint) { - if (controlPoint is SampleControlPoint typed) - samplePoints.Remove(typed); + switch (controlPoint) + { + case SampleControlPoint typed: + samplePoints.Remove(typed); + break; + + case DifficultyControlPoint typed: + difficultyPoints.Remove(typed); + break; + } base.GroupItemRemoved(controlPoint); } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 18adecb7aa..d2c0f7de0f 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -189,11 +189,14 @@ namespace osu.Game.Beatmaps /// public void CancelAsyncLoad() { - loadCancellation?.Cancel(); - loadCancellation = new CancellationTokenSource(); + lock (beatmapFetchLock) + { + loadCancellation?.Cancel(); + loadCancellation = new CancellationTokenSource(); - if (beatmapLoadTask?.IsCompleted != true) - beatmapLoadTask = null; + if (beatmapLoadTask?.IsCompleted != true) + beatmapLoadTask = null; + } } private CancellationTokenSource createCancellationTokenSource(TimeSpan? timeout) @@ -205,19 +208,27 @@ namespace osu.Game.Beatmaps return new CancellationTokenSource(timeout ?? TimeSpan.FromSeconds(10)); } - private Task loadBeatmapAsync() => beatmapLoadTask ??= Task.Factory.StartNew(() => + private readonly object beatmapFetchLock = new object(); + + private Task loadBeatmapAsync() { - // Todo: Handle cancellation during beatmap parsing - var b = GetBeatmap() ?? new Beatmap(); + lock (beatmapFetchLock) + { + return beatmapLoadTask ??= Task.Factory.StartNew(() => + { + // Todo: Handle cancellation during beatmap parsing + var b = GetBeatmap() ?? new Beatmap(); - // The original beatmap version needs to be preserved as the database doesn't contain it - BeatmapInfo.BeatmapVersion = b.BeatmapInfo.BeatmapVersion; + // The original beatmap version needs to be preserved as the database doesn't contain it + BeatmapInfo.BeatmapVersion = b.BeatmapInfo.BeatmapVersion; - // Use the database-backed info for more up-to-date values (beatmap id, ranked status, etc) - b.BeatmapInfo = BeatmapInfo; + // Use the database-backed info for more up-to-date values (beatmap id, ranked status, etc) + b.BeatmapInfo = BeatmapInfo; - return b; - }, loadCancellation.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + return b; + }, loadCancellation.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default); + } + } public override string ToString() => BeatmapInfo.ToString(); diff --git a/osu.Game/Beatmaps/WorkingBeatmapCache.cs b/osu.Game/Beatmaps/WorkingBeatmapCache.cs index ad3e890b3a..cf83345e2a 100644 --- a/osu.Game/Beatmaps/WorkingBeatmapCache.cs +++ b/osu.Game/Beatmaps/WorkingBeatmapCache.cs @@ -66,8 +66,12 @@ namespace osu.Game.Beatmaps lock (workingCache) { var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == info.ID); + if (working != null) + { + Logger.Log($"Invalidating working beatmap cache for {info}"); workingCache.Remove(working); + } } } @@ -86,6 +90,7 @@ namespace osu.Game.Beatmaps lock (workingCache) { var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID); + if (working != null) return working; diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index c235fc7728..84e33e3f36 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -30,7 +30,7 @@ namespace osu.Game.Database /// /// The model type. /// The associated file join type. - public abstract class ArchiveModelManager : ICanAcceptFiles, IModelManager, IModelFileManager, IPostImports + public abstract class ArchiveModelManager : ICanAcceptFiles, IModelManager, IModelFileManager where TModel : class, IHasFiles, IHasPrimaryKey, ISoftDelete where TFileModel : class, INamedFileInfo, new() { diff --git a/osu.Game/Database/IModelImporter.cs b/osu.Game/Database/IModelImporter.cs index 8e658cb0f5..479f33c3b4 100644 --- a/osu.Game/Database/IModelImporter.cs +++ b/osu.Game/Database/IModelImporter.cs @@ -13,7 +13,7 @@ namespace osu.Game.Database /// A class which handles importing of associated models to the game store. /// /// The model type. - public interface IModelImporter : IPostNotifications + public interface IModelImporter : IPostNotifications, IPostImports where TModel : class { /// diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs index c3810eb441..82d51e365e 100644 --- a/osu.Game/Database/RealmContextFactory.cs +++ b/osu.Game/Database/RealmContextFactory.cs @@ -5,7 +5,6 @@ using System; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Development; -using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Framework.Statistics; @@ -18,7 +17,7 @@ namespace osu.Game.Database /// /// A factory which provides both the main (update thread bound) realm context and creates contexts for async usage. /// - public class RealmContextFactory : Component, IRealmFactory + public class RealmContextFactory : IDisposable, IRealmFactory { private readonly Storage storage; @@ -79,10 +78,11 @@ namespace osu.Game.Database /// public bool Compact() => Realm.Compact(getConfiguration()); - protected override void Update() + /// + /// Perform a blocking refresh on the main realm context. + /// + public void Refresh() { - base.Update(); - lock (contextLock) { if (context?.Refresh() == true) @@ -92,7 +92,7 @@ namespace osu.Game.Database public Realm CreateContext() { - if (IsDisposed) + if (isDisposed) throw new ObjectDisposedException(nameof(RealmContextFactory)); try @@ -132,7 +132,7 @@ namespace osu.Game.Database /// An which should be disposed to end the blocking section. public IDisposable BlockAllOperations() { - if (IsDisposed) + if (isDisposed) throw new ObjectDisposedException(nameof(RealmContextFactory)); if (!ThreadSafety.IsUpdateThread) @@ -176,21 +176,23 @@ namespace osu.Game.Database }); } - protected override void Dispose(bool isDisposing) + private bool isDisposed; + + public void Dispose() { lock (contextLock) { context?.Dispose(); } - if (!IsDisposed) + if (!isDisposed) { // intentionally block context creation indefinitely. this ensures that nothing can start consuming a new context after disposal. contextCreationLock.Wait(); contextCreationLock.Dispose(); - } - base.Dispose(isDisposing); + isDisposed = true; + } } } } diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs index aaad72f65c..017ea6ec32 100644 --- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -1,11 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Game.Overlays; using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -141,12 +144,12 @@ namespace osu.Game.Graphics.Containers Child = box = new Box { RelativeSizeAxes = Axes.Both }; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + [BackgroundDependencyLoader(true)] + private void load(OverlayColourProvider? colourProvider, OsuColour colours) { Colour = defaultColour = colours.Gray8; hoverColour = colours.GrayF; - highlightColour = colours.Green; + highlightColour = colourProvider?.Highlight1 ?? colours.Green; } public override void ResizeTo(float val, int duration = 0, Easing easing = Easing.None) diff --git a/osu.Game/Graphics/UserInterface/Nub.cs b/osu.Game/Graphics/UserInterface/Nub.cs index 6807d007bb..8f0fed580f 100644 --- a/osu.Game/Graphics/UserInterface/Nub.cs +++ b/osu.Game/Graphics/UserInterface/Nub.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using JetBrains.Annotations; using osuTK; using osuTK.Graphics; using osu.Framework.Allocation; @@ -12,63 +13,74 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterface { - public class Nub : CircularContainer, IHasCurrentValue, IHasAccentColour + public class Nub : CompositeDrawable, IHasCurrentValue, IHasAccentColour { - public const float COLLAPSED_SIZE = 20; - public const float EXPANDED_SIZE = 40; + public const float HEIGHT = 15; + + public const float EXPANDED_SIZE = 50; private const float border_width = 3; - private const double animate_in_duration = 150; + private const double animate_in_duration = 200; private const double animate_out_duration = 500; + private readonly Box fill; + private readonly Container main; + public Nub() { - Box fill; + Size = new Vector2(EXPANDED_SIZE, HEIGHT); - Size = new Vector2(COLLAPSED_SIZE, 12); - - BorderColour = Color4.White; - BorderThickness = border_width; - - Masking = true; - - Children = new[] + InternalChildren = new[] { - fill = new Box + main = new CircularContainer { + BorderColour = Color4.White, + BorderThickness = border_width, + Masking = true, RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Children = new Drawable[] + { + fill = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true, + }, + } }, }; - - Current.ValueChanged += filled => - { - fill.FadeTo(filled.NewValue ? 1 : 0, 200, Easing.OutQuint); - this.TransformTo(nameof(BorderThickness), filled.NewValue ? 8.5f : border_width, 200, Easing.OutQuint); - }; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + [BackgroundDependencyLoader(true)] + private void load([CanBeNull] OverlayColourProvider colourProvider, OsuColour colours) { - AccentColour = colours.Pink; - GlowingAccentColour = colours.PinkLighter; - GlowColour = colours.PinkDarker; + AccentColour = colourProvider?.Highlight1 ?? colours.Pink; + GlowingAccentColour = colourProvider?.Highlight1.Lighten(0.2f) ?? colours.PinkLighter; + GlowColour = colourProvider?.Highlight1 ?? colours.PinkLighter; - EdgeEffect = new EdgeEffectParameters + main.EdgeEffect = new EdgeEffectParameters { Colour = GlowColour.Opacity(0), Type = EdgeEffectType.Glow, - Radius = 10, - Roundness = 8, + Radius = 8, + Roundness = 5, }; } + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(onCurrentValueChanged, true); + } + private bool glowing; public bool Glowing @@ -80,28 +92,17 @@ namespace osu.Game.Graphics.UserInterface if (value) { - this.FadeColour(GlowingAccentColour, animate_in_duration, Easing.OutQuint); - FadeEdgeEffectTo(1, animate_in_duration, Easing.OutQuint); + main.FadeColour(GlowingAccentColour, animate_in_duration, Easing.OutQuint); + main.FadeEdgeEffectTo(0.2f, animate_in_duration, Easing.OutQuint); } else { - FadeEdgeEffectTo(0, animate_out_duration); - this.FadeColour(AccentColour, animate_out_duration); + main.FadeEdgeEffectTo(0, animate_out_duration, Easing.OutQuint); + main.FadeColour(AccentColour, animate_out_duration, Easing.OutQuint); } } } - public bool Expanded - { - set - { - if (value) - this.ResizeTo(new Vector2(EXPANDED_SIZE, 12), animate_in_duration, Easing.OutQuint); - else - this.ResizeTo(new Vector2(COLLAPSED_SIZE, 12), animate_out_duration, Easing.OutQuint); - } - } - private readonly Bindable current = new Bindable(); public Bindable Current @@ -126,7 +127,7 @@ namespace osu.Game.Graphics.UserInterface { accentColour = value; if (!Glowing) - Colour = value; + main.Colour = value; } } @@ -139,7 +140,7 @@ namespace osu.Game.Graphics.UserInterface { glowingAccentColour = value; if (Glowing) - Colour = value; + main.Colour = value; } } @@ -152,10 +153,22 @@ namespace osu.Game.Graphics.UserInterface { glowColour = value; - var effect = EdgeEffect; + var effect = main.EdgeEffect; effect.Colour = Glowing ? value : value.Opacity(0); - EdgeEffect = effect; + main.EdgeEffect = effect; } } + + private void onCurrentValueChanged(ValueChangedEvent filled) + { + fill.FadeTo(filled.NewValue ? 1 : 0, 200, Easing.OutQuint); + + if (filled.NewValue) + main.ResizeWidthTo(1, animate_in_duration, Easing.OutElasticHalf); + else + main.ResizeWidthTo(0.9f, animate_out_duration, Easing.OutElastic); + + main.TransformTo(nameof(BorderThickness), filled.NewValue ? 8.5f : border_width, 200, Easing.OutQuint); + } } } diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs index 5f2d884cd7..e8f80dec57 100644 --- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs @@ -9,16 +9,11 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; -using osuTK.Graphics; namespace osu.Game.Graphics.UserInterface { public class OsuCheckbox : Checkbox { - public Color4 CheckedColor { get; set; } = Color4.Cyan; - public Color4 UncheckedColor { get; set; } = Color4.White; - public int FadeDuration { get; set; } - /// /// Whether to play sounds when the state changes as a result of user interaction. /// @@ -104,14 +99,12 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnHover(HoverEvent e) { Nub.Glowing = true; - Nub.Expanded = true; return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { Nub.Glowing = false; - Nub.Expanded = false; base.OnHoverLost(e); } diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index fe88e6f78a..5831d9ab1f 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -1,8 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System.Linq; -using osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -14,13 +15,15 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; using osuTK; +using osuTK.Graphics; namespace osu.Game.Graphics.UserInterface { public class OsuDropdown : Dropdown, IHasAccentColour { - private const float corner_radius = 4; + private const float corner_radius = 5; private Color4 accentColour; @@ -34,11 +37,11 @@ namespace osu.Game.Graphics.UserInterface } } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + [BackgroundDependencyLoader(true)] + private void load(OverlayColourProvider? colourProvider, OsuColour colours) { if (accentColour == default) - accentColour = colours.PinkDarker; + accentColour = colourProvider?.Light4 ?? colours.PinkDarker; updateAccentColour(); } @@ -59,14 +62,13 @@ namespace osu.Game.Graphics.UserInterface { public override bool HandleNonPositionalInput => State == MenuState.Open; - private Sample sampleOpen; - private Sample sampleClose; + private Sample? sampleOpen; + private Sample? sampleClose; // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring public OsuDropdownMenu() { CornerRadius = corner_radius; - BackgroundColour = Color4.Black.Opacity(0.5f); MaskingContainer.CornerRadius = corner_radius; Alpha = 0; @@ -75,9 +77,11 @@ namespace osu.Game.Graphics.UserInterface ItemsContainer.Padding = new MarginPadding(5); } - [BackgroundDependencyLoader] - private void load(AudioManager audio) + [BackgroundDependencyLoader(true)] + private void load(OverlayColourProvider? colourProvider, AudioManager audio) { + BackgroundColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f); + sampleOpen = audio.Samples.Get(@"UI/dropdown-open"); sampleClose = audio.Samples.Get(@"UI/dropdown-close"); } @@ -159,6 +163,8 @@ namespace osu.Game.Graphics.UserInterface { BackgroundColourHover = accentColour ?? nonAccentHoverColour; BackgroundColourSelected = accentColour ?? nonAccentSelectedColour; + BackgroundColour = BackgroundColourHover.Opacity(0); + UpdateBackgroundColour(); UpdateForegroundColour(); } @@ -178,8 +184,6 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load(OsuColour colours) { - BackgroundColour = Color4.Transparent; - nonAccentHoverColour = colours.PinkDarker; nonAccentSelectedColour = Color4.Black.Opacity(0.5f); updateColours(); @@ -187,16 +191,29 @@ namespace osu.Game.Graphics.UserInterface AddInternal(new HoverSounds()); } + protected override void UpdateBackgroundColour() + { + if (!IsPreSelected && !IsSelected) + { + Background.FadeOut(600, Easing.OutQuint); + return; + } + + Background.FadeIn(100, Easing.OutQuint); + Background.FadeColour(IsPreSelected ? BackgroundColourHover : BackgroundColourSelected, 100, Easing.OutQuint); + } + protected override void UpdateForegroundColour() { base.UpdateForegroundColour(); - if (Foreground.Children.FirstOrDefault() is Content content) content.Chevron.Alpha = IsHovered ? 1 : 0; + if (Foreground.Children.FirstOrDefault() is Content content) + content.Hovering = IsHovered; } protected override Drawable CreateContent() => new Content(); - protected new class Content : FillFlowContainer, IHasText + protected new class Content : CompositeDrawable, IHasText { public LocalisableString Text { @@ -207,32 +224,64 @@ namespace osu.Game.Graphics.UserInterface public readonly OsuSpriteText Label; public readonly SpriteIcon Chevron; + private const float chevron_offset = -3; + public Content() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - Direction = FillDirection.Horizontal; - Children = new Drawable[] + InternalChildren = new Drawable[] { Chevron = new SpriteIcon { - AlwaysPresent = true, Icon = FontAwesome.Solid.ChevronRight, - Colour = Color4.Black, - Alpha = 0.5f, Size = new Vector2(8), + Alpha = 0, + X = chevron_offset, Margin = new MarginPadding { Left = 3, Right = 3 }, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, }, Label = new OsuSpriteText { + X = 15, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, }, }; } + + [BackgroundDependencyLoader(true)] + private void load(OverlayColourProvider? colourProvider) + { + Chevron.Colour = colourProvider?.Background5 ?? Color4.Black; + } + + private bool hovering; + + public bool Hovering + { + get => hovering; + set + { + if (value == hovering) + return; + + hovering = value; + + if (hovering) + { + Chevron.FadeIn(400, Easing.OutQuint); + Chevron.MoveToX(0, 400, Easing.OutQuint); + } + else + { + Chevron.FadeOut(200); + Chevron.MoveToX(chevron_offset, 200, Easing.In); + } + } + } } } @@ -267,7 +316,7 @@ namespace osu.Game.Graphics.UserInterface public OsuDropdownHeader() { - Foreground.Padding = new MarginPadding(4); + Foreground.Padding = new MarginPadding(10); AutoSizeAxes = Axes.None; Margin = new MarginPadding { Bottom = 4 }; @@ -303,8 +352,7 @@ namespace osu.Game.Graphics.UserInterface Icon = FontAwesome.Solid.ChevronDown, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Margin = new MarginPadding { Horizontal = 5 }, - Size = new Vector2(12), + Size = new Vector2(16), }, } } @@ -313,11 +361,11 @@ namespace osu.Game.Graphics.UserInterface AddInternal(new HoverClickSounds()); } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + [BackgroundDependencyLoader(true)] + private void load(OverlayColourProvider? colourProvider, OsuColour colours) { - BackgroundColour = Color4.Black.Opacity(0.5f); - BackgroundColourHover = colours.PinkDarker; + BackgroundColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f); + BackgroundColourHover = colourProvider?.Light4 ?? colours.PinkDarker; } } } diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index f85f9327fa..6963f7335e 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -3,11 +3,13 @@ using System; using System.Globalization; +using JetBrains.Annotations; using osuTK; using osuTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; @@ -16,6 +18,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Framework.Utils; +using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterface { @@ -52,34 +55,63 @@ namespace osu.Game.Graphics.UserInterface { accentColour = value; leftBox.Colour = value; + } + } + + private Colour4 backgroundColour; + + public Color4 BackgroundColour + { + get => backgroundColour; + set + { + backgroundColour = value; rightBox.Colour = value; } } public OsuSliderBar() { - Height = 12; - RangePadding = 20; + Height = Nub.HEIGHT; + RangePadding = Nub.EXPANDED_SIZE / 2; Children = new Drawable[] { - leftBox = new Box + new Container { - Height = 2, - EdgeSmoothness = new Vector2(0, 0.5f), - Position = new Vector2(2, 0), - RelativeSizeAxes = Axes.None, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - }, - rightBox = new Box - { - Height = 2, - EdgeSmoothness = new Vector2(0, 0.5f), - Position = new Vector2(-2, 0), - RelativeSizeAxes = Axes.None, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Alpha = 0.5f, + Padding = new MarginPadding { Horizontal = 2 }, + Child = new CircularContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Masking = true, + CornerRadius = 5f, + Children = new Drawable[] + { + leftBox = new Box + { + Height = 5, + EdgeSmoothness = new Vector2(0, 0.5f), + RelativeSizeAxes = Axes.None, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + rightBox = new Box + { + Height = 5, + EdgeSmoothness = new Vector2(0, 0.5f), + RelativeSizeAxes = Axes.None, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Alpha = 0.5f, + }, + }, + }, }, nubContainer = new Container { @@ -88,7 +120,7 @@ namespace osu.Game.Graphics.UserInterface { Origin = Anchor.TopCentre, RelativePositionAxes = Axes.X, - Expanded = true, + Current = { Value = true } }, }, new HoverClickSounds() @@ -97,11 +129,12 @@ namespace osu.Game.Graphics.UserInterface Current.DisabledChanged += disabled => { Alpha = disabled ? 0.3f : 1; }; } - [BackgroundDependencyLoader] - private void load(AudioManager audio, OsuColour colours) + [BackgroundDependencyLoader(true)] + private void load(AudioManager audio, [CanBeNull] OverlayColourProvider colourProvider, OsuColour colours) { sample = audio.Samples.Get(@"UI/notch-tick"); - AccentColour = colours.Pink; + AccentColour = colourProvider?.Highlight1 ?? colours.Pink; + BackgroundColour = colourProvider?.Background5 ?? colours.Pink.Opacity(0.5f); } protected override void Update() @@ -119,26 +152,25 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnHover(HoverEvent e) { - Nub.Glowing = true; + updateGlow(); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - Nub.Glowing = false; + updateGlow(); base.OnHoverLost(e); } - protected override bool OnMouseDown(MouseDownEvent e) + protected override void OnDragEnd(DragEndEvent e) { - Nub.Current.Value = true; - return base.OnMouseDown(e); + updateGlow(); + base.OnDragEnd(e); } - protected override void OnMouseUp(MouseUpEvent e) + private void updateGlow() { - Nub.Current.Value = false; - base.OnMouseUp(e); + Nub.Glowing = IsHovered || IsDragged; } protected override void OnUserChange(T value) diff --git a/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs b/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs index 965734792c..c01ee1a059 100644 --- a/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs +++ b/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs @@ -2,11 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; -using osuTK; -using osuTK.Graphics; namespace osu.Game.Graphics.UserInterface { @@ -15,30 +12,13 @@ namespace osu.Game.Graphics.UserInterface { protected override DropdownHeader CreateHeader() => new SlimDropdownHeader(); - protected override DropdownMenu CreateMenu() => new SlimMenu(); - private class SlimDropdownHeader : OsuDropdownHeader { public SlimDropdownHeader() { Height = 25; - Icon.Size = new Vector2(16); Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 8, Right = 4 }; } - - protected override void LoadComplete() - { - base.LoadComplete(); - BackgroundColour = Color4.Black.Opacity(0.25f); - } - } - - private class SlimMenu : OsuDropdownMenu - { - public SlimMenu() - { - BackgroundColour = Color4.Black.Opacity(0.7f); - } } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs index 5a697623c9..d5f76733cf 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs @@ -1,12 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.Containers; +using osu.Game.Overlays; using osuTK; namespace osu.Game.Graphics.UserInterfaceV2 @@ -44,6 +47,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 /// protected readonly T Component; + private readonly Box background; private readonly GridContainer grid; private readonly OsuTextFlowContainer labelText; private readonly OsuTextFlowContainer descriptionText; @@ -62,10 +66,9 @@ namespace osu.Game.Graphics.UserInterfaceV2 InternalChildren = new Drawable[] { - new Box + background = new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("1c2125"), }, new FillFlowContainer { @@ -146,9 +149,10 @@ namespace osu.Game.Graphics.UserInterfaceV2 } } - [BackgroundDependencyLoader] - private void load(OsuColour osuColour) + [BackgroundDependencyLoader(true)] + private void load(OverlayColourProvider? colourProvider, OsuColour osuColour) { + background.Colour = colourProvider?.Background4 ?? Color4Extensions.FromHex(@"1c2125"); descriptionText.Colour = osuColour.Yellow; } diff --git a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs index 27e28f1e03..23ebc6e98d 100644 --- a/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/RoundedButton.cs @@ -2,10 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; namespace osu.Game.Graphics.UserInterfaceV2 { @@ -23,10 +25,10 @@ namespace osu.Game.Graphics.UserInterfaceV2 } } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + [BackgroundDependencyLoader(true)] + private void load([CanBeNull] OverlayColourProvider overlayColourProvider, OsuColour colours) { - BackgroundColour = colours.Blue3; + BackgroundColour = overlayColourProvider?.Highlight1 ?? colours.Blue3; } protected override void LoadComplete() diff --git a/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs b/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs index a7fd25b554..deb2e6baf6 100644 --- a/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs +++ b/osu.Game/Graphics/UserInterfaceV2/SwitchButton.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -10,6 +12,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Game.Overlays; using osuTK; using osuTK.Graphics; @@ -66,11 +69,11 @@ namespace osu.Game.Graphics.UserInterfaceV2 }; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) + [BackgroundDependencyLoader(true)] + private void load(OverlayColourProvider? colourProvider, OsuColour colours) { - enabledColour = colours.BlueDark; - disabledColour = colours.Gray3; + enabledColour = colourProvider?.Highlight1 ?? colours.BlueDark; + disabledColour = colourProvider?.Background3 ?? colours.Gray3; switchContainer.Colour = enabledColour; fill.Colour = disabledColour; diff --git a/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs b/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs new file mode 100644 index 0000000000..f5709b5158 --- /dev/null +++ b/osu.Game/IO/FileAbstraction/StreamFileAbstraction.cs @@ -0,0 +1,30 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.IO; + +namespace osu.Game.IO.FileAbstraction +{ + public class StreamFileAbstraction : TagLib.File.IFileAbstraction + { + public StreamFileAbstraction(string filename, Stream fileStream) + { + ReadStream = fileStream; + Name = filename; + } + + public string Name { get; } + + public Stream ReadStream { get; } + public Stream WriteStream => ReadStream; + + public void CloseStream(Stream stream) + { + if (stream == null) + throw new ArgumentNullException(nameof(stream)); + + stream.Close(); + } + } +} diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 75bbaec0ef..28505f6b0e 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -374,7 +374,7 @@ namespace osu.Game.Online.Multiplayer UserJoined?.Invoke(user); RoomUpdated?.Invoke(); - }, false); + }); } Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user) => diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 020cdebab6..820597488b 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -554,6 +554,7 @@ namespace osu.Game { beatmap.OldValue?.CancelAsyncLoad(); beatmap.NewValue?.BeginAsyncLoad(); + Logger.Log($"Game-wide working beatmap updated to {beatmap.NewValue}"); } private void modsChanged(ValueChangedEvent> mods) @@ -642,7 +643,7 @@ namespace osu.Game SkinManager.PostNotification = n => Notifications.Post(n); BeatmapManager.PostNotification = n => Notifications.Post(n); - BeatmapManager.PresentImport = items => PresentBeatmap(items.First().Value); + BeatmapManager.PostImport = items => PresentBeatmap(items.First().Value); ScoreManager.PostNotification = n => Notifications.Post(n); ScoreManager.PostImport = items => PresentScore(items.First().Value); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 09eb482d16..f6ec22a536 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -187,8 +187,6 @@ namespace osu.Game dependencies.Cache(realmFactory = new RealmContextFactory(Storage, "client")); - AddInternal(realmFactory); - dependencies.CacheAs(Storage); var largeStore = new LargeTextureStore(Host.CreateTextureLoaderStore(new NamespacedResourceStore(Resources, @"Textures"))); @@ -529,6 +527,7 @@ namespace osu.Game LocalConfig?.Dispose(); contextFactory?.FlushConnections(); + realmFactory?.Dispose(); } } } diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 3105ecd742..f8cd31f193 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -31,10 +31,12 @@ namespace osu.Game.Overlays.Notifications set { progress = value; - Scheduler.AddOnce(() => progressBar.Progress = progress); + Scheduler.AddOnce(updateProgress, progress); } } + private void updateProgress(float progress) => progressBar.Progress = progress; + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/Overlays/Settings/SettingsDropdown.cs b/osu.Game/Overlays/Settings/SettingsDropdown.cs index 1175ddaab8..a281d03ee7 100644 --- a/osu.Game/Overlays/Settings/SettingsDropdown.cs +++ b/osu.Game/Overlays/Settings/SettingsDropdown.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; +using osuTK; namespace osu.Game.Overlays.Settings { @@ -27,6 +28,11 @@ namespace osu.Game.Overlays.Settings public override IEnumerable FilterTerms => base.FilterTerms.Concat(Control.Items.Select(i => i.ToString())); + public SettingsDropdown() + { + FlowContent.Spacing = new Vector2(0, 10); + } + protected sealed override Drawable CreateControl() => CreateDropdown(); protected virtual OsuDropdown CreateDropdown() => new DropdownControl(); @@ -35,7 +41,6 @@ namespace osu.Game.Overlays.Settings { public DropdownControl() { - Margin = new MarginPadding { Top = 5 }; RelativeSizeAxes = Axes.X; } diff --git a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs index 9987a0c607..199ba14b48 100644 --- a/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs +++ b/osu.Game/Overlays/Settings/SettingsEnumDropdown.cs @@ -16,7 +16,6 @@ namespace osu.Game.Overlays.Settings { public DropdownControl() { - Margin = new MarginPadding { Top = 5 }; RelativeSizeAxes = Axes.X; } diff --git a/osu.Game/Overlays/Settings/SettingsSlider.cs b/osu.Game/Overlays/Settings/SettingsSlider.cs index 9fc3379b94..bb9c0dd4d7 100644 --- a/osu.Game/Overlays/Settings/SettingsSlider.cs +++ b/osu.Game/Overlays/Settings/SettingsSlider.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Settings { protected override Drawable CreateControl() => new TSlider { - Margin = new MarginPadding { Top = 5, Bottom = 5 }, + Margin = new MarginPadding { Vertical = 10 }, RelativeSizeAxes = Axes.X }; diff --git a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs index a0ec8e3e0e..eec71a3623 100644 --- a/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs +++ b/osu.Game/Rulesets/Configuration/RulesetConfigManager.cs @@ -47,9 +47,34 @@ namespace osu.Game.Rulesets.Configuration } } + private readonly HashSet pendingWrites = new HashSet(); + protected override bool PerformSave() { - // do nothing, realm saves immediately + TLookup[] changed; + + lock (pendingWrites) + { + changed = pendingWrites.ToArray(); + pendingWrites.Clear(); + } + + if (realmFactory == null) + return true; + + using (var context = realmFactory.CreateContext()) + { + context.Write(realm => + { + foreach (var c in changed) + { + var setting = realm.All().First(s => s.RulesetID == rulesetId && s.Variant == variant && s.Key == c.ToString()); + + setting.Value = ConfigStore[c].ToString(); + } + }); + } + return true; } @@ -80,7 +105,8 @@ namespace osu.Game.Rulesets.Configuration bindable.ValueChanged += b => { - realmFactory?.Context.Write(() => setting.Value = b.NewValue.ToString()); + lock (pendingWrites) + pendingWrites.Add(lookup); }; } } diff --git a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs index 81f4808789..6ed91e983a 100644 --- a/osu.Game/Rulesets/Edit/BeatmapVerifier.cs +++ b/osu.Game/Rulesets/Edit/BeatmapVerifier.cs @@ -24,6 +24,11 @@ namespace osu.Game.Rulesets.Edit new CheckAudioQuality(), new CheckMutedObjects(), new CheckFewHitsounds(), + new CheckTooShortAudioFiles(), + new CheckAudioInVideo(), + + // Files + new CheckZeroByteFiles(), // Compose new CheckUnsnappedObjects(), diff --git a/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs new file mode 100644 index 0000000000..ac2542beb0 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckAudioInVideo.cs @@ -0,0 +1,112 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using osu.Game.IO.FileAbstraction; +using osu.Game.Rulesets.Edit.Checks.Components; +using osu.Game.Storyboards; +using TagLib; +using File = TagLib.File; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckAudioInVideo : ICheck + { + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Audio track in video files"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateHasAudioTrack(this), + new IssueTemplateMissingFile(this), + new IssueTemplateFileError(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet; + var videoPaths = new List(); + + foreach (var layer in context.WorkingBeatmap.Storyboard.Layers) + { + foreach (var element in layer.Elements) + { + if (!(element is StoryboardVideo video)) + continue; + + // Ensures we don't check the same video file multiple times in case of multiple elements using it. + if (!videoPaths.Contains(video.Path)) + videoPaths.Add(video.Path); + } + } + + foreach (var filename in videoPaths) + { + string storagePath = beatmapSet.GetPathForFile(filename); + + if (storagePath == null) + { + // There's an element in the storyboard that requires this resource, so it being missing is worth warning about. + yield return new IssueTemplateMissingFile(this).Create(filename); + + continue; + } + + Issue issue; + + try + { + // We use TagLib here for platform invariance; BASS cannot detect audio presence on Linux. + using (Stream data = context.WorkingBeatmap.GetStream(storagePath)) + using (File tagFile = File.Create(new StreamFileAbstraction(filename, data))) + { + if (tagFile.Properties.AudioChannels == 0) + continue; + } + + issue = new IssueTemplateHasAudioTrack(this).Create(filename); + } + catch (CorruptFileException) + { + issue = new IssueTemplateFileError(this).Create(filename, "Corrupt file"); + } + catch (UnsupportedFormatException) + { + issue = new IssueTemplateFileError(this).Create(filename, "Unsupported format"); + } + + yield return issue; + } + } + + public class IssueTemplateHasAudioTrack : IssueTemplate + { + public IssueTemplateHasAudioTrack(ICheck check) + : base(check, IssueType.Problem, "\"{0}\" has an audio track.") + { + } + + public Issue Create(string filename) => new Issue(this, filename); + } + + public class IssueTemplateFileError : IssueTemplate + { + public IssueTemplateFileError(ICheck check) + : base(check, IssueType.Error, "Could not check whether \"{0}\" has an audio track ({1}).") + { + } + + public Issue Create(string filename, string errorReason) => new Issue(this, filename, errorReason); + } + + public class IssueTemplateMissingFile : IssueTemplate + { + public IssueTemplateMissingFile(ICheck check) + : base(check, IssueType.Warning, "Could not check whether \"{0}\" has an audio track, because it is missing.") + { + } + + public Issue Create(string filename) => new Issue(this, filename); + } + } +} diff --git a/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs new file mode 100644 index 0000000000..57f7c60916 --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckTooShortAudioFiles.cs @@ -0,0 +1,85 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using ManagedBass; +using osu.Framework.Audio.Callbacks; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckTooShortAudioFiles : ICheck + { + private const int ms_threshold = 25; + private const int min_bytes_threshold = 100; + + private readonly string[] audioExtensions = { "mp3", "ogg", "wav" }; + + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Audio, "Too short audio files"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateTooShort(this), + new IssueTemplateBadFormat(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet; + + foreach (var file in beatmapSet.Files) + { + using (Stream data = context.WorkingBeatmap.GetStream(file.FileInfo.StoragePath)) + { + if (data == null) + continue; + + var fileCallbacks = new FileCallbacks(new DataStreamFileProcedures(data)); + int decodeStream = Bass.CreateStream(StreamSystem.NoBuffer, BassFlags.Decode | BassFlags.Prescan, fileCallbacks.Callbacks, fileCallbacks.Handle); + + if (decodeStream == 0) + { + // If the file is not likely to be properly parsed by Bass, we don't produce Error issues about it. + // Image files and audio files devoid of audio data both fail, for example, but neither would be issues in this check. + if (hasAudioExtension(file.Filename) && probablyHasAudioData(data)) + yield return new IssueTemplateBadFormat(this).Create(file.Filename); + + continue; + } + + long length = Bass.ChannelGetLength(decodeStream); + double ms = Bass.ChannelBytes2Seconds(decodeStream, length) * 1000; + + // Extremely short audio files do not play on some soundcards, resulting in nothing being heard in-game for some users. + if (ms > 0 && ms < ms_threshold) + yield return new IssueTemplateTooShort(this).Create(file.Filename, ms); + } + } + } + + private bool hasAudioExtension(string filename) => audioExtensions.Any(filename.ToLower().EndsWith); + private bool probablyHasAudioData(Stream data) => data.Length > min_bytes_threshold; + + public class IssueTemplateTooShort : IssueTemplate + { + public IssueTemplateTooShort(ICheck check) + : base(check, IssueType.Problem, "\"{0}\" is too short ({1:0} ms), should be at least {2:0} ms.") + { + } + + public Issue Create(string filename, double ms) => new Issue(this, filename, ms, ms_threshold); + } + + public class IssueTemplateBadFormat : IssueTemplate + { + public IssueTemplateBadFormat(ICheck check) + : base(check, IssueType.Error, "Could not check whether \"{0}\" is too short (code \"{1}\").") + { + } + + public Issue Create(string filename) => new Issue(this, filename, Bass.LastError); + } + } +} diff --git a/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs b/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs new file mode 100644 index 0000000000..3a994fabfa --- /dev/null +++ b/osu.Game/Rulesets/Edit/Checks/CheckZeroByteFiles.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using osu.Game.Rulesets.Edit.Checks.Components; + +namespace osu.Game.Rulesets.Edit.Checks +{ + public class CheckZeroByteFiles : ICheck + { + public CheckMetadata Metadata => new CheckMetadata(CheckCategory.Files, "Zero-byte files"); + + public IEnumerable PossibleTemplates => new IssueTemplate[] + { + new IssueTemplateZeroBytes(this) + }; + + public IEnumerable Run(BeatmapVerifierContext context) + { + var beatmapSet = context.Beatmap.BeatmapInfo.BeatmapSet; + + foreach (var file in beatmapSet.Files) + { + using (Stream data = context.WorkingBeatmap.GetStream(file.FileInfo.StoragePath)) + { + if (data?.Length == 0) + yield return new IssueTemplateZeroBytes(this).Create(file.Filename); + } + } + } + + public class IssueTemplateZeroBytes : IssueTemplate + { + public IssueTemplateZeroBytes(ICheck check) + : base(check, IssueType.Problem, "\"{0}\" is a 0-byte file.") + { + } + + public Issue Create(string filename) => new Issue(this, filename); + } + } +} diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index b41e0442bc..91cc80e930 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -13,7 +13,6 @@ using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; @@ -389,41 +388,42 @@ namespace osu.Game.Rulesets.Edit return new SnapResult(screenSpacePosition, targetTime, playfield); } - public override float GetBeatSnapDistanceAt(double referenceTime) + public override float GetBeatSnapDistanceAt(HitObject referenceObject) { - DifficultyControlPoint difficultyPoint = EditorBeatmap.ControlPointInfo.DifficultyPointAt(referenceTime); - return (float)(100 * EditorBeatmap.Difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / BeatSnapProvider.BeatDivisor); + return (float)(100 * EditorBeatmap.Difficulty.SliderMultiplier * referenceObject.DifficultyControlPoint.SliderVelocity / BeatSnapProvider.BeatDivisor); } - public override float DurationToDistance(double referenceTime, double duration) + public override float DurationToDistance(HitObject referenceObject, double duration) { - double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime); - return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceTime)); + double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); + return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceObject)); } - public override double DistanceToDuration(double referenceTime, float distance) + public override double DistanceToDuration(HitObject referenceObject, float distance) { - double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime); - return distance / GetBeatSnapDistanceAt(referenceTime) * beatLength; + double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime); + return distance / GetBeatSnapDistanceAt(referenceObject) * beatLength; } - public override double GetSnappedDurationFromDistance(double referenceTime, float distance) - => BeatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime) - referenceTime; + public override double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) + => BeatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime; - public override float GetSnappedDistanceFromDistance(double referenceTime, float distance) + public override float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) { - double actualDuration = referenceTime + DistanceToDuration(referenceTime, distance); + double startTime = referenceObject.StartTime; - double snappedEndTime = BeatSnapProvider.SnapTime(actualDuration, referenceTime); + double actualDuration = startTime + DistanceToDuration(referenceObject, distance); - double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime); + double snappedEndTime = BeatSnapProvider.SnapTime(actualDuration, startTime); + + double beatLength = BeatSnapProvider.GetBeatLengthAtTime(startTime); // we don't want to exceed the actual duration and snap to a point in the future. // as we are snapping to beat length via SnapTime (which will round-to-nearest), check for snapping in the forward direction and reverse it. if (snappedEndTime > actualDuration + 1) snappedEndTime -= beatLength; - return DurationToDistance(referenceTime, snappedEndTime - referenceTime); + return DurationToDistance(referenceObject, snappedEndTime - startTime); } #endregion @@ -466,15 +466,15 @@ namespace osu.Game.Rulesets.Edit public virtual SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, null); - public abstract float GetBeatSnapDistanceAt(double referenceTime); + public abstract float GetBeatSnapDistanceAt(HitObject referenceObject); - public abstract float DurationToDistance(double referenceTime, double duration); + public abstract float DurationToDistance(HitObject referenceObject, double duration); - public abstract double DistanceToDuration(double referenceTime, float distance); + public abstract double DistanceToDuration(HitObject referenceObject, float distance); - public abstract double GetSnappedDurationFromDistance(double referenceTime, float distance); + public abstract double GetSnappedDurationFromDistance(HitObject referenceObject, float distance); - public abstract float GetSnappedDistanceFromDistance(double referenceTime, float distance); + public abstract float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance); #endregion } diff --git a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs index 4664f3808c..743a2f41fc 100644 --- a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Rulesets.Edit @@ -27,41 +28,41 @@ namespace osu.Game.Rulesets.Edit /// /// Retrieves the distance between two points within a timing point that are one beat length apart. /// - /// The time of the timing point. + /// An object to be used as a reference point for this operation. /// The distance between two points residing in the timing point that are one beat length apart. - float GetBeatSnapDistanceAt(double referenceTime); + float GetBeatSnapDistanceAt(HitObject referenceObject); /// /// Converts a duration to a distance. /// - /// The time of the timing point which resides in. + /// An object to be used as a reference point for this operation. /// The duration to convert. /// A value that represents as a distance in the timing point. - float DurationToDistance(double referenceTime, double duration); + float DurationToDistance(HitObject referenceObject, double duration); /// /// Converts a distance to a duration. /// - /// The time of the timing point which resides in. + /// An object to be used as a reference point for this operation. /// The distance to convert. /// A value that represents as a duration in the timing point. - double DistanceToDuration(double referenceTime, float distance); + double DistanceToDuration(HitObject referenceObject, float distance); /// /// Converts a distance to a snapped duration. /// - /// The time of the timing point which resides in. + /// An object to be used as a reference point for this operation. /// The distance to convert. /// A value that represents as a duration snapped to the closest beat of the timing point. - double GetSnappedDurationFromDistance(double referenceTime, float distance); + double GetSnappedDurationFromDistance(HitObject referenceObject, float distance); /// /// Converts an unsnapped distance to a snapped distance. /// The returned distance will always be floored (as to never exceed the provided . /// - /// The time of the timing point which resides in. + /// An object to be used as a reference point for this operation. /// The distance to convert. /// A value that represents snapped to the closest beat of the timing point. - float GetSnappedDistanceFromDistance(double referenceTime, float distance); + float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance); } } diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 0b159819d4..035ebe10cb 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -67,7 +67,8 @@ namespace osu.Game.Rulesets.Objects } } - public SampleControlPoint SampleControlPoint; + public SampleControlPoint SampleControlPoint = SampleControlPoint.DEFAULT; + public DifficultyControlPoint DifficultyControlPoint = DifficultyControlPoint.DEFAULT; /// /// Whether this is in Kiai time. @@ -94,6 +95,12 @@ namespace osu.Game.Rulesets.Objects foreach (var nested in nestedHitObjects) nested.StartTime += offset; + + if (DifficultyControlPoint != DifficultyControlPoint.DEFAULT) + DifficultyControlPoint.Time = time.NewValue; + + if (SampleControlPoint != SampleControlPoint.DEFAULT) + SampleControlPoint.Time = this.GetEndTime() + control_point_leniency; }; } @@ -105,16 +112,21 @@ namespace osu.Game.Rulesets.Objects /// The cancellation token. public void ApplyDefaults(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty, CancellationToken cancellationToken = default) { + var legacyInfo = controlPointInfo as LegacyControlPointInfo; + + if (legacyInfo != null) + { + DifficultyControlPoint = (DifficultyControlPoint)legacyInfo.DifficultyPointAt(StartTime).DeepClone(); + DifficultyControlPoint.Time = StartTime; + } + ApplyDefaultsToSelf(controlPointInfo, difficulty); - if (controlPointInfo is LegacyControlPointInfo legacyInfo) + // This is done here after ApplyDefaultsToSelf as we may require custom defaults to be applied to have an accurate end time. + if (legacyInfo != null) { - // This is done here since ApplyDefaultsToSelf may be used to determine the end time - SampleControlPoint = legacyInfo.SamplePointAt(this.GetEndTime() + control_point_leniency); - } - else - { - SampleControlPoint ??= SampleControlPoint.DEFAULT; + SampleControlPoint = (SampleControlPoint)legacyInfo.SamplePointAt(this.GetEndTime() + control_point_leniency).DeepClone(); + SampleControlPoint.Time = this.GetEndTime() + control_point_leniency; } nestedHitObjects.Clear(); diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index e1de82ade7..ad191f7ff5 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -43,9 +43,8 @@ namespace osu.Game.Rulesets.Objects.Legacy base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); - DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime); - double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier; + double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * DifficultyControlPoint.SliderVelocity; Velocity = scoringDistance / timingPoint.BeatLength; } diff --git a/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs b/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs index dcd2cc8b55..23325bcd13 100644 --- a/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs +++ b/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs @@ -7,7 +7,7 @@ using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Rulesets.Timing { /// - /// A control point which adds an aggregated multiplier based on the provided 's BeatLength and 's SpeedMultiplier. + /// A control point which adds an aggregated multiplier based on the provided 's BeatLength and 's SpeedMultiplier. /// public class MultiplierControlPoint : IComparable { @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Timing /// /// The aggregate multiplier which this provides. /// - public double Multiplier => Velocity * DifficultyPoint.SpeedMultiplier * BaseBeatLength / TimingPoint.BeatLength; + public double Multiplier => Velocity * EffectPoint.ScrollSpeed * BaseBeatLength / TimingPoint.BeatLength; /// /// The base beat length to scale the provided multiplier relative to. @@ -38,9 +38,9 @@ namespace osu.Game.Rulesets.Timing public TimingControlPoint TimingPoint = new TimingControlPoint(); /// - /// The that provides additional difficulty information for this . + /// The that provides additional difficulty information for this . /// - public DifficultyControlPoint DifficultyPoint = new DifficultyControlPoint(); + public EffectControlPoint EffectPoint = new EffectControlPoint(); /// /// Creates a . This is required for JSON serialization diff --git a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs index 041c5ebef5..2a9d3d1cf0 100644 --- a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs +++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs @@ -140,25 +140,32 @@ namespace osu.Game.Rulesets.UI.Scrolling // Merge sequences of timing and difficulty control points to create the aggregate "multiplier" control point var lastTimingPoint = new TimingControlPoint(); - var lastDifficultyPoint = new DifficultyControlPoint(); + var lastEffectPoint = new EffectControlPoint(); var allPoints = new SortedList(Comparer.Default); + allPoints.AddRange(Beatmap.ControlPointInfo.TimingPoints); - allPoints.AddRange(Beatmap.ControlPointInfo.DifficultyPoints); + allPoints.AddRange(Beatmap.ControlPointInfo.EffectPoints); // Generate the timing points, making non-timing changes use the previous timing change and vice-versa var timingChanges = allPoints.Select(c => { - if (c is TimingControlPoint timingPoint) - lastTimingPoint = timingPoint; - else if (c is DifficultyControlPoint difficultyPoint) - lastDifficultyPoint = difficultyPoint; + switch (c) + { + case TimingControlPoint timingPoint: + lastTimingPoint = timingPoint; + break; + + case EffectControlPoint difficultyPoint: + lastEffectPoint = difficultyPoint; + break; + } return new MultiplierControlPoint(c.Time) { Velocity = Beatmap.Difficulty.SliderMultiplier, BaseBeatLength = baseBeatLength, TimingPoint = lastTimingPoint, - DifficultyPoint = lastDifficultyPoint + EffectPoint = lastEffectPoint }; }); diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index cf22a8fda4..8494cdcd22 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -25,7 +25,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Scoring { - public class ScoreManager : IModelManager, IModelFileManager, IModelDownloader, ICanAcceptFiles, IPostImports + public class ScoreManager : IModelManager, IModelFileManager, IModelDownloader, ICanAcceptFiles { private readonly Scheduler scheduler; private readonly Func difficulties; diff --git a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs index 730f482f83..6b32ff96c4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/CircularDistanceSnapGrid.cs @@ -5,14 +5,15 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; +using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components { public abstract class CircularDistanceSnapGrid : DistanceSnapGrid { - protected CircularDistanceSnapGrid(Vector2 startPosition, double startTime, double? endTime = null) - : base(startPosition, startTime, endTime) + protected CircularDistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null) + : base(referenceObject, startPosition, startTime, endTime) { } @@ -79,7 +80,7 @@ namespace osu.Game.Screens.Edit.Compose.Components Vector2 normalisedDirection = direction * new Vector2(1f / distance); Vector2 snappedPosition = StartPosition + normalisedDirection * radialCount * radius; - return (snappedPosition, StartTime + SnapProvider.GetSnappedDurationFromDistance(StartTime, (snappedPosition - StartPosition).Length)); + return (snappedPosition, StartTime + SnapProvider.GetSnappedDurationFromDistance(ReferenceObject, (snappedPosition - StartPosition).Length)); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs index 59f88ac641..9d43e3258a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Layout; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components @@ -54,15 +55,20 @@ namespace osu.Game.Screens.Edit.Compose.Components private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); private readonly double? endTime; + protected readonly HitObject ReferenceObject; + /// /// Creates a new . /// + /// A reference object to gather relevant difficulty values from. /// The position at which the grid should start. The first tick is located one distance spacing length away from this point. /// The snapping time at . /// The time at which the snapping grid should end. If null, the grid will continue until the bounds of the screen are exceeded. - protected DistanceSnapGrid(Vector2 startPosition, double startTime, double? endTime = null) + protected DistanceSnapGrid(HitObject referenceObject, Vector2 startPosition, double startTime, double? endTime = null) { + ReferenceObject = referenceObject; this.endTime = endTime; + StartPosition = startPosition; StartTime = startTime; @@ -80,7 +86,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updateSpacing() { - DistanceSpacing = SnapProvider.GetBeatSnapDistanceAt(StartTime); + DistanceSpacing = SnapProvider.GetBeatSnapDistanceAt(ReferenceObject); if (endTime == null) MaxIntervals = int.MaxValue; @@ -88,7 +94,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { // +1 is added since a snapped hitobject may have its start time slightly less than the snapped time due to floating point errors double maxDuration = endTime.Value - StartTime + 1; - MaxIntervals = (int)(maxDuration / SnapProvider.DistanceToDuration(StartTime, DistanceSpacing)); + MaxIntervals = (int)(maxDuration / SnapProvider.DistanceToDuration(ReferenceObject, DistanceSpacing)); } gridCache.Invalidate(); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs index 3248936765..21457ea273 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs @@ -1,27 +1,106 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Rulesets.Objects; +using osu.Game.Screens.Edit.Timing; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { - public class DifficultyPointPiece : TopPointPiece + public class DifficultyPointPiece : HitObjectPointPiece, IHasPopover { + private readonly HitObject hitObject; + private readonly BindableNumber speedMultiplier; - public DifficultyPointPiece(DifficultyControlPoint point) - : base(point) + public DifficultyPointPiece(HitObject hitObject) + : base(hitObject.DifficultyControlPoint) { - speedMultiplier = point.SpeedMultiplierBindable.GetBoundCopy(); + this.hitObject = hitObject; - Y = Height; + speedMultiplier = hitObject.DifficultyControlPoint.SliderVelocityBindable.GetBoundCopy(); } protected override void LoadComplete() { base.LoadComplete(); + speedMultiplier.BindValueChanged(multiplier => Label.Text = $"{multiplier.NewValue:n2}x", true); } + + protected override bool OnClick(ClickEvent e) + { + this.ShowPopover(); + return true; + } + + public Popover GetPopover() => new DifficultyEditPopover(hitObject); + + public class DifficultyEditPopover : OsuPopover + { + private readonly HitObject hitObject; + private readonly DifficultyControlPoint point; + + private SliderWithTextBoxInput sliderVelocitySlider; + + [Resolved(canBeNull: true)] + private EditorBeatmap beatmap { get; set; } + + public DifficultyEditPopover(HitObject hitObject) + { + this.hitObject = hitObject; + point = hitObject.DifficultyControlPoint; + } + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + new FillFlowContainer + { + Width = 200, + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + sliderVelocitySlider = new SliderWithTextBoxInput("Velocity") + { + Current = new DifficultyControlPoint().SliderVelocityBindable, + KeyboardStep = 0.1f + }, + new OsuTextFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Text = "Hold shift while dragging the end of an object to adjust velocity while snapping." + } + } + } + }; + + var selectedPointBindable = point.SliderVelocityBindable; + + // there may be legacy control points, which contain infinite precision for compatibility reasons (see LegacyDifficultyControlPoint). + // generally that level of precision could only be set by externally editing the .osu file, so at the point + // a user is looking to update this within the editor it should be safe to obliterate this additional precision. + double expectedPrecision = new DifficultyControlPoint().SliderVelocityBindable.Precision; + if (selectedPointBindable.Precision < expectedPrecision) + selectedPointBindable.Precision = expectedPrecision; + + sliderVelocitySlider.Current = selectedPointBindable; + sliderVelocitySlider.Current.BindValueChanged(_ => beatmap?.Update(hitObject)); + } + } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs new file mode 100644 index 0000000000..6b62459c97 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/HitObjectPointPiece.cs @@ -0,0 +1,63 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit.Compose.Components.Timeline +{ + public class HitObjectPointPiece : CircularContainer + { + private readonly ControlPoint point; + + protected OsuSpriteText Label { get; private set; } + + protected HitObjectPointPiece(ControlPoint point) + { + this.point = point; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + AutoSizeAxes = Axes.Both; + + Color4 colour = point.GetRepresentingColour(colours); + + InternalChildren = new Drawable[] + { + new Container + { + AutoSizeAxes = Axes.X, + Height = 16, + Masking = true, + CornerRadius = 8, + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Children = new Drawable[] + { + new Box + { + Colour = colour, + RelativeSizeAxes = Axes.Both, + }, + Label = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding(5), + Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold), + Colour = colours.B5, + } + } + }, + }; + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 9461f5e885..6a26f69e41 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -3,88 +3,102 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osuTK.Graphics; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Rulesets.Objects; +using osu.Game.Screens.Edit.Timing; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { - public class SamplePointPiece : CompositeDrawable + public class SamplePointPiece : HitObjectPointPiece, IHasPopover { - private readonly SampleControlPoint samplePoint; + private readonly HitObject hitObject; private readonly Bindable bank; private readonly BindableNumber volume; - private OsuSpriteText text; - private Container volumeBox; - - private const int max_volume_height = 22; - - public SamplePointPiece(SampleControlPoint samplePoint) + public SamplePointPiece(HitObject hitObject) + : base(hitObject.SampleControlPoint) { - this.samplePoint = samplePoint; - volume = samplePoint.SampleVolumeBindable.GetBoundCopy(); - bank = samplePoint.SampleBankBindable.GetBoundCopy(); + this.hitObject = hitObject; + volume = hitObject.SampleControlPoint.SampleVolumeBindable.GetBoundCopy(); + bank = hitObject.SampleControlPoint.SampleBankBindable.GetBoundCopy(); } [BackgroundDependencyLoader] private void load(OsuColour colours) { - Margin = new MarginPadding { Vertical = 5 }; + volume.BindValueChanged(volume => updateText()); + bank.BindValueChanged(bank => updateText(), true); + } - Origin = Anchor.BottomCentre; - Anchor = Anchor.BottomCentre; + protected override bool OnClick(ClickEvent e) + { + this.ShowPopover(); + return true; + } - AutoSizeAxes = Axes.X; - RelativeSizeAxes = Axes.Y; + private void updateText() + { + Label.Text = $"{bank.Value} {volume.Value}"; + } - Color4 colour = samplePoint.GetRepresentingColour(colours); + public Popover GetPopover() => new SampleEditPopover(hitObject); - InternalChildren = new Drawable[] + public class SampleEditPopover : OsuPopover + { + private readonly HitObject hitObject; + private readonly SampleControlPoint point; + + private LabelledTextBox bank; + private SliderWithTextBoxInput volume; + + [Resolved(canBeNull: true)] + private EditorBeatmap beatmap { get; set; } + + public SampleEditPopover(HitObject hitObject) { - volumeBox = new Circle + this.hitObject = hitObject; + point = hitObject.SampleControlPoint; + } + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] { - CornerRadius = 5, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Y = -20, - Width = 10, - Colour = colour, - }, - new Container - { - AutoSizeAxes = Axes.X, - Height = 16, - Masking = true, - CornerRadius = 8, - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Children = new Drawable[] + new FillFlowContainer { - new Box + Width = 200, + Direction = FillDirection.Vertical, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] { - Colour = colour, - RelativeSizeAxes = Axes.Both, - }, - text = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Padding = new MarginPadding(5), - Font = OsuFont.Default.With(size: 12, weight: FontWeight.SemiBold), - Colour = colours.B5, + bank = new LabelledTextBox + { + Label = "Bank Name", + }, + volume = new SliderWithTextBoxInput("Volume") + { + Current = new SampleControlPoint().SampleVolumeBindable, + } } } - }, - }; + }; - volume.BindValueChanged(volume => volumeBox.Height = max_volume_height * volume.NewValue / 100f, true); - bank.BindValueChanged(bank => text.Text = bank.NewValue, true); + bank.Current = point.SampleBankBindable; + bank.Current.BindValueChanged(_ => beatmap.Update(hitObject)); + + volume.Current = point.SampleVolumeBindable; + volume.Current.BindValueChanged(_ => beatmap.Update(hitObject)); + } } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 621a24c67d..b8fa05e7eb 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -15,6 +15,7 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components.Timeline @@ -58,7 +59,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private Track track; private const float timeline_height = 72; - private const float timeline_expanded_height = 156; + private const float timeline_expanded_height = 94; public Timeline(Drawable userContent) { @@ -158,7 +159,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (visible.NewValue) { this.ResizeHeightTo(timeline_expanded_height, 200, Easing.OutQuint); - mainContent.MoveToY(36, 200, Easing.OutQuint); + mainContent.MoveToY(20, 200, Easing.OutQuint); // delay the fade in else masking looks weird. controlPoints.Delay(180).FadeIn(400, Easing.OutQuint); @@ -298,14 +299,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private double getTimeFromPosition(Vector2 localPosition) => (localPosition.X / Content.DrawWidth) * track.Length; - public float GetBeatSnapDistanceAt(double referenceTime) => throw new NotImplementedException(); + public float GetBeatSnapDistanceAt(HitObject referenceObject) => throw new NotImplementedException(); - public float DurationToDistance(double referenceTime, double duration) => throw new NotImplementedException(); + public float DurationToDistance(HitObject referenceObject, double duration) => throw new NotImplementedException(); - public double DistanceToDuration(double referenceTime, float distance) => throw new NotImplementedException(); + public double DistanceToDuration(HitObject referenceObject, float distance) => throw new NotImplementedException(); - public double GetSnappedDurationFromDistance(double referenceTime, float distance) => throw new NotImplementedException(); + public double GetSnappedDurationFromDistance(HitObject referenceObject, float distance) => throw new NotImplementedException(); - public float GetSnappedDistanceFromDistance(double referenceTime, float distance) => throw new NotImplementedException(); + public float GetSnappedDistanceFromDistance(HitObject referenceObject, float distance) => throw new NotImplementedException(); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs index c4beb40f92..2b2e66fb18 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs @@ -45,17 +45,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { switch (point) { - case DifficultyControlPoint difficultyPoint: - AddInternal(new DifficultyPointPiece(difficultyPoint) { Depth = -2 }); - break; - case TimingControlPoint timingPoint: AddInternal(new TimingPointPiece(timingPoint)); break; - - case SampleControlPoint samplePoint: - AddInternal(new SamplePointPiece(samplePoint) { Depth = -1 }); - break; } } }, true); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 911c9fea51..e2458d45c9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -13,7 +13,9 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Framework.Threading; using osu.Framework.Utils; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Edit; @@ -179,6 +181,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline colouredComponents.Colour = OsuColour.ForegroundTextColourFor(col); } + private SamplePointPiece sampleOverrideDisplay; + private DifficultyPointPiece difficultyOverrideDisplay; + + [Resolved] + private EditorBeatmap beatmap { get; set; } + + private DifficultyControlPoint difficultyControlPoint; + private SampleControlPoint sampleControlPoint; + protected override void Update() { base.Update(); @@ -194,6 +205,36 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (Item is IHasRepeats repeats) updateRepeats(repeats); } + + if (difficultyControlPoint != Item.DifficultyControlPoint) + { + difficultyControlPoint = Item.DifficultyControlPoint; + difficultyOverrideDisplay?.Expire(); + + if (Item.DifficultyControlPoint != null && Item is IHasDistance) + { + AddInternal(difficultyOverrideDisplay = new DifficultyPointPiece(Item) + { + Anchor = Anchor.TopLeft, + Origin = Anchor.BottomCentre + }); + } + } + + if (sampleControlPoint != Item.SampleControlPoint) + { + sampleControlPoint = Item.SampleControlPoint; + sampleOverrideDisplay?.Expire(); + + if (Item.SampleControlPoint != null) + { + AddInternal(sampleOverrideDisplay = new SamplePointPiece(Item) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.TopCentre + }); + } + } } private void updateRepeats(IHasRepeats repeats) @@ -331,39 +372,66 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return true; } + private ScheduledDelegate dragOperation; + protected override void OnDrag(DragEvent e) { base.OnDrag(e); - OnDragHandled?.Invoke(e); - - if (timeline.SnapScreenSpacePositionToValidTime(e.ScreenSpaceMousePosition).Time is double time) + // schedule is temporary to ensure we don't process multiple times on a single update frame. we need to find a better method of doing this. + // without it, a hitobject's endtime may not always be in a valid state (ie. sliders, which needs to recompute their path). + dragOperation?.Cancel(); + dragOperation = Scheduler.Add(() => { - switch (hitObject) + OnDragHandled?.Invoke(e); + + if (timeline.SnapScreenSpacePositionToValidTime(e.ScreenSpaceMousePosition).Time is double time) { - case IHasRepeats repeatHitObject: - // find the number of repeats which can fit in the requested time. - var lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1); - var proposedCount = Math.Max(0, (int)Math.Round((time - hitObject.StartTime) / lengthOfOneRepeat) - 1); + switch (hitObject) + { + case IHasRepeats repeatHitObject: + double proposedDuration = time - hitObject.StartTime; - if (proposedCount == repeatHitObject.RepeatCount) - return; + if (e.CurrentState.Keyboard.ShiftPressed) + { + if (hitObject.DifficultyControlPoint == DifficultyControlPoint.DEFAULT) + hitObject.DifficultyControlPoint = new DifficultyControlPoint(); - repeatHitObject.RepeatCount = proposedCount; - beatmap.Update(hitObject); - break; + var newVelocity = hitObject.DifficultyControlPoint.SliderVelocity * (repeatHitObject.Duration / proposedDuration); - case IHasDuration endTimeHitObject: - var snappedTime = Math.Max(hitObject.StartTime, beatSnapProvider.SnapTime(time)); + if (Precision.AlmostEquals(newVelocity, hitObject.DifficultyControlPoint.SliderVelocity)) + return; - if (endTimeHitObject.EndTime == snappedTime || Precision.AlmostEquals(snappedTime, hitObject.StartTime, beatmap.GetBeatLengthAtTime(snappedTime))) - return; + hitObject.DifficultyControlPoint.SliderVelocity = newVelocity; + beatmap.Update(hitObject); + } + else + { + // find the number of repeats which can fit in the requested time. + var lengthOfOneRepeat = repeatHitObject.Duration / (repeatHitObject.RepeatCount + 1); + var proposedCount = Math.Max(0, (int)Math.Round(proposedDuration / lengthOfOneRepeat) - 1); - endTimeHitObject.Duration = snappedTime - hitObject.StartTime; - beatmap.Update(hitObject); - break; + if (proposedCount == repeatHitObject.RepeatCount) + return; + + repeatHitObject.RepeatCount = proposedCount; + beatmap.Update(hitObject); + } + + break; + + case IHasDuration endTimeHitObject: + var snappedTime = Math.Max(hitObject.StartTime, beatSnapProvider.SnapTime(time)); + + if (endTimeHitObject.EndTime == snappedTime || Precision.AlmostEquals(snappedTime, hitObject.StartTime, beatmap.GetBeatLengthAtTime(snappedTime))) + return; + + endTimeHitObject.Duration = snappedTime - hitObject.StartTime; + beatmap.Update(hitObject); + break; + } } - } + }); } protected override void OnDragEnd(DragEndEvent e) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 1170658abb..512226413b 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -162,7 +162,7 @@ namespace osu.Game.Screens.Edit // todo: remove caching of this and consume via editorBeatmap? dependencies.Cache(beatDivisor); - AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, loadableBeatmap.GetSkin())); + AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, loadableBeatmap.GetSkin(), loadableBeatmap.BeatmapInfo)); dependencies.CacheAs(editorBeatmap); changeHandler = new EditorChangeHandler(editorBeatmap); dependencies.CacheAs(changeHandler); @@ -333,10 +333,10 @@ namespace osu.Game.Screens.Edit isNewBeatmap = false; // apply any set-level metadata changes. - beatmapManager.Update(playableBeatmap.BeatmapInfo.BeatmapSet); + beatmapManager.Update(editorBeatmap.BeatmapInfo.BeatmapSet); // save the loaded beatmap's data stream. - beatmapManager.Save(playableBeatmap.BeatmapInfo, editorBeatmap, editorBeatmap.BeatmapSkin); + beatmapManager.Save(editorBeatmap.BeatmapInfo, editorBeatmap.PlayableBeatmap, editorBeatmap.BeatmapSkin); updateLastSavedHash(); } @@ -523,7 +523,10 @@ namespace osu.Game.Screens.Edit var refetchedBeatmap = beatmapManager.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo); if (!(refetchedBeatmap is DummyWorkingBeatmap)) + { + Logger.Log("Editor providing re-fetched beatmap post edit session"); Beatmap.Value = refetchedBeatmap; + } return base.OnExiting(next); } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 64eb6225fa..2e84ef437a 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -10,6 +10,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -44,6 +45,7 @@ namespace osu.Game.Screens.Edit /// public readonly Bindable PlacementObject = new Bindable(); + private readonly BeatmapInfo beatmapInfo; public readonly IBeatmap PlayableBeatmap; /// @@ -66,9 +68,37 @@ namespace osu.Game.Screens.Edit private readonly Dictionary> startTimeBindables = new Dictionary>(); - public EditorBeatmap(IBeatmap playableBeatmap, ISkin beatmapSkin = null) + public EditorBeatmap(IBeatmap playableBeatmap, ISkin beatmapSkin = null, BeatmapInfo beatmapInfo = null) { PlayableBeatmap = playableBeatmap; + + // ensure we are not working with legacy control points. + // if we leave the legacy points around they will be applied over any local changes on + // ApplyDefaults calls. this should eventually be removed once the default logic is moved to the decoder/converter. + if (PlayableBeatmap.ControlPointInfo is LegacyControlPointInfo) + { + var newControlPoints = new ControlPointInfo(); + + foreach (var controlPoint in PlayableBeatmap.ControlPointInfo.AllControlPoints) + { + switch (controlPoint) + { + case DifficultyControlPoint _: + case SampleControlPoint _: + // skip legacy types. + continue; + + default: + newControlPoints.Add(controlPoint.Time, controlPoint); + break; + } + } + + playableBeatmap.ControlPointInfo = newControlPoints; + } + + this.beatmapInfo = beatmapInfo ?? playableBeatmap.BeatmapInfo; + if (beatmapSkin is Skin skin) BeatmapSkin = new EditorBeatmapSkin(skin); @@ -80,11 +110,11 @@ namespace osu.Game.Screens.Edit public BeatmapInfo BeatmapInfo { - get => PlayableBeatmap.BeatmapInfo; - set => PlayableBeatmap.BeatmapInfo = value; + get => beatmapInfo; + set => throw new InvalidOperationException(); } - public BeatmapMetadata Metadata => PlayableBeatmap.Metadata; + public BeatmapMetadata Metadata => beatmapInfo.Metadata; public BeatmapDifficulty Difficulty { diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index ba83261731..86e5729196 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -25,7 +25,9 @@ namespace osu.Game.Screens.Edit public double TrackLength => track.Value?.Length ?? 60000; - public ControlPointInfo ControlPointInfo; + public ControlPointInfo ControlPointInfo => Beatmap.ControlPointInfo; + + public IBeatmap Beatmap { get; set; } private readonly BindableBeatDivisor beatDivisor; @@ -42,25 +44,15 @@ namespace osu.Game.Screens.Edit /// public bool IsSeeking { get; private set; } - public EditorClock(IBeatmap beatmap, BindableBeatDivisor beatDivisor) - : this(beatmap.ControlPointInfo, beatDivisor) + public EditorClock(IBeatmap beatmap = null, BindableBeatDivisor beatDivisor = null) { - } + Beatmap = beatmap ?? new Beatmap(); - public EditorClock(ControlPointInfo controlPointInfo, BindableBeatDivisor beatDivisor) - { - this.beatDivisor = beatDivisor; - - ControlPointInfo = controlPointInfo; + this.beatDivisor = beatDivisor ?? new BindableBeatDivisor(); underlyingClock = new DecoupleableInterpolatingFramedClock(); } - public EditorClock() - : this(new ControlPointInfo(), new BindableBeatDivisor()) - { - } - /// /// Seek to the closest snappable beat from a time. /// diff --git a/osu.Game/Screens/Edit/EditorRoundedScreen.cs b/osu.Game/Screens/Edit/EditorRoundedScreen.cs index b271a145f5..508663224d 100644 --- a/osu.Game/Screens/Edit/EditorRoundedScreen.cs +++ b/osu.Game/Screens/Edit/EditorRoundedScreen.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Edit { new Box { - Colour = ColourProvider.Dark4, + Colour = ColourProvider.Background3, RelativeSizeAxes = Axes.Both, }, roundedContent = new Container diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 9e93b0b038..5bb40c09a5 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -10,7 +10,7 @@ using osu.Game.Graphics.UserInterfaceV2; namespace osu.Game.Screens.Edit.Setup { - internal class MetadataSection : SetupSection + public class MetadataSection : SetupSection { protected LabelledTextBox ArtistTextBox; protected LabelledTextBox RomanisedArtistTextBox; diff --git a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs index 48639789af..938c7f9cf0 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs @@ -12,8 +12,6 @@ namespace osu.Game.Screens.Edit.Timing { new GroupSection(), new TimingSection(), - new DifficultySection(), - new SampleSection(), new EffectSection(), }; } diff --git a/osu.Game/Screens/Edit/Timing/DifficultySection.cs b/osu.Game/Screens/Edit/Timing/DifficultySection.cs deleted file mode 100644 index 97d110c502..0000000000 --- a/osu.Game/Screens/Edit/Timing/DifficultySection.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Game.Beatmaps.ControlPoints; - -namespace osu.Game.Screens.Edit.Timing -{ - internal class DifficultySection : Section - { - private SliderWithTextBoxInput multiplierSlider; - - [BackgroundDependencyLoader] - private void load() - { - Flow.AddRange(new[] - { - multiplierSlider = new SliderWithTextBoxInput("Speed Multiplier") - { - Current = new DifficultyControlPoint().SpeedMultiplierBindable, - KeyboardStep = 0.1f - } - }); - } - - protected override void OnControlPointChanged(ValueChangedEvent point) - { - if (point.NewValue != null) - { - var selectedPointBindable = point.NewValue.SpeedMultiplierBindable; - - // there may be legacy control points, which contain infinite precision for compatibility reasons (see LegacyDifficultyControlPoint). - // generally that level of precision could only be set by externally editing the .osu file, so at the point - // a user is looking to update this within the editor it should be safe to obliterate this additional precision. - double expectedPrecision = new DifficultyControlPoint().SpeedMultiplierBindable.Precision; - if (selectedPointBindable.Precision < expectedPrecision) - selectedPointBindable.Precision = expectedPrecision; - - multiplierSlider.Current = selectedPointBindable; - multiplierSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); - } - } - - protected override DifficultyControlPoint CreatePoint() - { - var reference = Beatmap.ControlPointInfo.DifficultyPointAt(SelectedGroup.Value.Time); - - return new DifficultyControlPoint - { - SpeedMultiplier = reference.SpeedMultiplier, - }; - } - } -} diff --git a/osu.Game/Screens/Edit/Timing/EffectSection.cs b/osu.Game/Screens/Edit/Timing/EffectSection.cs index 6d23b52c05..c8944d0357 100644 --- a/osu.Game/Screens/Edit/Timing/EffectSection.cs +++ b/osu.Game/Screens/Edit/Timing/EffectSection.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.UserInterfaceV2; @@ -13,13 +14,20 @@ namespace osu.Game.Screens.Edit.Timing private LabelledSwitchButton kiai; private LabelledSwitchButton omitBarLine; + private SliderWithTextBoxInput scrollSpeedSlider; + [BackgroundDependencyLoader] private void load() { - Flow.AddRange(new[] + Flow.AddRange(new Drawable[] { kiai = new LabelledSwitchButton { Label = "Kiai Time" }, omitBarLine = new LabelledSwitchButton { Label = "Skip Bar Line" }, + scrollSpeedSlider = new SliderWithTextBoxInput("Scroll Speed") + { + Current = new EffectControlPoint().ScrollSpeedBindable, + KeyboardStep = 0.1f + } }); } @@ -32,6 +40,9 @@ namespace osu.Game.Screens.Edit.Timing omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable; omitBarLine.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); + + scrollSpeedSlider.Current = point.NewValue.ScrollSpeedBindable; + scrollSpeedSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); } } @@ -42,7 +53,8 @@ namespace osu.Game.Screens.Edit.Timing return new EffectControlPoint { KiaiMode = reference.KiaiMode, - OmitFirstBarLine = reference.OmitFirstBarLine + OmitFirstBarLine = reference.OmitFirstBarLine, + ScrollSpeed = reference.ScrollSpeed, }; } } diff --git a/osu.Game/Screens/Edit/Timing/RowAttributes/DifficultyRowAttribute.cs b/osu.Game/Screens/Edit/Timing/RowAttributes/DifficultyRowAttribute.cs index 7b553ac7ad..a8de476d67 100644 --- a/osu.Game/Screens/Edit/Timing/RowAttributes/DifficultyRowAttribute.cs +++ b/osu.Game/Screens/Edit/Timing/RowAttributes/DifficultyRowAttribute.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes public DifficultyRowAttribute(DifficultyControlPoint difficulty) : base(difficulty, "difficulty") { - speedMultiplier = difficulty.SpeedMultiplierBindable.GetBoundCopy(); + speedMultiplier = difficulty.SliderVelocityBindable.GetBoundCopy(); } [BackgroundDependencyLoader] @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes }, text = new AttributeText(Point) { - Width = 40, + Width = 45, }, }); diff --git a/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs b/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs index 812407d6da..1b33fd62aa 100644 --- a/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs +++ b/osu.Game/Screens/Edit/Timing/RowAttributes/EffectRowAttribute.cs @@ -12,14 +12,18 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes { private readonly Bindable kiaiMode; private readonly Bindable omitBarLine; + private readonly BindableNumber scrollSpeed; + private AttributeText kiaiModeBubble; private AttributeText omitBarLineBubble; + private AttributeText text; public EffectRowAttribute(EffectControlPoint effect) : base(effect, "effect") { kiaiMode = effect.KiaiModeBindable.GetBoundCopy(); omitBarLine = effect.OmitFirstBarLineBindable.GetBoundCopy(); + scrollSpeed = effect.ScrollSpeedBindable.GetBoundCopy(); } [BackgroundDependencyLoader] @@ -27,12 +31,20 @@ namespace osu.Game.Screens.Edit.Timing.RowAttributes { Content.AddRange(new Drawable[] { + new AttributeProgressBar(Point) + { + Current = scrollSpeed, + }, + text = new AttributeText(Point) { Width = 45 }, kiaiModeBubble = new AttributeText(Point) { Text = "kiai" }, omitBarLineBubble = new AttributeText(Point) { Text = "no barline" }, }); kiaiMode.BindValueChanged(enabled => kiaiModeBubble.FadeTo(enabled.NewValue ? 1 : 0), true); omitBarLine.BindValueChanged(enabled => omitBarLineBubble.FadeTo(enabled.NewValue ? 1 : 0), true); + scrollSpeed.BindValueChanged(_ => updateText(), true); } + + private void updateText() => text.Text = $"{scrollSpeed.Value:n2}x"; } } diff --git a/osu.Game/Screens/Edit/Timing/SampleSection.cs b/osu.Game/Screens/Edit/Timing/SampleSection.cs deleted file mode 100644 index 52709a2bbe..0000000000 --- a/osu.Game/Screens/Edit/Timing/SampleSection.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Graphics.UserInterfaceV2; - -namespace osu.Game.Screens.Edit.Timing -{ - internal class SampleSection : Section - { - private LabelledTextBox bank; - private SliderWithTextBoxInput volume; - - [BackgroundDependencyLoader] - private void load() - { - Flow.AddRange(new Drawable[] - { - bank = new LabelledTextBox - { - Label = "Bank Name", - }, - volume = new SliderWithTextBoxInput("Volume") - { - Current = new SampleControlPoint().SampleVolumeBindable, - } - }); - } - - protected override void OnControlPointChanged(ValueChangedEvent point) - { - if (point.NewValue != null) - { - bank.Current = point.NewValue.SampleBankBindable; - bank.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); - - volume.Current = point.NewValue.SampleVolumeBindable; - volume.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); - } - } - - protected override SampleControlPoint CreatePoint() => new SampleControlPoint(); // TODO: remove - } -} diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index 97377278a6..abda9e897b 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -150,6 +150,11 @@ namespace osu.Game.Screens.OnlinePlay.Components notifyRoomsUpdated(); } - private void notifyRoomsUpdated() => Scheduler.AddOnce(() => RoomsUpdated?.Invoke()); + private void notifyRoomsUpdated() + { + Scheduler.AddOnce(invokeRoomsUpdated); + + void invokeRoomsUpdated() => RoomsUpdated?.Invoke(); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 80a5daa7c8..0edf5dde6d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -7,7 +7,6 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -99,14 +98,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OverlayColourProvider colourProvider, OsuColour colours) { InternalChildren = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"28242d"), + Colour = colourProvider.Background4 }, new GridContainer { @@ -249,7 +248,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f), + Colour = colourProvider.Background5 }, new FillFlowContainer { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index 6c3dfe7382..cf1066df10 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -79,11 +79,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void load() { isConnected.BindTo(client.IsConnected); - isConnected.BindValueChanged(c => Scheduler.AddOnce(() => - { - if (isConnected.Value && IsLoaded) - PollImmediately(); - }), true); + isConnected.BindValueChanged(c => Scheduler.AddOnce(poll), true); + } + + private void poll() + { + if (isConnected.Value && IsLoaded) + PollImmediately(); } protected override Task Poll() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs index 0f256160eb..a380ddef25 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs @@ -19,15 +19,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.LoadComplete(); - Client.RoomUpdated += OnRoomUpdated; - - Client.UserLeft += UserLeft; - Client.UserKicked += UserKicked; - Client.UserJoined += UserJoined; + Client.RoomUpdated += invokeOnRoomUpdated; + Client.UserLeft += invokeUserLeft; + Client.UserKicked += invokeUserKicked; + Client.UserJoined += invokeUserJoined; OnRoomUpdated(); } + private void invokeOnRoomUpdated() => Scheduler.AddOnce(OnRoomUpdated); + private void invokeUserJoined(MultiplayerRoomUser user) => Scheduler.AddOnce(UserJoined, user); + private void invokeUserKicked(MultiplayerRoomUser user) => Scheduler.AddOnce(UserKicked, user); + private void invokeUserLeft(MultiplayerRoomUser user) => Scheduler.AddOnce(UserLeft, user); + /// /// Invoked when a user has joined the room. /// @@ -63,10 +67,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { if (Client != null) { - Client.UserLeft -= UserLeft; - Client.UserKicked -= UserKicked; - Client.UserJoined -= UserJoined; - Client.RoomUpdated -= OnRoomUpdated; + Client.RoomUpdated -= invokeOnRoomUpdated; + Client.UserLeft -= invokeUserLeft; + Client.UserKicked -= invokeUserKicked; + Client.UserJoined -= invokeUserJoined; } base.Dispose(isDisposing); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs index 7384c60888..9e000aa712 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSettingsOverlay.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Specialized; using Humanizer; using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -77,14 +76,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OverlayColourProvider colourProvider, OsuColour colours) { InternalChildren = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"28242d"), + Colour = colourProvider.Background4 }, new GridContainer { @@ -256,7 +255,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f), + Colour = colourProvider.Background5 }, new FillFlowContainer { diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index ea158c5789..2a1c4599d5 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -6,11 +6,13 @@ using osu.Framework.Bindables; using osu.Game.Rulesets.UI; using System; using System.Collections.Generic; +using ManagedBass.Fx; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Utils; using osu.Game.Audio.Effects; using osu.Game.Beatmaps; @@ -24,25 +26,38 @@ namespace osu.Game.Screens.Play /// Manage the animation to be applied when a player fails. /// Single use and automatically disposed after use. /// - public class FailAnimation : CompositeDrawable + public class FailAnimation : Container { public Action OnComplete; private readonly DrawableRuleset drawableRuleset; - private readonly BindableDouble trackFreq = new BindableDouble(1); + private Container filters; + + private Box failFlash; + private Track track; private AudioFilter failLowPassFilter; + private AudioFilter failHighPassFilter; private const float duration = 2500; private Sample failSample; + protected override Container Content { get; } = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }; + public FailAnimation(DrawableRuleset drawableRuleset) { this.drawableRuleset = drawableRuleset; + + RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] @@ -51,7 +66,26 @@ namespace osu.Game.Screens.Play track = beatmap.Value.Track; failSample = audio.Samples.Get(@"Gameplay/failsound"); - AddInternal(failLowPassFilter = new AudioFilter(audio.TrackMixer)); + AddRangeInternal(new Drawable[] + { + filters = new Container + { + Children = new Drawable[] + { + failLowPassFilter = new AudioFilter(audio.TrackMixer), + failHighPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass), + }, + }, + Content, + failFlash = new Box + { + Colour = Color4.Red, + RelativeSizeAxes = Axes.Both, + Blending = BlendingParameters.Additive, + Depth = float.MinValue, + Alpha = 0 + }, + }); } private bool started; @@ -66,21 +100,42 @@ namespace osu.Game.Screens.Play started = true; - failSample.Play(); - this.TransformBindableTo(trackFreq, 0, duration).OnComplete(_ => { OnComplete?.Invoke(); - Expire(); }); + failHighPassFilter.CutoffTo(300); failLowPassFilter.CutoffTo(300, duration, Easing.OutCubic); + failSample.Play(); track.AddAdjustment(AdjustableProperty.Frequency, trackFreq); applyToPlayfield(drawableRuleset.Playfield); - drawableRuleset.Playfield.HitObjectContainer.FlashColour(Color4.Red, 500); drawableRuleset.Playfield.HitObjectContainer.FadeOut(duration / 2); + + failFlash.FadeOutFromOne(1000); + + Content.Masking = true; + + Content.Add(new Box + { + Colour = Color4.Black, + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue + }); + + Content.ScaleTo(0.85f, duration, Easing.OutQuart); + Content.RotateTo(1, duration, Easing.OutQuart); + Content.FadeColour(Color4.Gray, duration); + } + + public void RemoveFilters() + { + RemoveInternal(filters); + filters.Dispose(); + + track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq); } protected override void Update() @@ -129,11 +184,5 @@ namespace osu.Game.Screens.Play obj.MoveTo(originalPosition + new Vector2(0, 400), duration); } } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq); - } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 444bea049b..5398a955b3 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -230,17 +230,53 @@ namespace osu.Game.Screens.Play // this is intentionally done in two stages to ensure things are in a loaded state before exposing the ruleset to skin sources. GameplayClockContainer.Add(rulesetSkinProvider); - rulesetSkinProvider.AddRange(new[] + rulesetSkinProvider.AddRange(new Drawable[] { - // underlay and gameplay should have access to the skinning sources. - createUnderlayComponents(), - createGameplayComponents(Beatmap.Value, playableBeatmap) + failAnimationLayer = new FailAnimation(DrawableRuleset) + { + OnComplete = onFailComplete, + Children = new[] + { + // underlay and gameplay should have access to the skinning sources. + createUnderlayComponents(), + createGameplayComponents(Beatmap.Value, playableBeatmap) + } + }, + FailOverlay = new FailOverlay + { + OnRetry = Restart, + OnQuit = () => PerformExit(true), + }, + new HotkeyExitOverlay + { + Action = () => + { + if (!this.IsCurrentScreen()) return; + + fadeOut(true); + PerformExit(false); + }, + }, }); + if (Configuration.AllowRestart) + { + rulesetSkinProvider.Add(new HotkeyRetryOverlay + { + Action = () => + { + if (!this.IsCurrentScreen()) return; + + fadeOut(true); + Restart(); + }, + }); + } + // add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components. // also give the overlays the ruleset skin provider to allow rulesets to potentially override HUD elements (used to disable combo counters etc.) // we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there. - rulesetSkinProvider.Add(createOverlayComponents(Beatmap.Value)); + failAnimationLayer.Add(createOverlayComponents(Beatmap.Value)); if (!DrawableRuleset.AllowGameplayOverlays) { @@ -375,11 +411,6 @@ namespace osu.Game.Screens.Play RequestSkip = () => progressToResults(false), Alpha = 0 }, - FailOverlay = new FailOverlay - { - OnRetry = Restart, - OnQuit = () => PerformExit(true), - }, PauseOverlay = new PauseOverlay { OnResume = Resume, @@ -387,18 +418,7 @@ namespace osu.Game.Screens.Play OnRetry = Restart, OnQuit = () => PerformExit(true), }, - new HotkeyExitOverlay - { - Action = () => - { - if (!this.IsCurrentScreen()) return; - - fadeOut(true); - PerformExit(false); - }, - }, - failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, - } + }, }; if (!Configuration.AllowSkipping || !DrawableRuleset.AllowGameplayOverlays) @@ -410,20 +430,6 @@ namespace osu.Game.Screens.Play if (GameplayClockContainer is MasterGameplayClockContainer master) HUDOverlay.PlayerSettingsOverlay.PlaybackSettings.UserPlaybackRate.BindTarget = master.UserPlaybackRate; - if (Configuration.AllowRestart) - { - container.Add(new HotkeyRetryOverlay - { - Action = () => - { - if (!this.IsCurrentScreen()) return; - - fadeOut(true); - Restart(); - }, - }); - } - return container; } @@ -541,7 +547,7 @@ namespace osu.Game.Screens.Play // if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion). if (ValidForResume && HasFailed) { - failAnimation.FinishTransforms(true); + failAnimationLayer.FinishTransforms(true); return; } @@ -766,7 +772,7 @@ namespace osu.Game.Screens.Play protected FailOverlay FailOverlay { get; private set; } - private FailAnimation failAnimation; + private FailAnimation failAnimationLayer; private bool onFail() { @@ -782,7 +788,7 @@ namespace osu.Game.Screens.Play if (PauseOverlay.State.Value == Visibility.Visible) PauseOverlay.Hide(); - failAnimation.Start(); + failAnimationLayer.Start(); if (GameplayState.Mods.OfType().Any(m => m.RestartOnFail)) Restart(); @@ -956,7 +962,7 @@ namespace osu.Game.Screens.Play public override bool OnExiting(IScreen next) { screenSuspension?.RemoveAndDisposeImmediately(); - failAnimation?.RemoveAndDisposeImmediately(); + failAnimationLayer?.RemoveFilters(); // if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap. if (prepareScoreForDisplayTask == null) diff --git a/osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs index 8a4acacb24..26887327cd 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlayerCheckbox.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.Play.PlayerSettings { Nub.AccentColour = colours.Yellow; Nub.GlowingAccentColour = colours.YellowLighter; - Nub.GlowColour = colours.YellowDarker; + Nub.GlowColour = colours.YellowDark; } } } diff --git a/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs index c8e281195a..216e46d429 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlayerSliderBar.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Play.PlayerSettings AccentColour = colours.Yellow; Nub.AccentColour = colours.Yellow; Nub.GlowingAccentColour = colours.YellowLighter; - Nub.GlowColour = colours.YellowDarker; + Nub.GlowColour = colours.YellowDark; } } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 6cafcb9d16..a2dea355ac 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -410,7 +410,7 @@ namespace osu.Game.Screens.Select { if (e.NewValue is DummyWorkingBeatmap || !this.IsCurrentScreen()) return; - Logger.Log($"working beatmap updated to {e.NewValue}"); + Logger.Log($"Song select working beatmap updated to {e.NewValue}"); if (!Carousel.SelectBeatmap(e.NewValue.BeatmapInfo, false)) { diff --git a/osu.Game/Stores/RealmRulesetStore.cs b/osu.Game/Stores/RealmRulesetStore.cs new file mode 100644 index 0000000000..27eb5d797f --- /dev/null +++ b/osu.Game/Stores/RealmRulesetStore.cs @@ -0,0 +1,263 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using osu.Framework; +using osu.Framework.Extensions.ObjectExtensions; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Database; +using osu.Game.Models; +using osu.Game.Rulesets; + +#nullable enable + +namespace osu.Game.Stores +{ + public class RealmRulesetStore : IDisposable + { + private readonly RealmContextFactory realmFactory; + + private const string ruleset_library_prefix = @"osu.Game.Rulesets"; + + private readonly Dictionary loadedAssemblies = new Dictionary(); + + /// + /// All available rulesets. + /// + public IEnumerable AvailableRulesets => availableRulesets; + + private readonly List availableRulesets = new List(); + + public RealmRulesetStore(RealmContextFactory realmFactory, Storage? storage = null) + { + this.realmFactory = realmFactory; + + // On android in release configuration assemblies are loaded from the apk directly into memory. + // We cannot read assemblies from cwd, so should check loaded assemblies instead. + loadFromAppDomain(); + + // This null check prevents Android from attempting to load the rulesets from disk, + // as the underlying path "AppContext.BaseDirectory", despite being non-nullable, it returns null on android. + // See https://github.com/xamarin/xamarin-android/issues/3489. + if (RuntimeInfo.StartupDirectory != null) + loadFromDisk(); + + // the event handler contains code for resolving dependency on the game assembly for rulesets located outside the base game directory. + // It needs to be attached to the assembly lookup event before the actual call to loadUserRulesets() else rulesets located out of the base game directory will fail + // to load as unable to locate the game core assembly. + AppDomain.CurrentDomain.AssemblyResolve += resolveRulesetDependencyAssembly; + + var rulesetStorage = storage?.GetStorageForDirectory(@"rulesets"); + if (rulesetStorage != null) + loadUserRulesets(rulesetStorage); + + addMissingRulesets(); + } + + /// + /// Retrieve a ruleset using a known ID. + /// + /// The ruleset's internal ID. + /// A ruleset, if available, else null. + public IRulesetInfo? GetRuleset(int id) => AvailableRulesets.FirstOrDefault(r => r.OnlineID == id); + + /// + /// Retrieve a ruleset using a known short name. + /// + /// The ruleset's short name. + /// A ruleset, if available, else null. + public IRulesetInfo? GetRuleset(string shortName) => AvailableRulesets.FirstOrDefault(r => r.ShortName == shortName); + + private Assembly? resolveRulesetDependencyAssembly(object? sender, ResolveEventArgs args) + { + var asm = new AssemblyName(args.Name); + + // the requesting assembly may be located out of the executable's base directory, thus requiring manual resolving of its dependencies. + // this attempts resolving the ruleset dependencies on game core and framework assemblies by returning assemblies with the same assembly name + // already loaded in the AppDomain. + var domainAssembly = AppDomain.CurrentDomain.GetAssemblies() + // Given name is always going to be equally-or-more qualified than the assembly name. + .Where(a => + { + string? name = a.GetName().Name; + if (name == null) + return false; + + return args.Name.Contains(name, StringComparison.Ordinal); + }) + // Pick the greatest assembly version. + .OrderByDescending(a => a.GetName().Version) + .FirstOrDefault(); + + if (domainAssembly != null) + return domainAssembly; + + return loadedAssemblies.Keys.FirstOrDefault(a => a.FullName == asm.FullName); + } + + private void addMissingRulesets() + { + realmFactory.Context.Write(realm => + { + var rulesets = realm.All(); + + List instances = loadedAssemblies.Values + .Select(r => Activator.CreateInstance(r) as Ruleset) + .Where(r => r != null) + .Select(r => r.AsNonNull()) + .ToList(); + + // add all legacy rulesets first to ensure they have exclusive choice of primary key. + foreach (var r in instances.Where(r => r is ILegacyRuleset)) + { + if (realm.All().FirstOrDefault(rr => rr.OnlineID == r.RulesetInfo.ID) == null) + realm.Add(new RealmRuleset(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.ID)); + } + + // add any other rulesets which have assemblies present but are not yet in the database. + foreach (var r in instances.Where(r => !(r is ILegacyRuleset))) + { + if (rulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null) + { + var existingSameShortName = rulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName); + + if (existingSameShortName != null) + { + // even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName. + // this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one. + // in such cases, update the instantiation info of the existing entry to point to the new one. + existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo; + } + else + realm.Add(new RealmRuleset(r.RulesetInfo.ShortName, r.RulesetInfo.Name, r.RulesetInfo.InstantiationInfo, r.RulesetInfo.ID)); + } + } + + List detachedRulesets = new List(); + + // perform a consistency check and detach final rulesets from realm for cross-thread runtime usage. + foreach (var r in rulesets) + { + try + { + var type = Type.GetType(r.InstantiationInfo); + + if (type == null) + throw new InvalidOperationException(@"Type resolution failure."); + + var rInstance = (Activator.CreateInstance(type) as Ruleset)?.RulesetInfo; + + if (rInstance == null) + throw new InvalidOperationException(@"Instantiation failure."); + + r.Name = rInstance.Name; + r.ShortName = rInstance.ShortName; + r.InstantiationInfo = rInstance.InstantiationInfo; + r.Available = true; + + detachedRulesets.Add(r.Clone()); + } + catch (Exception ex) + { + r.Available = false; + Logger.Log($"Could not load ruleset {r}: {ex.Message}"); + } + } + + availableRulesets.AddRange(detachedRulesets); + }); + } + + private void loadFromAppDomain() + { + foreach (var ruleset in AppDomain.CurrentDomain.GetAssemblies()) + { + string? rulesetName = ruleset.GetName().Name; + + if (rulesetName == null) + continue; + + if (!rulesetName.StartsWith(ruleset_library_prefix, StringComparison.InvariantCultureIgnoreCase) || rulesetName.Contains(@"Tests")) + continue; + + addRuleset(ruleset); + } + } + + private void loadUserRulesets(Storage rulesetStorage) + { + var rulesets = rulesetStorage.GetFiles(@".", @$"{ruleset_library_prefix}.*.dll"); + + foreach (var ruleset in rulesets.Where(f => !f.Contains(@"Tests"))) + loadRulesetFromFile(rulesetStorage.GetFullPath(ruleset)); + } + + private void loadFromDisk() + { + try + { + var files = Directory.GetFiles(RuntimeInfo.StartupDirectory, @$"{ruleset_library_prefix}.*.dll"); + + foreach (string file in files.Where(f => !Path.GetFileName(f).Contains("Tests"))) + loadRulesetFromFile(file); + } + catch (Exception e) + { + Logger.Error(e, $"Could not load rulesets from directory {RuntimeInfo.StartupDirectory}"); + } + } + + private void loadRulesetFromFile(string file) + { + var filename = Path.GetFileNameWithoutExtension(file); + + if (loadedAssemblies.Values.Any(t => Path.GetFileNameWithoutExtension(t.Assembly.Location) == filename)) + return; + + try + { + addRuleset(Assembly.LoadFrom(file)); + } + catch (Exception e) + { + Logger.Error(e, $"Failed to load ruleset {filename}"); + } + } + + private void addRuleset(Assembly assembly) + { + if (loadedAssemblies.ContainsKey(assembly)) + return; + + // the same assembly may be loaded twice in the same AppDomain (currently a thing in certain Rider versions https://youtrack.jetbrains.com/issue/RIDER-48799). + // as a failsafe, also compare by FullName. + if (loadedAssemblies.Any(a => a.Key.FullName == assembly.FullName)) + return; + + try + { + loadedAssemblies[assembly] = assembly.GetTypes().First(t => t.IsPublic && t.IsSubclassOf(typeof(Ruleset))); + } + catch (Exception e) + { + Logger.Error(e, $"Failed to add ruleset {assembly}"); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + AppDomain.CurrentDomain.AssemblyResolve -= resolveRulesetDependencyAssembly; + } + } +} diff --git a/osu.Game/Tests/Visual/EditorClockTestScene.cs b/osu.Game/Tests/Visual/EditorClockTestScene.cs index 34393fba7d..c2e9892735 100644 --- a/osu.Game/Tests/Visual/EditorClockTestScene.cs +++ b/osu.Game/Tests/Visual/EditorClockTestScene.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Input.Events; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Screens.Edit; namespace osu.Game.Tests.Visual @@ -23,7 +22,7 @@ namespace osu.Game.Tests.Visual protected EditorClockTestScene() { - Clock = new EditorClock(new ControlPointInfo(), BeatDivisor) { IsCoupled = false }; + Clock = new EditorClock(new Beatmap(), BeatDivisor) { IsCoupled = false }; } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -44,7 +43,7 @@ namespace osu.Game.Tests.Visual private void beatmapChanged(ValueChangedEvent e) { - Clock.ControlPointInfo = e.NewValue.Beatmap.ControlPointInfo; + Clock.Beatmap = e.NewValue.Beatmap; Clock.ChangeSource(e.NewValue.Track); Clock.ProcessFrame(); } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 2c0ca0b872..5e4e5942d9 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -53,7 +53,8 @@ namespace osu.Game.Tests.Visual.Multiplayer public MultiplayerRoomUser AddUser(User user, bool markAsPlaying = false) { var roomUser = new MultiplayerRoomUser(user.Id) { User = user }; - ((IMultiplayerClient)this).UserJoined(roomUser); + + addUser(roomUser); if (markAsPlaying) PlayingUserIds.Add(user.Id); @@ -61,7 +62,15 @@ namespace osu.Game.Tests.Visual.Multiplayer return roomUser; } - public void AddNullUser() => ((IMultiplayerClient)this).UserJoined(new MultiplayerRoomUser(TestUserLookupCache.NULL_USER_ID)); + public void AddNullUser() => addUser(new MultiplayerRoomUser(TestUserLookupCache.NULL_USER_ID)); + + private void addUser(MultiplayerRoomUser user) + { + ((IMultiplayerClient)this).UserJoined(user).Wait(); + + // We want the user to be immediately available for testing, so force a scheduler update to run the update-bound continuation. + Scheduler.Update(); + } public void RemoveUser(User user) { diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index c6121ddd5f..32d6eeab29 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,11 +36,12 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - + + + diff --git a/osu.iOS.props b/osu.iOS.props index 110de79285..92abab036a 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,8 +70,8 @@ - - + + @@ -93,7 +93,7 @@ - +