diff --git a/.github/workflows/diffcalc.yml b/.github/workflows/diffcalc.yml index 3c57d971ae..bc2626d3d6 100644 --- a/.github/workflows/diffcalc.yml +++ b/.github/workflows/diffcalc.yml @@ -74,16 +74,25 @@ jobs: mkdir -p $GITHUB_WORKSPACE/master/ mkdir -p $GITHUB_WORKSPACE/pr/ + - name: Get upstream branch # https://akaimo.hatenablog.jp/entry/2020/05/16/101251 + id: upstreambranch + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "::set-output name=branchname::$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.issue.pull_request.url }} | jq '.head.ref' | sed 's/\"//g')" + echo "::set-output name=repo::$(curl -H "Authorization: token ${GITHUB_TOKEN}" ${{ github.event.issue.pull_request.url }} | jq '.head.repo.full_name' | sed 's/\"//g')" + # Checkout osu - name: Checkout osu (master) uses: actions/checkout@v2 with: - repository: ppy/osu path: 'master/osu' - name: Checkout osu (pr) uses: actions/checkout@v2 with: path: 'pr/osu' + repository: ${{ steps.upstreambranch.outputs.repo }} + ref: ${{ steps.upstreambranch.outputs.branchname }} - name: Checkout osu-difficulty-calculator (master) uses: actions/checkout@v2 diff --git a/osu.Android.props b/osu.Android.props index 7a739e6f2a..967405cd2e 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,7 +52,7 @@ - + diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs new file mode 100644 index 0000000000..47b2d3a098 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditorGrids.cs @@ -0,0 +1,78 @@ +// 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.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Rulesets.Osu.Edit; +using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; +using osu.Game.Tests.Visual; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + public class TestSceneOsuEditorGrids : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + + [Test] + public void TestGridExclusivity() + { + AddStep("enable distance snap grid", () => InputManager.Key(Key.T)); + AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1))); + AddUntilStep("distance snap grid visible", () => this.ChildrenOfType().Any()); + rectangularGridActive(false); + + AddStep("enable rectangular grid", () => InputManager.Key(Key.Y)); + AddUntilStep("distance snap grid hidden", () => !this.ChildrenOfType().Any()); + rectangularGridActive(true); + + AddStep("enable distance snap grid", () => InputManager.Key(Key.T)); + AddStep("select second object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.ElementAt(1))); + AddUntilStep("distance snap grid visible", () => this.ChildrenOfType().Any()); + rectangularGridActive(false); + } + + private void rectangularGridActive(bool active) + { + AddStep("choose placement tool", () => InputManager.Key(Key.Number2)); + AddStep("move cursor to (1, 1)", () => + { + var composer = Editor.ChildrenOfType().Single(); + InputManager.MoveMouseTo(composer.ToScreenSpace(new Vector2(1, 1))); + }); + + if (active) + AddAssert("placement blueprint at (0, 0)", () => Precision.AlmostEquals(Editor.ChildrenOfType().Single().HitObject.Position, new Vector2(0, 0))); + else + AddAssert("placement blueprint at (1, 1)", () => Precision.AlmostEquals(Editor.ChildrenOfType().Single().HitObject.Position, new Vector2(1, 1))); + + AddStep("choose selection tool", () => InputManager.Key(Key.Number1)); + } + + [Test] + public void TestGridSizeToggling() + { + AddStep("enable rectangular grid", () => InputManager.Key(Key.Y)); + AddUntilStep("rectangular grid visible", () => this.ChildrenOfType().Any()); + gridSizeIs(4); + + nextGridSizeIs(8); + nextGridSizeIs(16); + nextGridSizeIs(32); + nextGridSizeIs(4); + } + + private void nextGridSizeIs(int size) + { + AddStep("toggle to next grid size", () => InputManager.Key(Key.G)); + gridSizeIs(size); + } + + private void gridSizeIs(int size) + => AddAssert($"grid size is {size}", () => this.ChildrenOfType().Single().Spacing == new Vector2(size) + && EditorBeatmap.BeatmapInfo.GridSize == size); + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs index 11b1f5b2af..bd39dead34 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorParticles.cs @@ -98,6 +98,7 @@ namespace osu.Game.Rulesets.Osu.Tests { var controlPointInfo = new ControlPointInfo(); controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true }); + controlPointInfo.Add(5000, new EffectControlPoint { KiaiMode = false }); return new Beatmap { diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index cb3338126c..4c8d0b2ce6 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -21,6 +21,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty public class OsuDifficultyCalculator : DifficultyCalculator { private const double difficulty_multiplier = 0.0675; + private double hitWindowGreat; public OsuDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) @@ -52,11 +53,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0; - HitWindows hitWindows = new OsuHitWindows(); - hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); - - // Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be removed in the future - double hitWindowGreat = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate; double preempt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; int maxCombo = beatmap.HitObjects.Count; @@ -96,12 +92,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty } } - protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[] + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) { - new Aim(mods), - new Speed(mods), - new Flashlight(mods) - }; + HitWindows hitWindows = new OsuHitWindows(); + hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); + + // Todo: These int casts are temporary to achieve 1:1 results with osu!stable, and should be removed in the future + hitWindowGreat = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate; + + return new Skill[] + { + new Aim(mods), + new Speed(mods, hitWindowGreat), + new Flashlight(mods) + }; + } protected override Mod[] DifficultyAdjustmentMods => new Mod[] { diff --git a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs index fa6c5c4d9c..8e8f9bc06e 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Preprocessing/OsuDifficultyHitObject.cs @@ -16,6 +16,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject; + /// + /// Milliseconds elapsed since the start time of the previous , with a minimum of 25ms to account for simultaneous s. + /// + public double StrainTime { get; private set; } + /// /// Normalized distance from the end position of the previous to the start position of this . /// @@ -32,11 +37,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing /// public double? Angle { get; private set; } - /// - /// Milliseconds elapsed since the start time of the previous , with a minimum of 50ms. - /// - public readonly double StrainTime; - private readonly OsuHitObject lastLastObject; private readonly OsuHitObject lastObject; @@ -48,8 +48,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing setDistances(); - // Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure - StrainTime = Math.Max(50, DeltaTime); + // Capped to 25ms to prevent difficulty calculation breaking from simulatenous objects. + StrainTime = Math.Max(DeltaTime, 25); } private void setDistances() diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index f0eb199e5f..9364b11048 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -6,6 +6,7 @@ using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; +using osu.Framework.Utils; namespace osu.Game.Rulesets.Osu.Difficulty.Skills { @@ -26,12 +27,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills protected override double DifficultyMultiplier => 1.04; private const double min_speed_bonus = 75; // ~200BPM - private const double max_speed_bonus = 45; // ~330BPM private const double speed_balancing_factor = 40; - public Speed(Mod[] mods) + private readonly double greatWindow; + + public Speed(Mod[] mods, double hitWindowGreat) : base(mods) { + greatWindow = hitWindowGreat; } protected override double StrainValueOf(DifficultyHitObject current) @@ -40,13 +43,25 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills return 0; var osuCurrent = (OsuDifficultyHitObject)current; + var osuPrevious = Previous.Count > 0 ? (OsuDifficultyHitObject)Previous[0] : null; double distance = Math.Min(single_spacing_threshold, osuCurrent.TravelDistance + osuCurrent.JumpDistance); - double deltaTime = Math.Max(max_speed_bonus, current.DeltaTime); + double strainTime = osuCurrent.StrainTime; + + double greatWindowFull = greatWindow * 2; + double speedWindowRatio = strainTime / greatWindowFull; + + // Aim to nerf cheesy rhythms (Very fast consecutive doubles with large deltatimes between) + if (osuPrevious != null && strainTime < greatWindowFull && osuPrevious.StrainTime > strainTime) + strainTime = Interpolation.Lerp(osuPrevious.StrainTime, strainTime, speedWindowRatio); + + // Cap deltatime to the OD 300 hitwindow. + // 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap. + strainTime /= Math.Clamp((strainTime / greatWindowFull) / 0.93, 0.92, 1); double speedBonus = 1.0; - if (deltaTime < min_speed_bonus) - speedBonus = 1 + Math.Pow((min_speed_bonus - deltaTime) / speed_balancing_factor, 2); + if (strainTime < min_speed_bonus) + speedBonus = 1 + Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2); double angleBonus = 1.0; @@ -64,7 +79,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills } } - return (1 + (speedBonus - 1) * 0.75) * angleBonus * (0.95 + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / osuCurrent.StrainTime; + return (1 + (speedBonus - 1) * 0.75) + * angleBonus + * (0.95 + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) + / strainTime; } } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 806b7e6051..1e84ec80e1 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -42,10 +42,12 @@ namespace osu.Game.Rulesets.Osu.Edit }; private readonly Bindable distanceSnapToggle = new Bindable(); + private readonly Bindable rectangularGridSnapToggle = new Bindable(); protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[] { - new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }) + new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }), + new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Th }) }); private BindableList selectedHitObjects; @@ -63,6 +65,10 @@ namespace osu.Game.Rulesets.Osu.Edit PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners } }, distanceSnapGridContainer = new Container + { + RelativeSizeAxes = Axes.Both + }, + rectangularPositionSnapGrid = new OsuRectangularPositionSnapGrid { RelativeSizeAxes = Axes.Both } @@ -73,7 +79,19 @@ namespace osu.Game.Rulesets.Osu.Edit placementObject = EditorBeatmap.PlacementObject.GetBoundCopy(); placementObject.ValueChanged += _ => updateDistanceSnapGrid(); - distanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid(); + distanceSnapToggle.ValueChanged += _ => + { + updateDistanceSnapGrid(); + + if (distanceSnapToggle.Value == TernaryState.True) + rectangularGridSnapToggle.Value = TernaryState.False; + }; + + rectangularGridSnapToggle.ValueChanged += _ => + { + if (rectangularGridSnapToggle.Value == TernaryState.True) + distanceSnapToggle.Value = TernaryState.False; + }; // we may be entering the screen with a selection already active updateDistanceSnapGrid(); @@ -91,6 +109,8 @@ namespace osu.Game.Rulesets.Osu.Edit private readonly Cached distanceSnapGridCache = new Cached(); private double? lastDistanceSnapGridTime; + private RectangularPositionSnapGrid rectangularPositionSnapGrid; + protected override void Update() { base.Update(); @@ -122,13 +142,19 @@ namespace osu.Game.Rulesets.Osu.Edit if (positionSnap.ScreenSpacePosition != screenSpacePosition) return positionSnap; - // will be null if distance snap is disabled or not feasible for the current time value. - if (distanceSnapGrid == null) - return base.SnapScreenSpacePositionToValidTime(screenSpacePosition); + if (distanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null) + { + (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); + return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition)); + } - (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition)); + if (rectangularGridSnapToggle.Value == TernaryState.True) + { + Vector2 pos = rectangularPositionSnapGrid.GetSnappedPosition(rectangularPositionSnapGrid.ToLocalSpace(screenSpacePosition)); + return new SnapResult(rectangularPositionSnapGrid.ToScreenSpace(pos), null, PlayfieldAtScreenSpacePosition(screenSpacePosition)); + } - return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition)); + return base.SnapScreenSpacePositionToValidTime(screenSpacePosition); } private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuRectangularPositionSnapGrid.cs b/osu.Game.Rulesets.Osu/Edit/OsuRectangularPositionSnapGrid.cs new file mode 100644 index 0000000000..b8ff92bd37 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Edit/OsuRectangularPositionSnapGrid.cs @@ -0,0 +1,69 @@ +// 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.Framework.Allocation; +using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; +using osu.Game.Input.Bindings; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Compose.Components; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Edit +{ + public class OsuRectangularPositionSnapGrid : RectangularPositionSnapGrid, IKeyBindingHandler + { + private static readonly int[] grid_sizes = { 4, 8, 16, 32 }; + + private int currentGridSizeIndex = grid_sizes.Length - 1; + + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } + + public OsuRectangularPositionSnapGrid() + : base(OsuPlayfield.BASE_SIZE / 2) + { + } + + [BackgroundDependencyLoader] + private void load() + { + var gridSizeIndex = Array.IndexOf(grid_sizes, editorBeatmap.BeatmapInfo.GridSize); + if (gridSizeIndex >= 0) + currentGridSizeIndex = gridSizeIndex; + updateSpacing(); + } + + private void nextGridSize() + { + currentGridSizeIndex = (currentGridSizeIndex + 1) % grid_sizes.Length; + updateSpacing(); + } + + private void updateSpacing() + { + int gridSize = grid_sizes[currentGridSizeIndex]; + + editorBeatmap.BeatmapInfo.GridSize = gridSize; + Spacing = new Vector2(gridSize); + } + + public bool OnPressed(KeyBindingPressEvent e) + { + switch (e.Action) + { + case GlobalAction.EditorCycleGridDisplayMode: + nextGridSize(); + return true; + } + + return false; + } + + public void OnReleased(KeyBindingReleaseEvent e) + { + } + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs index 2b0dfba1dd..c2db5f3f82 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorParticles.cs @@ -39,9 +39,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy [Resolved(canBeNull: true)] private GameplayBeatmap gameplayBeatmap { get; set; } - [Resolved(canBeNull: true)] - private GameplayClock gameplayClock { get; set; } - [BackgroundDependencyLoader] private void load(ISkinSource skin, OsuColour colours) { @@ -83,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy DrawableHitObject kiaiHitObject = null; // Check whether currently in a kiai section first. This is only done as an optimisation to avoid enumerating AliveObjects when not necessary. - if (gameplayBeatmap.ControlPointInfo.EffectPointAt(gameplayBeatmap.Time.Current).KiaiMode) + if (gameplayBeatmap.ControlPointInfo.EffectPointAt(Time.Current).KiaiMode) kiaiHitObject = playfield.HitObjectContainer.AliveObjects.FirstOrDefault(isTracking); kiaiSpewer.Active.Value = kiaiHitObject != null; diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs index a6b3fe1cd9..7f565cb82d 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHidden.cs @@ -2,45 +2,55 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects.Drawables; +using osu.Game.Rulesets.Taiko.UI; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Taiko.Mods { - public class TaikoModHidden : ModHidden + public class TaikoModHidden : ModHidden, IApplicableToDrawableRuleset { public override string Description => @"Beats fade out before you hit them!"; public override double ScoreMultiplier => 1.06; - private ControlPointInfo controlPointInfo; + /// + /// How far away from the hit target should hitobjects start to fade out. + /// Range: [0, 1] + /// + private const float fade_out_start_time = 1f; + + /// + /// How long hitobjects take to fade out, in terms of the scrolling length. + /// Range: [0, 1] + /// + private const float fade_out_duration = 0.375f; + + private DrawableTaikoRuleset drawableRuleset; + + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + this.drawableRuleset = (DrawableTaikoRuleset)drawableRuleset; + } protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) { ApplyNormalVisibilityState(hitObject, state); } - protected double MultiplierAt(double position) - { - double beatLength = controlPointInfo.TimingPointAt(position).BeatLength; - double speedMultiplier = controlPointInfo.DifficultyPointAt(position).SpeedMultiplier; - - return speedMultiplier * TimingControlPoint.DEFAULT_BEAT_LENGTH / beatLength; - } - protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) { switch (hitObject) { case DrawableDrumRollTick _: case DrawableHit _: - double preempt = 10000 / MultiplierAt(hitObject.HitObject.StartTime); - double start = hitObject.HitObject.StartTime - preempt * 0.6; - double duration = preempt * 0.3; + double preempt = drawableRuleset.TimeRange.Value / drawableRuleset.ControlPointAt(hitObject.HitObject.StartTime).Multiplier; + double start = hitObject.HitObject.StartTime - preempt * fade_out_start_time; + double duration = preempt * fade_out_duration; using (hitObject.BeginAbsoluteSequence(start)) { @@ -56,10 +66,5 @@ namespace osu.Game.Rulesets.Taiko.Mods break; } } - - public override void ApplyToBeatmap(IBeatmap beatmap) - { - controlPointInfo = beatmap.ControlPointInfo; - } } } diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index 6ddbf3c16b..824b95639b 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.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 System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -16,6 +17,7 @@ using osu.Game.Input.Handlers; using osu.Game.Replays; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Timing; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Scoring; using osu.Game.Skinning; @@ -60,6 +62,14 @@ namespace osu.Game.Rulesets.Taiko.UI scroller.Height = ToLocalSpace(playfieldScreen.TopLeft + new Vector2(0, playfieldScreen.Height / 20)).Y; } + public MultiplierControlPoint ControlPointAt(double time) + { + int result = ControlPoints.BinarySearch(new MultiplierControlPoint(time)); + if (result < 0) + result = Math.Clamp(~result - 1, 0, ControlPoints.Count); + return ControlPoints[result]; + } + public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer(); protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.cs new file mode 100644 index 0000000000..85a98eca47 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneRectangularPositionSnapGrid.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; +using NUnit.Framework; +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.Screens.Edit.Compose.Components; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneRectangularPositionSnapGrid : OsuManualInputManagerTestScene + { + private Container content; + protected override Container Content => content; + + [BackgroundDependencyLoader] + private void load() + { + base.Content.AddRange(new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Colour4.Gray + }, + content = new Container + { + RelativeSizeAxes = Axes.Both + } + }); + } + + private static readonly object[][] test_cases = + { + new object[] { new Vector2(0, 0), new Vector2(10, 10) }, + new object[] { new Vector2(240, 180), new Vector2(10, 15) }, + new object[] { new Vector2(160, 120), new Vector2(30, 20) }, + new object[] { new Vector2(480, 360), new Vector2(100, 100) }, + }; + + [TestCaseSource(nameof(test_cases))] + public void TestRectangularGrid(Vector2 position, Vector2 spacing) + { + RectangularPositionSnapGrid grid = null; + + AddStep("create grid", () => Child = grid = new RectangularPositionSnapGrid(position) + { + RelativeSizeAxes = Axes.Both, + Spacing = spacing + }); + + AddStep("add snapping cursor", () => Add(new SnappingCursorContainer + { + RelativeSizeAxes = Axes.Both, + GetSnapPosition = pos => grid.GetSnappedPosition(grid.ToLocalSpace(pos)) + })); + } + + private class SnappingCursorContainer : CompositeDrawable + { + public Func GetSnapPosition; + + private readonly Drawable cursor; + + public SnappingCursorContainer() + { + RelativeSizeAxes = Axes.Both; + + InternalChild = cursor = new Circle + { + Origin = Anchor.Centre, + Size = new Vector2(50), + Colour = Color4.Red + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + updatePosition(GetContainingInputManager().CurrentState.Mouse.Position); + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + base.OnMouseMove(e); + + updatePosition(e.ScreenSpaceMousePosition); + return true; + } + + private void updatePosition(Vector2 screenSpacePosition) + { + cursor.Position = GetSnapPosition.Invoke(screenSpacePosition); + } + } + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDialogOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDialogOverlay.cs index f5cba2c900..405461eec8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDialogOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDialogOverlay.cs @@ -24,9 +24,10 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestBasic() { - TestPopupDialog dialog = null; + TestPopupDialog firstDialog = null; + TestPopupDialog secondDialog = null; - AddStep("dialog #1", () => overlay.Push(dialog = new TestPopupDialog + AddStep("dialog #1", () => overlay.Push(firstDialog = new TestPopupDialog { Icon = FontAwesome.Regular.TrashAlt, HeaderText = @"Confirm deletion of", @@ -46,9 +47,9 @@ namespace osu.Game.Tests.Visual.UserInterface }, })); - AddAssert("first dialog displayed", () => overlay.CurrentDialog == dialog); + AddAssert("first dialog displayed", () => overlay.CurrentDialog == firstDialog); - AddStep("dialog #2", () => overlay.Push(dialog = new TestPopupDialog + AddStep("dialog #2", () => overlay.Push(secondDialog = new TestPopupDialog { Icon = FontAwesome.Solid.Cog, HeaderText = @"What do you want to do with", @@ -82,30 +83,33 @@ namespace osu.Game.Tests.Visual.UserInterface }, })); - AddAssert("second dialog displayed", () => overlay.CurrentDialog == dialog); + AddAssert("second dialog displayed", () => overlay.CurrentDialog == secondDialog); + AddAssert("first dialog is not part of hierarchy", () => firstDialog.Parent == null); } [Test] public void TestDismissBeforePush() { + TestPopupDialog testDialog = null; AddStep("dismissed dialog push", () => { - overlay.Push(new TestPopupDialog + overlay.Push(testDialog = new TestPopupDialog { State = { Value = Visibility.Hidden } }); }); AddAssert("no dialog pushed", () => overlay.CurrentDialog == null); + AddAssert("dialog is not part of hierarchy", () => testDialog.Parent == null); } [Test] public void TestDismissBeforePushViaButtonPress() { + TestPopupDialog testDialog = null; AddStep("dismissed dialog push", () => { - TestPopupDialog dialog; - overlay.Push(dialog = new TestPopupDialog + overlay.Push(testDialog = new TestPopupDialog { Buttons = new PopupDialogButton[] { @@ -113,10 +117,11 @@ namespace osu.Game.Tests.Visual.UserInterface }, }); - dialog.PerformOkAction(); + testDialog.PerformOkAction(); }); AddAssert("no dialog pushed", () => overlay.CurrentDialog == null); + AddAssert("dialog is not part of hierarchy", () => testDialog.Parent == null); } private class TestPopupDialog : PopupDialog diff --git a/osu.Game/Graphics/ParticleExplosion.cs b/osu.Game/Graphics/ParticleExplosion.cs index e0d2b50c55..094cc87bbe 100644 --- a/osu.Game/Graphics/ParticleExplosion.cs +++ b/osu.Game/Graphics/ParticleExplosion.cs @@ -115,6 +115,8 @@ namespace osu.Game.Graphics null, TextureCoords); } } + + protected override bool CanDrawOpaqueInterior => false; } private readonly struct ParticlePart diff --git a/osu.Game/Graphics/ParticleSpewer.cs b/osu.Game/Graphics/ParticleSpewer.cs index 466bf04369..54a2b1e890 100644 --- a/osu.Game/Graphics/ParticleSpewer.cs +++ b/osu.Game/Graphics/ParticleSpewer.cs @@ -164,6 +164,8 @@ namespace osu.Game.Graphics return Vector2Extensions.Transform(new Vector2(x, y), DrawInfo.Matrix); } + + protected override bool CanDrawOpaqueInterior => false; } #endregion diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index 42f628a75a..fe88e6f78a 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -274,21 +274,40 @@ namespace osu.Game.Graphics.UserInterface CornerRadius = corner_radius; Height = 40; - Foreground.Children = new Drawable[] + Foreground.Child = new GridContainer { - Text = new OsuSpriteText + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + new Dimension(GridSizeMode.AutoSize), }, - Icon = new SpriteIcon + ColumnDimensions = new[] { - Icon = FontAwesome.Solid.ChevronDown, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Margin = new MarginPadding { Right = 5 }, - Size = new Vector2(12), + new Dimension(), + new Dimension(GridSizeMode.AutoSize), }, + Content = new[] + { + new Drawable[] + { + Text = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + Truncate = true, + }, + Icon = new SpriteIcon + { + Icon = FontAwesome.Solid.ChevronDown, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Margin = new MarginPadding { Horizontal = 5 }, + Size = new Vector2(12), + }, + } + } }; AddInternal(new HoverClickSounds()); diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index f62131e2d7..9fd7caadd0 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -75,6 +75,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.A }, GlobalAction.EditorVerifyMode), new KeyBinding(new[] { InputKey.J }, GlobalAction.EditorNudgeLeft), new KeyBinding(new[] { InputKey.K }, GlobalAction.EditorNudgeRight), + new KeyBinding(new[] { InputKey.G }, GlobalAction.EditorCycleGridDisplayMode), }; public IEnumerable InGameKeyBindings => new[] @@ -284,6 +285,9 @@ namespace osu.Game.Input.Bindings SeekReplayBackward, [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.ToggleChatFocus))] - ToggleChatFocus + ToggleChatFocus, + + [LocalisableDescription(typeof(GlobalActionKeyBindingStrings), nameof(GlobalActionKeyBindingStrings.EditorCycleGridDisplayMode))] + EditorCycleGridDisplayMode } } diff --git a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs index 14159f0d34..06f1b094bf 100644 --- a/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs +++ b/osu.Game/Localisation/GlobalActionKeyBindingStrings.cs @@ -164,6 +164,11 @@ namespace osu.Game.Localisation /// public static LocalisableString EditorTimingMode => new TranslatableString(getKey(@"editor_timing_mode"), @"Timing mode"); + /// + /// "Cycle grid display mode" + /// + public static LocalisableString EditorCycleGridDisplayMode => new TranslatableString(getKey(@"editor_cycle_grid_display_mode"), @"Cycle grid display mode"); + /// /// "Hold for HUD" /// diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index d5d31343f2..f051e09c08 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -49,6 +49,8 @@ namespace osu.Game.Overlays Show(); } + public override bool IsPresent => dialogContainer.Children.Count > 0; + protected override bool BlockNonPositionalInput => true; private void onDialogOnStateChanged(VisibilityContainer dialog, Visibility v) diff --git a/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs new file mode 100644 index 0000000000..95b4b2fe53 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/RectangularPositionSnapGrid.cs @@ -0,0 +1,113 @@ +// 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.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Layout; +using osuTK; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + public class RectangularPositionSnapGrid : CompositeDrawable + { + /// + /// The position of the origin of this in local coordinates. + /// + public Vector2 StartPosition { get; } + + private Vector2 spacing = Vector2.One; + + /// + /// The spacing between grid lines of this . + /// + public Vector2 Spacing + { + get => spacing; + set + { + if (spacing.X <= 0 || spacing.Y <= 0) + throw new ArgumentException("Grid spacing must be positive."); + + spacing = value; + gridCache.Invalidate(); + } + } + + private readonly LayoutValue gridCache = new LayoutValue(Invalidation.RequiredParentSizeToFit); + + public RectangularPositionSnapGrid(Vector2 startPosition) + { + StartPosition = startPosition; + + AddLayout(gridCache); + } + + protected override void Update() + { + base.Update(); + + if (!gridCache.IsValid) + { + ClearInternal(); + createContent(); + gridCache.Validate(); + } + } + + private void createContent() + { + var drawSize = DrawSize; + + generateGridLines(Direction.Horizontal, StartPosition.Y, 0, -Spacing.Y); + generateGridLines(Direction.Horizontal, StartPosition.Y, drawSize.Y, Spacing.Y); + + generateGridLines(Direction.Vertical, StartPosition.X, 0, -Spacing.X); + generateGridLines(Direction.Vertical, StartPosition.X, drawSize.X, Spacing.X); + } + + private void generateGridLines(Direction direction, float startPosition, float endPosition, float step) + { + int index = 0; + float currentPosition = startPosition; + + while ((endPosition - currentPosition) * Math.Sign(step) > 0) + { + var gridLine = new Box + { + Colour = Colour4.White, + Alpha = index == 0 ? 0.3f : 0.1f, + EdgeSmoothness = new Vector2(0.2f) + }; + + if (direction == Direction.Horizontal) + { + gridLine.RelativeSizeAxes = Axes.X; + gridLine.Height = 1; + gridLine.Y = currentPosition; + } + else + { + gridLine.RelativeSizeAxes = Axes.Y; + gridLine.Width = 1; + gridLine.X = currentPosition; + } + + AddInternal(gridLine); + + index += 1; + currentPosition = startPosition + index * step; + } + } + + public Vector2 GetSnappedPosition(Vector2 original) + { + Vector2 relativeToStart = original - StartPosition; + Vector2 offset = Vector2.Divide(relativeToStart, Spacing); + Vector2 roundedOffset = new Vector2(MathF.Round(offset.X), MathF.Round(offset.Y)); + + return StartPosition + Vector2.Multiply(roundedOffset, Spacing); + } + } +} diff --git a/osu.Game/Screens/Play/KeyCounterDisplay.cs b/osu.Game/Screens/Play/KeyCounterDisplay.cs index 2ed4afafd3..66a44e5314 100644 --- a/osu.Game/Screens/Play/KeyCounterDisplay.cs +++ b/osu.Game/Screens/Play/KeyCounterDisplay.cs @@ -33,8 +33,6 @@ namespace osu.Game.Screens.Play public KeyCounterDisplay() { - AutoSizeAxes = Axes.Both; - InternalChild = KeyFlow = new FillFlowContainer { Direction = FillDirection.Horizontal, @@ -42,6 +40,15 @@ namespace osu.Game.Screens.Play }; } + protected override void Update() + { + base.Update(); + + // Don't use autosize as it will shrink to zero when KeyFlow is hidden. + // In turn this can cause the display to be masked off screen and never become visible again. + Size = KeyFlow.Size; + } + public override void Add(KeyCounter key) { if (key == null) throw new ArgumentNullException(nameof(key)); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 82497fe550..61ae6aa69e 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -36,7 +36,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/osu.iOS.props b/osu.iOS.props index b547e98419..e032926a9c 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - +