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 @@
-
+