1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-13 03:15:06 +08:00

Merge branch 'master' into slider_fix_lazyjump

This commit is contained in:
Givikap120 2024-11-13 18:15:37 +02:00
commit 60efb5ef92
37 changed files with 410 additions and 164 deletions

View File

@ -26,7 +26,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="System.IO.Packaging" Version="8.0.1" /> <PackageReference Include="System.IO.Packaging" Version="8.0.1" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" /> <PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="Velopack" Version="0.0.630-g9c52e40" /> <PackageReference Include="Velopack" Version="0.0.869" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Resources"> <ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" /> <EmbeddedResource Include="lazer.ico" />

View File

@ -5,6 +5,7 @@ using System;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -73,7 +74,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
// 0.0 +--------+-+---------------> Release Difference / ms // 0.0 +--------+-+---------------> Release Difference / ms
// release_threshold // release_threshold
if (isOverlapping) if (isOverlapping)
holdAddition = 1 / (1 + Math.Exp(0.27 * (release_threshold - closestEndTime))); holdAddition = DifficultyCalculationUtils.Logistic(x: closestEndTime, multiplier: 0.27, midpointOffset: release_threshold);
// Decay and increase individualStrains in own column // Decay and increase individualStrains in own column
individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base); individualStrains[column] = applyDecay(individualStrains[column], startTime - startTimes[column], individual_decay_base);

View File

@ -54,7 +54,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
}, },
columnBackgrounds = new ColumnFlow<Drawable>(stageDefinition) columnBackgrounds = new ColumnFlow<Drawable>(stageDefinition)
{ {
RelativeSizeAxes = Axes.Y RelativeSizeAxes = Axes.Y,
Masking = false,
}, },
new HitTargetInsetContainer new HitTargetInsetContainer
{ {
@ -126,8 +127,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
}, },
new Container new Container
{ {
X = isLastColumn ? -0.16f : 0,
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Width = rightLineWidth, Width = rightLineWidth,
Scale = new Vector2(0.740f, 1), Scale = new Vector2(0.740f, 1),

View File

@ -28,6 +28,12 @@ namespace osu.Game.Rulesets.Mania.UI
private readonly FillFlowContainer<Container<TContent>> columns; private readonly FillFlowContainer<Container<TContent>> columns;
private readonly StageDefinition stageDefinition; private readonly StageDefinition stageDefinition;
public new bool Masking
{
get => base.Masking;
set => base.Masking = value;
}
public ColumnFlow(StageDefinition stageDefinition) public ColumnFlow(StageDefinition stageDefinition)
{ {
this.stageDefinition = stageDefinition; this.stageDefinition = stageDefinition;

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Components.RadioButtons; using osu.Game.Screens.Edit.Components.RadioButtons;
@ -24,38 +25,57 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
[Test] [Test]
public void TestTouchInputAfterTouchingComposeArea() public void TestTouchInputPlaceHitCircleDirectly()
{ {
AddStep("tap circle", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "HitCircle"))); AddStep("tap circle", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "HitCircle")));
// this input is just for interacting with compose area AddStep("tap to place circle", () => tap(this.ChildrenOfType<Playfield>().Single()));
AddStep("tap playfield", () => tap(this.ChildrenOfType<Playfield>().Single()));
AddStep("move current time", () => InputManager.Key(Key.Right));
AddStep("tap to place circle", () => tap(this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(10, 10))));
AddAssert("circle placed correctly", () => AddAssert("circle placed correctly", () =>
{ {
var circle = (HitCircle)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate); var circle = (HitCircle)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate);
Assert.Multiple(() => Assert.Multiple(() =>
{ {
Assert.That(circle.Position.X, Is.EqualTo(10f).Within(0.01f)); Assert.That(circle.Position.X, Is.EqualTo(256f).Within(0.01f));
Assert.That(circle.Position.Y, Is.EqualTo(10f).Within(0.01f)); Assert.That(circle.Position.Y, Is.EqualTo(192f).Within(0.01f));
}); });
return true; return true;
}); });
}
AddStep("tap slider", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "Slider"))); [Test]
public void TestTouchInputPlaceCircleAfterTouchingComposeArea()
{
AddStep("tap circle", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "HitCircle")));
// this input is just for interacting with compose area
AddStep("tap playfield", () => tap(this.ChildrenOfType<Playfield>().Single())); AddStep("tap playfield", () => tap(this.ChildrenOfType<Playfield>().Single()));
AddAssert("circle placed", () => EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate) is HitCircle);
AddStep("move current time", () => InputManager.Key(Key.Right)); AddStep("move forward", () => InputManager.Key(Key.Right));
AddStep("tap to place circle", () => tap(this.ChildrenOfType<Playfield>().Single()));
AddAssert("circle placed correctly", () =>
{
var circle = (HitCircle)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate);
Assert.Multiple(() =>
{
Assert.That(circle.Position.X, Is.EqualTo(256f).Within(0.01f));
Assert.That(circle.Position.Y, Is.EqualTo(192f).Within(0.01f));
});
return true;
});
}
[Test]
public void TestTouchInputPlaceSliderDirectly()
{
AddStep("tap slider", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "Slider")));
AddStep("hold to draw slider", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(50, 20))))); AddStep("hold to draw slider", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(50, 20)))));
AddStep("drag to draw", () => InputManager.MoveTouchTo(new Touch(TouchSource.Touch1, this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(200, 50))))); AddStep("drag to draw", () => InputManager.MoveTouchTo(new Touch(TouchSource.Touch1, this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(200, 50)))));
AddAssert("selection not initiated", () => this.ChildrenOfType<DragBox>().All(d => d.State == Visibility.Hidden)); AddAssert("selection not initiated", () => this.ChildrenOfType<DragBox>().All(d => d.State == Visibility.Hidden));
AddAssert("blueprint visible", () => this.ChildrenOfType<SliderPlacementBlueprint>().Single().Alpha > 0);
AddStep("end", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value))); AddStep("end", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value)));
AddAssert("slider placed correctly", () => AddAssert("slider placed correctly", () =>
{ {
@ -76,12 +96,55 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
}); });
} }
private void tap(Drawable drawable) => tap(drawable.ScreenSpaceDrawQuad.Centre); [Test]
public void TestTouchInputPlaceSliderAfterTouchingComposeArea()
{
AddStep("tap slider", () => tap(this.ChildrenOfType<EditorRadioButton>().Single(b => b.Button.Label == "Slider")));
AddStep("tap playfield", () => tap(this.ChildrenOfType<Playfield>().Single()));
AddStep("tap and hold another spot", () => hold(this.ChildrenOfType<Playfield>().Single(), new Vector2(50, 0)));
AddUntilStep("wait for slider placement", () => EditorBeatmap.HitObjects.SingleOrDefault(h => h.StartTime == EditorClock.CurrentTimeAccurate) is Slider);
AddStep("end", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value)));
AddStep("move forward", () => InputManager.Key(Key.Right));
AddStep("hold to draw slider", () => InputManager.BeginTouch(new Touch(TouchSource.Touch1, this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(50, 20)))));
AddStep("drag to draw", () => InputManager.MoveTouchTo(new Touch(TouchSource.Touch1, this.ChildrenOfType<Playfield>().Single().ToScreenSpace(new Vector2(200, 50)))));
AddAssert("selection not initiated", () => this.ChildrenOfType<DragBox>().All(d => d.State == Visibility.Hidden));
AddAssert("blueprint visible", () => this.ChildrenOfType<SliderPlacementBlueprint>().Single().IsPresent);
AddStep("end", () => InputManager.EndTouch(new Touch(TouchSource.Touch1, InputManager.CurrentState.Touch.GetTouchPosition(TouchSource.Touch1)!.Value)));
AddAssert("slider placed correctly", () =>
{
var slider = (Slider)EditorBeatmap.HitObjects.Single(h => h.StartTime == EditorClock.CurrentTimeAccurate);
Assert.Multiple(() =>
{
Assert.That(slider.Position.X, Is.EqualTo(50f).Within(0.01f));
Assert.That(slider.Position.Y, Is.EqualTo(20f).Within(0.01f));
Assert.That(slider.Path.ControlPoints.Count, Is.EqualTo(2));
Assert.That(slider.Path.ControlPoints[0].Position, Is.EqualTo(Vector2.Zero));
// the final position may be slightly off from the mouse position when drawing, account for that.
Assert.That(slider.Path.ControlPoints[1].Position.X, Is.EqualTo(150).Within(5));
Assert.That(slider.Path.ControlPoints[1].Position.Y, Is.EqualTo(30).Within(5));
});
return true;
});
}
private void tap(Drawable drawable, Vector2 offset = default) => tap(drawable.ToScreenSpace(drawable.LayoutRectangle.Centre + offset));
private void tap(Vector2 position) private void tap(Vector2 position)
{ {
InputManager.BeginTouch(new Touch(TouchSource.Touch1, position)); hold(position);
InputManager.EndTouch(new Touch(TouchSource.Touch1, position)); InputManager.EndTouch(new Touch(TouchSource.Touch1, position));
} }
private void hold(Drawable drawable, Vector2 offset = default) => hold(drawable.ToScreenSpace(drawable.LayoutRectangle.Centre + offset));
private void hold(Vector2 position)
{
InputManager.BeginTouch(new Touch(TouchSource.Touch1, position));
}
} }
} }

View File

@ -3,6 +3,7 @@
using System; using System;
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -33,6 +34,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
var osuLastObj = (OsuDifficultyHitObject)current.Previous(0); var osuLastObj = (OsuDifficultyHitObject)current.Previous(0);
var osuLastLastObj = (OsuDifficultyHitObject)current.Previous(1); var osuLastLastObj = (OsuDifficultyHitObject)current.Previous(1);
const int radius = OsuDifficultyHitObject.NORMALISED_RADIUS;
const int diameter = OsuDifficultyHitObject.NORMALISED_DIAMETER;
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle. // Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
double currDistance = withSliderTravelDistance ? osuCurrObj.LazyJumpFromEndDistance : osuCurrObj.JumpDistance; double currDistance = withSliderTravelDistance ? osuCurrObj.LazyJumpFromEndDistance : osuCurrObj.JumpDistance;
double currVelocity = currDistance / osuCurrObj.StrainTime; double currVelocity = currDistance / osuCurrObj.StrainTime;
@ -79,14 +83,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
wideAngleBonus = calcWideAngleBonus(currAngle); wideAngleBonus = calcWideAngleBonus(currAngle);
acuteAngleBonus = calcAcuteAngleBonus(currAngle); acuteAngleBonus = calcAcuteAngleBonus(currAngle);
if (osuCurrObj.StrainTime > 100) // Only buff deltaTime exceeding 300 bpm 1/2. if (DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2) < 300) // Only buff deltaTime exceeding 300 bpm 1/2.
acuteAngleBonus = 0; acuteAngleBonus = 0;
else else
{ {
acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern. acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern.
* Math.Min(angleBonus, 125 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime * Math.Min(angleBonus, diameter * 1.25 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4 * Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4
* Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(currDistance, 50, 100) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter). * Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(currDistance, radius, diameter) - radius) / radius), 2); // Buff distance exceeding radius up to diameter.
} }
// Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute. // Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.
@ -109,7 +113,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
double distRatio = Math.Pow(Math.Sin(Math.PI / 2 * Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity)), 2); double distRatio = Math.Pow(Math.Sin(Math.PI / 2 * Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity)), 2);
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing. // Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
double overlapVelocityBuff = Math.Min(125 / Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Abs(prevVelocity - currVelocity)); double overlapVelocityBuff = Math.Min(diameter * 1.25 / Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Abs(prevVelocity - currVelocity));
velocityChangeBonus = overlapVelocityBuff * distRatio; velocityChangeBonus = overlapVelocityBuff * distRatio;

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -120,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
islandCount.Count++; islandCount.Count++;
// repeated island (ex: triplet -> triplet) // repeated island (ex: triplet -> triplet)
double power = logistic(island.Delta, 2.75, 0.24, 14); double power = DifficultyCalculationUtils.Logistic(island.Delta, maxValue: 2.75, multiplier: 0.24, midpointOffset: 58.33);
effectiveRatio *= Math.Min(3.0 / islandCount.Count, Math.Pow(1.0 / islandCount.Count, power)); effectiveRatio *= Math.Min(3.0 / islandCount.Count, Math.Pow(1.0 / islandCount.Count, power));
islandCounts[countIndex] = (islandCount.Island, islandCount.Count); islandCounts[countIndex] = (islandCount.Island, islandCount.Count);
@ -172,8 +173,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_overall_multiplier) / 2.0; // produces multiplier that can be applied to strain. range [1, infinity) (not really though) return Math.Sqrt(4 + rhythmComplexitySum * rhythm_overall_multiplier) / 2.0; // produces multiplier that can be applied to strain. range [1, infinity) (not really though)
} }
private static double logistic(double x, double maxValue, double multiplier, double offset) => (maxValue / (1 + Math.Pow(Math.E, offset - (multiplier * x))));
private class Island : IEquatable<Island> private class Island : IEquatable<Island>
{ {
private readonly double deltaDifferenceEpsilon; private readonly double deltaDifferenceEpsilon;

View File

@ -3,6 +3,7 @@
using System; using System;
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -10,8 +11,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
{ {
public static class SpeedEvaluator public static class SpeedEvaluator
{ {
private const double single_spacing_threshold = 125; // 1.25 circles distance between centers private const double single_spacing_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers
private const double min_speed_bonus = 75; // ~200BPM private const double min_speed_bonus = 200; // 200 BPM 1/4th
private const double speed_balancing_factor = 40; private const double speed_balancing_factor = 40;
private const double distance_multiplier = 0.94; private const double distance_multiplier = 0.94;
@ -43,8 +44,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
double speedBonus = 0.0; double speedBonus = 0.0;
// Add additional scaling bonus for streams/bursts higher than 200bpm // Add additional scaling bonus for streams/bursts higher than 200bpm
if (strainTime < min_speed_bonus) if (DifficultyCalculationUtils.MillisecondsToBPM(strainTime) > min_speed_bonus)
speedBonus = 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2); speedBonus = 0.75 * Math.Pow((DifficultyCalculationUtils.BPMToMilliseconds(min_speed_bonus) - strainTime) / speed_balancing_factor, 2);
double travelDistance = osuPrevObj?.TravelDistance ?? 0; double travelDistance = osuPrevObj?.TravelDistance ?? 0;
double distance = travelDistance + osuCurrObj.MinimumJumpDistance; double distance = travelDistance + osuCurrObj.MinimumJumpDistance;

View File

@ -48,8 +48,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double sliderFactor = aimRating > 0 ? Math.Min(aimRating, aimRatingNoSliders) / aimRating : 1; double sliderFactor = aimRating > 0 ? Math.Min(aimRating, aimRatingNoSliders) / aimRating : 1;
double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountDifficultStrains(); double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountTopWeightedStrains();
double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountDifficultStrains(); double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountTopWeightedStrains();
if (mods.Any(m => m is OsuModTouchDevice)) if (mods.Any(m => m is OsuModTouchDevice))
{ {

View File

@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
/// </summary> /// </summary>
public const int NORMALISED_RADIUS = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths. public const int NORMALISED_RADIUS = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
public const int NORMALISED_DIAMETER = NORMALISED_RADIUS * 2;
public const int MIN_DELTA_TIME = 25; public const int MIN_DELTA_TIME = 25;
private const float maximum_slider_radius = NORMALISED_RADIUS * 2.4f; private const float maximum_slider_radius = NORMALISED_RADIUS * 2.4f;

View File

@ -34,7 +34,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{ {
currentStrain *= strainDecay(current.DeltaTime); currentStrain *= strainDecay(current.DeltaTime);
currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier; currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier;
ObjectStrains.Add(currentStrain);
return currentStrain; return currentStrain;
} }

View File

@ -23,7 +23,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
/// </summary> /// </summary>
protected virtual double ReducedStrainBaseline => 0.75; protected virtual double ReducedStrainBaseline => 0.75;
protected List<double> ObjectStrains = new List<double>();
protected double Difficulty; protected double Difficulty;
protected OsuStrainSkill(Mod[] mods) protected OsuStrainSkill(Mod[] mods)
@ -60,20 +59,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
return Difficulty; return Difficulty;
} }
/// <summary>
/// Returns the number of strains weighted against the top strain.
/// The result is scaled by clock rate as it affects the total number of strains.
/// </summary>
public double CountDifficultStrains()
{
if (Difficulty == 0)
return 0.0;
double consistentTopStrain = Difficulty / 10; // What would the top strain be if all strain values were identical
// Use a weighted sum of all strains. Constants are arbitrary and give nice values
return ObjectStrains.Sum(s => 1.1 / (1 + Math.Exp(-10 * (s / consistentTopStrain - 0.88))));
}
public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0; public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0;
} }
} }

View File

@ -40,7 +40,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current); currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);
double totalStrain = currentStrain * currentRhythm; double totalStrain = currentStrain * currentRhythm;
ObjectStrains.Add(totalStrain);
return totalStrain; return totalStrain;
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osuTK; using osuTK;
@ -31,12 +32,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
public override void EndPlacement(bool commit) public override void EndPlacement(bool commit)
{ {
if (!commit && PlacementActive != PlacementState.Finished) if (!commit && PlacementActive != PlacementState.Finished)
{ resetGridState();
gridToolboxGroup.StartPosition.Value = originalOrigin;
gridToolboxGroup.Spacing.Value = originalSpacing;
if (!gridToolboxGroup.GridLinesRotation.Disabled)
gridToolboxGroup.GridLinesRotation.Value = originalRotation;
}
base.EndPlacement(commit); base.EndPlacement(commit);
@ -103,6 +99,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
public override void UpdateTimeAndPosition(SnapResult result) public override void UpdateTimeAndPosition(SnapResult result)
{ {
if (State.Value == Visibility.Hidden)
return;
var pos = ToLocalSpace(result.ScreenSpacePosition); var pos = ToLocalSpace(result.ScreenSpacePosition);
if (PlacementActive != PlacementState.Active) if (PlacementActive != PlacementState.Active)
@ -122,5 +121,19 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
} }
} }
} }
protected override void PopOut()
{
base.PopOut();
resetGridState();
}
private void resetGridState()
{
gridToolboxGroup.StartPosition.Value = originalOrigin;
gridToolboxGroup.Spacing.Value = originalSpacing;
if (!gridToolboxGroup.GridLinesRotation.Disabled)
gridToolboxGroup.GridLinesRotation.Value = originalRotation;
}
} }
} }

View File

@ -156,6 +156,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
return true; return true;
} }
// this allows sliders to be drawn outside compose area (after starting from a point within the compose area).
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || PlacementActive == PlacementState.Active;
// ReceivePositionalInputAtSubTree generally always returns true when masking is disabled, but we don't want that,
// otherwise a slider path tooltip will be displayed anywhere in the editor (outside compose area).
protected override bool ReceivePositionalInputAtSubTree(Vector2 screenSpacePos) => ReceivePositionalInputAt(screenSpacePos);
private void beginNewSegment(PathControlPoint lastPoint) private void beginNewSegment(PathControlPoint lastPoint)
{ {
segmentStart = lastPoint; segmentStart = lastPoint;

View File

@ -3,6 +3,7 @@
using System; using System;
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data;
@ -11,26 +12,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
{ {
public class ColourEvaluator public class ColourEvaluator
{ {
/// <summary>
/// A sigmoid function. It gives a value between (middle - height/2) and (middle + height/2).
/// </summary>
/// <param name="val">The input value.</param>
/// <param name="center">The center of the sigmoid, where the largest gradient occurs and value is equal to middle.</param>
/// <param name="width">The radius of the sigmoid, outside of which values are near the minimum/maximum.</param>
/// <param name="middle">The middle of the sigmoid output.</param>
/// <param name="height">The height of the sigmoid output. This will be equal to max value - min value.</param>
private static double sigmoid(double val, double center, double width, double middle, double height)
{
double sigmoid = Math.Tanh(Math.E * -(val - center) / width);
return sigmoid * (height / 2) + middle;
}
/// <summary> /// <summary>
/// Evaluate the difficulty of the first note of a <see cref="MonoStreak"/>. /// Evaluate the difficulty of the first note of a <see cref="MonoStreak"/>.
/// </summary> /// </summary>
public static double EvaluateDifficultyOf(MonoStreak monoStreak) public static double EvaluateDifficultyOf(MonoStreak monoStreak)
{ {
return sigmoid(monoStreak.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5; return DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5;
} }
/// <summary> /// <summary>
@ -38,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
/// </summary> /// </summary>
public static double EvaluateDifficultyOf(AlternatingMonoPattern alternatingMonoPattern) public static double EvaluateDifficultyOf(AlternatingMonoPattern alternatingMonoPattern)
{ {
return sigmoid(alternatingMonoPattern.Index, 2, 2, 0.5, 1) * EvaluateDifficultyOf(alternatingMonoPattern.Parent); return DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * EvaluateDifficultyOf(alternatingMonoPattern.Parent);
} }
/// <summary> /// <summary>
@ -46,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
/// </summary> /// </summary>
public static double EvaluateDifficultyOf(RepeatingHitPatterns repeatingHitPattern) public static double EvaluateDifficultyOf(RepeatingHitPatterns repeatingHitPattern)
{ {
return 2 * (1 - sigmoid(repeatingHitPattern.RepetitionInterval, 2, 2, 0.5, 1)); return 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E));
} }
public static double EvaluateDifficultyOf(DifficultyHitObject hitObject) public static double EvaluateDifficultyOf(DifficultyHitObject hitObject)

View File

@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge
Add(metadataClient); Add(metadataClient);
// add button to observe for daily challenge changes and perform its logic. // add button to observe for daily challenge changes and perform its logic.
Add(new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D)); Add(new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.D));
} }
[Test] [Test]

View File

@ -19,6 +19,7 @@ using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Edit; using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.RadioButtons; using osu.Game.Screens.Edit.Components.RadioButtons;
using osu.Game.Screens.Edit.Components.TernaryButtons; using osu.Game.Screens.Edit.Components.TernaryButtons;
@ -82,6 +83,45 @@ namespace osu.Game.Tests.Visual.Editing
}); });
} }
[Test]
public void TestPlacementOutsideComposeScreen()
{
AddStep("clear all control points and hitobjects", () =>
{
editorBeatmap.ControlPointInfo.Clear();
editorBeatmap.Clear();
});
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
AddStep("select circle", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "HitCircle").TriggerClick());
AddStep("move mouse to compose", () => InputManager.MoveMouseTo(hitObjectComposer.ChildrenOfType<HitObjectContainer>().Single()));
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("circle placed", () => editorBeatmap.HitObjects.Count == 1);
AddStep("move mouse outside compose", () => InputManager.MoveMouseTo(hitObjectComposer.ChildrenOfType<HitObjectContainer>().Single().ScreenSpaceDrawQuad.TopLeft - new Vector2(0f, 20f)));
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddAssert("no circle placed", () => editorBeatmap.HitObjects.Count == 1);
}
[Test]
public void TestDragSliderOutsideComposeScreen()
{
AddStep("clear all control points and hitobjects", () =>
{
editorBeatmap.ControlPointInfo.Clear();
editorBeatmap.Clear();
});
AddStep("Add timing point", () => editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint()));
AddStep("select slider", () => hitObjectComposer.ChildrenOfType<EditorRadioButton>().First(d => d.Button.Label == "Slider").TriggerClick());
AddStep("move mouse to compose", () => InputManager.MoveMouseTo(hitObjectComposer.ChildrenOfType<HitObjectContainer>().Single()));
AddStep("hold", () => InputManager.PressButton(MouseButton.Left));
AddStep("move mouse outside compose", () => InputManager.MoveMouseTo(hitObjectComposer.ChildrenOfType<HitObjectContainer>().Single().ScreenSpaceDrawQuad.TopLeft - new Vector2(0f, 80f)));
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("slider placed", () => editorBeatmap.HitObjects.Count == 1);
}
[Test] [Test]
public void TestPlacementOnlyWorksWithTiming() public void TestPlacementOnlyWorksWithTiming()
{ {

View File

@ -55,21 +55,21 @@ namespace osu.Game.Tests.Visual.Online
{ {
Username = @"flyte", Username = @"flyte",
Id = 3103765, Id = 3103765,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
}), }),
new UserBrickPanel(new APIUser new UserBrickPanel(new APIUser
{ {
Username = @"peppy", Username = @"peppy",
Id = 2, Id = 2,
Colour = "99EB47", Colour = "99EB47",
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
}), }),
new UserGridPanel(new APIUser new UserGridPanel(new APIUser
{ {
Username = @"flyte", Username = @"flyte",
Id = 3103765, Id = 3103765,
CountryCode = CountryCode.JP, CountryCode = CountryCode.JP,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
Status = { Value = UserStatus.Online } Status = { Value = UserStatus.Online }
}) { Width = 300 }, }) { Width = 300 },
boundPanel1 = new UserGridPanel(new APIUser boundPanel1 = new UserGridPanel(new APIUser
@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Online
Username = @"peppy", Username = @"peppy",
Id = 2, Id = 2,
CountryCode = CountryCode.AU, CountryCode = CountryCode.AU,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
IsSupporter = true, IsSupporter = true,
SupportLevel = 3, SupportLevel = 3,
}) { Width = 300 }, }) { Width = 300 },
@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.Online
Username = @"flyte", Username = @"flyte",
Id = 3103765, Id = 3103765,
CountryCode = CountryCode.JP, CountryCode = CountryCode.JP,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg", CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
Statistics = new UserStatistics { GlobalRank = 12345, CountryRank = 1234 } Statistics = new UserStatistics { GlobalRank = 12345, CountryRank = 1234 }
}) { Width = 300 }, }) { Width = 300 },
new UserRankPanel(new APIUser new UserRankPanel(new APIUser
@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 2, Id = 2,
Colour = "99EB47", Colour = "99EB47",
CountryCode = CountryCode.AU, CountryCode = CountryCode.AU,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
Statistics = new UserStatistics { GlobalRank = null, CountryRank = null } Statistics = new UserStatistics { GlobalRank = null, CountryRank = null }
}) { Width = 300 } }) { Width = 300 }
} }
@ -168,6 +168,14 @@ namespace osu.Game.Tests.Visual.Online
CountryRank = RNG.Next(100000) CountryRank = RNG.Next(100000)
}); });
}); });
AddStep("set statistics to something big", () =>
{
API.UpdateStatistics(new UserStatistics
{
GlobalRank = RNG.Next(1_000_000, 100_000_000),
CountryRank = RNG.Next(1_000_000, 100_000_000)
});
});
AddStep("set statistics to empty", () => AddStep("set statistics to empty", () =>
{ {
API.UpdateStatistics(new UserStatistics()); API.UpdateStatistics(new UserStatistics());

View File

@ -110,7 +110,7 @@ namespace osu.Game.Tests.Visual.UserInterface
break; break;
case Key.Q: case Key.Q:
buttons.OnExit = action; buttons.OnExit = _ => action();
break; break;
case Key.O: case Key.O:

View File

@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestStandardButton() public void TestStandardButton()
{ {
AddStep("add button", () => Child = new MainMenuButton( AddStep("add button", () => Child = new MainMenuButton(
ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), _ => { }, 0, Key.P) ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.P)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Origin = Anchor.Centre, Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
CachedDependencies = [(typeof(INotificationOverlay), notificationOverlay)], CachedDependencies = [(typeof(INotificationOverlay), notificationOverlay)],
Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D) Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.D)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -161,7 +161,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Origin = Anchor.Centre, Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
CachedDependencies = [(typeof(INotificationOverlay), notificationOverlay)], CachedDependencies = [(typeof(INotificationOverlay), notificationOverlay)],
Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D) Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), (_, _) => { }, 0, Key.D)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -26,10 +26,10 @@ namespace osu.Game.Rulesets.Difficulty.Skills
protected virtual int SectionLength => 400; protected virtual int SectionLength => 400;
private double currentSectionPeak; // We also keep track of the peak strain level in the current section. private double currentSectionPeak; // We also keep track of the peak strain level in the current section.
private double currentSectionEnd; private double currentSectionEnd;
private readonly List<double> strainPeaks = new List<double>(); private readonly List<double> strainPeaks = new List<double>();
protected readonly List<double> ObjectStrains = new List<double>(); // Store individual strains
protected StrainSkill(Mod[] mods) protected StrainSkill(Mod[] mods)
: base(mods) : base(mods)
@ -57,7 +57,25 @@ namespace osu.Game.Rulesets.Difficulty.Skills
currentSectionEnd += SectionLength; currentSectionEnd += SectionLength;
} }
currentSectionPeak = Math.Max(StrainValueAt(current), currentSectionPeak); double strain = StrainValueAt(current);
currentSectionPeak = Math.Max(strain, currentSectionPeak);
// Store the strain value for the object
ObjectStrains.Add(strain);
}
/// <summary>
/// Calculates the number of strains weighted against the top strain.
/// The result is scaled by clock rate as it affects the total number of strains.
/// </summary>
public virtual double CountTopWeightedStrains()
{
if (ObjectStrains.Count == 0)
return 0.0;
double consistentTopStrain = DifficultyValue() / 10; // What would the top strain be if all strain values were identical
// Use a weighted sum of all strains. Constants are arbitrary and give nice values
return ObjectStrains.Sum(s => 1.1 / (1 + Math.Exp(-10 * (s / consistentTopStrain - 0.88))));
} }
/// <summary> /// <summary>

View File

@ -0,0 +1,50 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
namespace osu.Game.Rulesets.Difficulty.Utils
{
public static class DifficultyCalculationUtils
{
/// <summary>
/// Converts BPM value into milliseconds
/// </summary>
/// <param name="bpm">Beats per minute</param>
/// <param name="delimiter">Which rhythm delimiter to use, default is 1/4</param>
/// <returns>BPM conveted to milliseconds</returns>
public static double BPMToMilliseconds(double bpm, int delimiter = 4)
{
return 60000.0 / delimiter / bpm;
}
/// <summary>
/// Converts milliseconds value into a BPM value
/// </summary>
/// <param name="ms">Milliseconds</param>
/// <param name="delimiter">Which rhythm delimiter to use, default is 1/4</param>
/// <returns>Milliseconds conveted to beats per minute</returns>
public static double MillisecondsToBPM(double ms, int delimiter = 4)
{
return 60000.0 / (ms * delimiter);
}
/// <summary>
/// Calculates a S-shaped logistic function (https://en.wikipedia.org/wiki/Logistic_function)
/// </summary>
/// <param name="x">Value to calculate the function for</param>
/// <param name="maxValue">Maximum value returnable by the function</param>
/// <param name="multiplier">Growth rate of the function</param>
/// <param name="midpointOffset">How much the function midpoint is offset from zero <paramref name="x"/></param>
/// <returns>The output of logistic function of <paramref name="x"/></returns>
public static double Logistic(double x, double midpointOffset, double multiplier, double maxValue = 1) => maxValue / (1 + Math.Exp(multiplier * (midpointOffset - x)));
/// <summary>
/// Calculates a S-shaped logistic function (https://en.wikipedia.org/wiki/Logistic_function)
/// </summary>
/// <param name="maxValue">Maximum value returnable by the function</param>
/// <param name="exponent">Exponent</param>
/// <returns>The output of logistic function</returns>
public static double Logistic(double exponent, double maxValue = 1) => maxValue / (1 + Math.Exp(exponent));
}
}

View File

@ -537,22 +537,23 @@ namespace osu.Game.Rulesets.Edit
#region IPlacementHandler #region IPlacementHandler
public void BeginPlacement(HitObject hitObject) public void ShowPlacement(HitObject hitObject)
{ {
EditorBeatmap.PlacementObject.Value = hitObject; EditorBeatmap.PlacementObject.Value = hitObject;
} }
public void EndPlacement(HitObject hitObject, bool commit) public void HidePlacement()
{ {
EditorBeatmap.PlacementObject.Value = null; EditorBeatmap.PlacementObject.Value = null;
}
if (commit) public void CommitPlacement(HitObject hitObject)
{ {
EditorBeatmap.Add(hitObject); EditorBeatmap.PlacementObject.Value = null;
EditorBeatmap.Add(hitObject);
if (autoSeekOnPlacement.Value && EditorClock.CurrentTime < hitObject.StartTime) if (autoSeekOnPlacement.Value && EditorClock.CurrentTime < hitObject.StartTime)
EditorClock.SeekSmoothlyTo(hitObject.StartTime); EditorClock.SeekSmoothlyTo(hitObject.StartTime);
}
} }
public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject); public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject);

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -63,18 +64,26 @@ namespace osu.Game.Rulesets.Edit
startTimeBindable.BindValueChanged(_ => ApplyDefaultsToHitObject(), true); startTimeBindable.BindValueChanged(_ => ApplyDefaultsToHitObject(), true);
} }
private bool placementBegun;
protected override void BeginPlacement(bool commitStart = false) protected override void BeginPlacement(bool commitStart = false)
{ {
base.BeginPlacement(commitStart); base.BeginPlacement(commitStart);
placementHandler.BeginPlacement(HitObject); if (State.Value == Visibility.Visible)
placementHandler.ShowPlacement(HitObject);
placementBegun = true;
} }
public override void EndPlacement(bool commit) public override void EndPlacement(bool commit)
{ {
base.EndPlacement(commit); base.EndPlacement(commit);
placementHandler.EndPlacement(HitObject, IsValidForPlacement && commit); if (IsValidForPlacement && commit)
placementHandler.CommitPlacement(HitObject);
else
placementHandler.HidePlacement();
} }
/// <summary> /// <summary>
@ -127,5 +136,19 @@ namespace osu.Game.Rulesets.Edit
/// refreshing <see cref="Objects.HitObject.NestedHitObjects"/> and parameters for the <see cref="HitObject"/>. /// refreshing <see cref="Objects.HitObject.NestedHitObjects"/> and parameters for the <see cref="HitObject"/>.
/// </summary> /// </summary>
protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty); protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
protected override void PopIn()
{
base.PopIn();
if (placementBegun)
placementHandler.ShowPlacement(HitObject);
}
protected override void PopOut()
{
base.PopOut();
placementHandler.HidePlacement();
}
} }
} }

View File

@ -7,7 +7,6 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osuTK;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Rulesets.Edit namespace osu.Game.Rulesets.Edit
@ -15,7 +14,7 @@ namespace osu.Game.Rulesets.Edit
/// <summary> /// <summary>
/// A blueprint which governs the placement of something. /// A blueprint which governs the placement of something.
/// </summary> /// </summary>
public abstract partial class PlacementBlueprint : CompositeDrawable, IKeyBindingHandler<GlobalAction> public abstract partial class PlacementBlueprint : VisibilityContainer, IKeyBindingHandler<GlobalAction>
{ {
/// <summary> /// <summary>
/// Whether the <see cref="HitObject"/> is currently mid-placement, but has not necessarily finished being placed. /// Whether the <see cref="HitObject"/> is currently mid-placement, but has not necessarily finished being placed.
@ -31,12 +30,17 @@ namespace osu.Game.Rulesets.Edit
/// </remarks> /// </remarks>
protected virtual bool IsValidForPlacement => true; protected virtual bool IsValidForPlacement => true;
// the blueprint should still be considered for input even if it is hidden,
// especially when such input is the reason for making the blueprint become visible.
public override bool PropagatePositionalInputSubTree => true;
public override bool PropagateNonPositionalInputSubTree => true;
protected PlacementBlueprint() protected PlacementBlueprint()
{ {
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
// This is required to allow the blueprint's position to be updated via OnMouseMove/Handle // the blueprint should still be considered for input even if it is hidden,
// on the same frame it is made visible via a PlacementState change. // especially when such input is the reason for making the blueprint become visible.
AlwaysPresent = true; AlwaysPresent = true;
} }
@ -104,8 +108,6 @@ namespace osu.Game.Rulesets.Edit
{ {
} }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
protected override bool Handle(UIEvent e) protected override bool Handle(UIEvent e)
{ {
base.Handle(e); base.Handle(e);
@ -127,6 +129,9 @@ namespace osu.Game.Rulesets.Edit
} }
} }
protected override void PopIn() => this.FadeIn();
protected override void PopOut() => this.FadeOut();
public enum PlacementState public enum PlacementState
{ {
Waiting, Waiting,

View File

@ -316,9 +316,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <summary> /// <summary>
/// Refreshes the current placement tool. /// Refreshes the current placement tool.
/// </summary> /// </summary>
private void refreshTool() private void refreshPlacement()
{ {
removePlacement(); CurrentPlacement?.EndPlacement(false);
CurrentPlacement?.Expire();
CurrentPlacement = null;
ensurePlacementCreated(); ensurePlacementCreated();
} }
@ -344,21 +347,24 @@ namespace osu.Game.Screens.Edit.Compose.Components
{ {
case PlacementBlueprint.PlacementState.Waiting: case PlacementBlueprint.PlacementState.Waiting:
if (!Composer.CursorInPlacementArea) if (!Composer.CursorInPlacementArea)
removePlacement(); CurrentPlacement.Hide();
else
CurrentPlacement.Show();
break;
case PlacementBlueprint.PlacementState.Active:
CurrentPlacement.Show();
break; break;
case PlacementBlueprint.PlacementState.Finished: case PlacementBlueprint.PlacementState.Finished:
removePlacement(); refreshPlacement();
break; break;
} }
}
if (Composer.CursorInPlacementArea) // updates the placement with the latest editor clock time.
ensurePlacementCreated();
// updates the placement with the latest editor clock time.
if (CurrentPlacement != null)
updatePlacementTimeAndPosition(); updatePlacementTimeAndPosition();
}
} }
protected override bool OnMouseMove(MouseMoveEvent e) protected override bool OnMouseMove(MouseMoveEvent e)
@ -386,7 +392,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void hitObjectAdded(HitObject obj) private void hitObjectAdded(HitObject obj)
{ {
// refresh the tool to handle the case of placement completing. // refresh the tool to handle the case of placement completing.
refreshTool(); refreshPlacement();
// on successful placement, the new combo button should be reset as this is the most common user interaction. // on successful placement, the new combo button should be reset as this is the most common user interaction.
if (Beatmap.SelectedHitObjects.Count == 0) if (Beatmap.SelectedHitObjects.Count == 0)
@ -415,14 +421,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
public void CommitIfPlacementActive() public void CommitIfPlacementActive()
{ {
CurrentPlacement?.EndPlacement(CurrentPlacement.PlacementActive == PlacementBlueprint.PlacementState.Active); CurrentPlacement?.EndPlacement(CurrentPlacement.PlacementActive == PlacementBlueprint.PlacementState.Active);
removePlacement(); refreshPlacement();
}
private void removePlacement()
{
CurrentPlacement?.EndPlacement(false);
CurrentPlacement?.Expire();
CurrentPlacement = null;
} }
private CompositionTool currentTool; private CompositionTool currentTool;

View File

@ -10,17 +10,21 @@ namespace osu.Game.Screens.Edit.Compose
public interface IPlacementHandler public interface IPlacementHandler
{ {
/// <summary> /// <summary>
/// Notifies that a placement has begun. /// Notifies that a placement blueprint became visible on the screen.
/// </summary> /// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> being placed.</param> /// <param name="hitObject">The <see cref="HitObject"/> representing the placement.</param>
void BeginPlacement(HitObject hitObject); void ShowPlacement(HitObject hitObject);
/// <summary> /// <summary>
/// Notifies that a placement has finished. /// Notifies that a visible placement blueprint has been hidden.
/// </summary>
void HidePlacement();
/// <summary>
/// Notifies that a placement has been committed.
/// </summary> /// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> that has been placed.</param> /// <param name="hitObject">The <see cref="HitObject"/> that has been placed.</param>
/// <param name="commit">Whether the object should be committed.</param> void CommitPlacement(HitObject hitObject);
void EndPlacement(HitObject hitObject, bool commit);
/// <summary> /// <summary>
/// Deletes a <see cref="HitObject"/>. /// Deletes a <see cref="HitObject"/>.

View File

@ -41,7 +41,7 @@ namespace osu.Game.Screens.Menu
public Action? OnEditBeatmap; public Action? OnEditBeatmap;
public Action? OnEditSkin; public Action? OnEditSkin;
public Action? OnExit; public Action<UIEvent>? OnExit;
public Action? OnBeatmapListing; public Action? OnBeatmapListing;
public Action? OnSolo; public Action? OnSolo;
public Action? OnSettings; public Action? OnSettings;
@ -104,11 +104,11 @@ namespace osu.Game.Screens.Menu
buttonArea.AddRange(new Drawable[] buttonArea.AddRange(new Drawable[]
{ {
new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), _ => OnSettings?.Invoke(), Key.O, Key.S) new MainMenuButton(ButtonSystemStrings.Settings, string.Empty, OsuIcon.Settings, new Color4(85, 85, 85, 255), (_, _) => OnSettings?.Invoke(), Key.O, Key.S)
{ {
Padding = new MarginPadding { Right = WEDGE_WIDTH }, Padding = new MarginPadding { Right = WEDGE_WIDTH },
}, },
backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), _ => State = ButtonSystemState.TopLevel) backButton = new MainMenuButton(ButtonSystemStrings.Back, @"back-to-top", OsuIcon.PrevCircle, new Color4(51, 58, 94, 255), (_, _) => State = ButtonSystemState.TopLevel)
{ {
Padding = new MarginPadding { Right = WEDGE_WIDTH }, Padding = new MarginPadding { Right = WEDGE_WIDTH },
VisibleStateMin = ButtonSystemState.Play, VisibleStateMin = ButtonSystemState.Play,
@ -132,7 +132,7 @@ namespace osu.Game.Screens.Menu
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio, IdleTracker? idleTracker, GameHost host) private void load(AudioManager audio, IdleTracker? idleTracker, GameHost host)
{ {
buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), _ => OnSolo?.Invoke(), Key.P) buttonsPlay.Add(new MainMenuButton(ButtonSystemStrings.Solo, @"button-default-select", OsuIcon.Player, new Color4(102, 68, 204, 255), (_, _) => OnSolo?.Invoke(), Key.P)
{ {
Padding = new MarginPadding { Left = WEDGE_WIDTH }, Padding = new MarginPadding { Left = WEDGE_WIDTH },
}); });
@ -141,22 +141,22 @@ namespace osu.Game.Screens.Menu
buttonsPlay.Add(new DailyChallengeButton(@"button-daily-select", new Color4(94, 63, 186, 255), onDailyChallenge, Key.D)); buttonsPlay.Add(new DailyChallengeButton(@"button-daily-select", new Color4(94, 63, 186, 255), onDailyChallenge, Key.D));
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play);
buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), _ => OnEditBeatmap?.Invoke(), Key.B, Key.E) buttonsEdit.Add(new MainMenuButton(EditorStrings.BeatmapEditor.ToLower(), @"button-default-select", OsuIcon.Beatmap, new Color4(238, 170, 0, 255), (_, _) => OnEditBeatmap?.Invoke(), Key.B, Key.E)
{ {
Padding = new MarginPadding { Left = WEDGE_WIDTH }, Padding = new MarginPadding { Left = WEDGE_WIDTH },
}); });
buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), _ => OnEditSkin?.Invoke(), Key.S)); buttonsEdit.Add(new MainMenuButton(SkinEditorStrings.SkinEditor.ToLower(), @"button-default-select", OsuIcon.SkinB, new Color4(220, 160, 0, 255), (_, _) => OnEditSkin?.Invoke(), Key.S));
buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit); buttonsEdit.ForEach(b => b.VisibleState = ButtonSystemState.Edit);
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), _ => State = ButtonSystemState.Play, Key.P, Key.M, Key.L) buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Play, @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), (_, _) => State = ButtonSystemState.Play, Key.P, Key.M, Key.L)
{ {
Padding = new MarginPadding { Left = WEDGE_WIDTH }, Padding = new MarginPadding { Left = WEDGE_WIDTH },
}); });
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), _ => State = ButtonSystemState.Edit, Key.E)); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Edit, @"button-play-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), (_, _) => State = ButtonSystemState.Edit, Key.E));
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), _ => OnBeatmapListing?.Invoke(), Key.B, Key.D)); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Browse, @"button-default-select", OsuIcon.Beatmap, new Color4(165, 204, 0, 255), (_, _) => OnBeatmapListing?.Invoke(), Key.B, Key.D));
if (host.CanExit) if (host.CanExit)
buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), _ => OnExit?.Invoke(), Key.Q)); buttonsTopLevel.Add(new MainMenuButton(ButtonSystemStrings.Exit, string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), (_, e) => OnExit?.Invoke(e), Key.Q));
buttonArea.AddRange(buttonsPlay); buttonArea.AddRange(buttonsPlay);
buttonArea.AddRange(buttonsEdit); buttonArea.AddRange(buttonsEdit);
@ -179,7 +179,7 @@ namespace osu.Game.Screens.Menu
sampleLogoSwoosh = audio.Samples.Get(@"Menu/osu-logo-swoosh"); sampleLogoSwoosh = audio.Samples.Get(@"Menu/osu-logo-swoosh");
} }
private void onMultiplayer(MainMenuButton _) private void onMultiplayer(MainMenuButton mainMenuButton, UIEvent uiEvent)
{ {
if (api.State.Value != APIState.Online) if (api.State.Value != APIState.Online)
{ {
@ -190,7 +190,7 @@ namespace osu.Game.Screens.Menu
OnMultiplayer?.Invoke(); OnMultiplayer?.Invoke();
} }
private void onPlaylists(MainMenuButton _) private void onPlaylists(MainMenuButton mainMenuButton, UIEvent uiEvent)
{ {
if (api.State.Value != APIState.Online) if (api.State.Value != APIState.Online)
{ {
@ -201,7 +201,7 @@ namespace osu.Game.Screens.Menu
OnPlaylists?.Invoke(); OnPlaylists?.Invoke();
} }
private void onDailyChallenge(MainMenuButton button) private void onDailyChallenge(MainMenuButton button, UIEvent uiEvent)
{ {
if (api.State.Value != APIState.Online) if (api.State.Value != APIState.Online)
{ {

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
@ -50,7 +51,7 @@ namespace osu.Game.Screens.Menu
[Resolved] [Resolved]
private SessionStatics statics { get; set; } = null!; private SessionStatics statics { get; set; } = null!;
public DailyChallengeButton(string sampleName, Color4 colour, Action<MainMenuButton>? clickAction = null, params Key[] triggerKeys) public DailyChallengeButton(string sampleName, Color4 colour, Action<MainMenuButton, UIEvent>? clickAction = null, params Key[] triggerKeys)
: base(ButtonSystemStrings.DailyChallenge, sampleName, OsuIcon.DailyChallenge, colour, clickAction, triggerKeys) : base(ButtonSystemStrings.DailyChallenge, sampleName, OsuIcon.DailyChallenge, colour, clickAction, triggerKeys)
{ {
BaseSize = new Vector2(ButtonSystem.BUTTON_WIDTH * 1.3f, ButtonArea.BUTTON_AREA_HEIGHT); BaseSize = new Vector2(ButtonSystem.BUTTON_WIDTH * 1.3f, ButtonArea.BUTTON_AREA_HEIGHT);

View File

@ -153,9 +153,9 @@ namespace osu.Game.Screens.Menu
else else
this.Push(new DailyChallengeIntro(room)); this.Push(new DailyChallengeIntro(room));
}, },
OnExit = () => OnExit = e =>
{ {
exitConfirmedViaHoldOrClick = true; exitConfirmedViaHoldOrClick = e is MouseEvent;
this.Exit(); this.Exit();
} }
} }

View File

@ -64,7 +64,7 @@ namespace osu.Game.Screens.Menu
protected Vector2 BaseSize { get; init; } = new Vector2(ButtonSystem.BUTTON_WIDTH, ButtonArea.BUTTON_AREA_HEIGHT); protected Vector2 BaseSize { get; init; } = new Vector2(ButtonSystem.BUTTON_WIDTH, ButtonArea.BUTTON_AREA_HEIGHT);
private readonly Action<MainMenuButton>? clickAction; private readonly Action<MainMenuButton, UIEvent>? clickAction;
private readonly Container background; private readonly Container background;
private readonly Drawable backgroundContent; private readonly Drawable backgroundContent;
@ -84,7 +84,7 @@ namespace osu.Game.Screens.Menu
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => background.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => background.ReceivePositionalInputAt(screenSpacePos);
public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action<MainMenuButton>? clickAction = null, params Key[] triggerKeys) public MainMenuButton(LocalisableString text, string sampleName, IconUsage symbol, Color4 colour, Action<MainMenuButton, UIEvent>? clickAction = null, params Key[] triggerKeys)
{ {
this.sampleName = sampleName; this.sampleName = sampleName;
this.clickAction = clickAction; this.clickAction = clickAction;
@ -263,7 +263,7 @@ namespace osu.Game.Screens.Menu
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
{ {
trigger(); trigger(e);
return true; return true;
} }
@ -274,19 +274,19 @@ namespace osu.Game.Screens.Menu
if (TriggerKeys.Contains(e.Key)) if (TriggerKeys.Contains(e.Key))
{ {
trigger(); trigger(e);
return true; return true;
} }
return false; return false;
} }
private void trigger() private void trigger(UIEvent e)
{ {
sampleChannel = sampleClick?.GetChannel(); sampleChannel = sampleClick?.GetChannel();
sampleChannel?.Play(); sampleChannel?.Play();
clickAction?.Invoke(this); clickAction?.Invoke(this, e);
boxHoverLayer.ClearTransforms(); boxHoverLayer.ClearTransforms();
boxHoverLayer.Alpha = 0.9f; boxHoverLayer.Alpha = 0.9f;

View File

@ -224,7 +224,7 @@ namespace osu.Game.Screens.Play
} }
}, },
}, },
idleTracker = new IdleTracker(750), idleTracker = new IdleTracker(1500),
sampleRestart = new SkinnableSound(new SampleInfo(@"Gameplay/restart", @"pause-retry-click")) sampleRestart = new SkinnableSound(new SampleInfo(@"Gameplay/restart", @"pause-retry-click"))
}; };

View File

@ -222,12 +222,6 @@ namespace osu.Game.Screens.Select
InternalChild = new Container InternalChild = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
// Avoid clash between scrollbar and osu! logo.
Top = 10,
Bottom = 100,
},
Children = new Drawable[] Children = new Drawable[]
{ {
setPool, setPool,
@ -1266,6 +1260,38 @@ namespace osu.Game.Screens.Select
return base.OnDragStart(e); return base.OnDragStart(e);
} }
protected override ScrollbarContainer CreateScrollbar(Direction direction)
{
return new PaddedScrollbar();
}
protected partial class PaddedScrollbar : OsuScrollbar
{
public PaddedScrollbar()
: base(Direction.Vertical)
{
}
}
private const float top_padding = 10;
private const float bottom_padding = 70;
protected override float ToScrollbarPosition(float scrollPosition)
{
if (Precision.AlmostEquals(0, ScrollableExtent))
return 0;
return top_padding + (ScrollbarMovementExtent - (top_padding + bottom_padding)) * (scrollPosition / ScrollableExtent);
}
protected override float FromScrollbarPosition(float scrollbarPosition)
{
if (Precision.AlmostEquals(0, ScrollbarMovementExtent))
return 0;
return ScrollableExtent * ((scrollbarPosition - top_padding) / (ScrollbarMovementExtent - (top_padding + bottom_padding)));
}
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

@ -59,20 +59,20 @@ namespace osu.Game.Tests.Visual
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
ResetPlacement(); ResetPlacement();
} }
public void BeginPlacement(HitObject hitObject) public void ShowPlacement(HitObject hitObject)
{ {
} }
public void EndPlacement(HitObject hitObject, bool commit) public void HidePlacement()
{ {
if (commit) }
AddHitObject(CreateHitObject(hitObject));
ResetPlacement(); public void CommitPlacement(HitObject hitObject)
{
AddHitObject(CreateHitObject(hitObject));
} }
protected void ResetPlacement() protected void ResetPlacement()
@ -89,6 +89,10 @@ namespace osu.Game.Tests.Visual
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
if (CurrentBlueprint.PlacementActive == PlacementBlueprint.PlacementState.Finished)
ResetPlacement();
updatePlacementTimeAndPosition(); updatePlacementTimeAndPosition();
} }

View File

@ -152,7 +152,7 @@ namespace osu.Game.Users
Margin = new MarginPadding { Top = main_content_height }, Margin = new MarginPadding { Top = main_content_height },
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Left = 80, Vertical = padding }, Padding = new MarginPadding(padding),
ColumnDimensions = new[] ColumnDimensions = new[]
{ {
new Dimension(), new Dimension(),

View File

@ -20,7 +20,7 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="AutoMapper" Version="13.0.1" /> <PackageReference Include="AutoMapper" Version="13.0.1" />
<PackageReference Include="DiffPlex" Version="1.7.2" /> <PackageReference Include="DiffPlex" Version="1.7.2" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.67" /> <PackageReference Include="HtmlAgilityPack" Version="1.11.70" />
<PackageReference Include="Humanizer" Version="2.14.1" /> <PackageReference Include="Humanizer" Version="2.14.1" />
<PackageReference Include="MessagePack" Version="2.5.187" /> <PackageReference Include="MessagePack" Version="2.5.187" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.10" /> <PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="8.0.10" />
@ -35,9 +35,9 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="11.5.0" /> <PackageReference Include="Realm" Version="11.5.0" />
<PackageReference Include="ppy.osu.Framework" Version="2024.1025.0" /> <PackageReference Include="ppy.osu.Framework" Version="2024.1111.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.1106.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2024.1106.0" />
<PackageReference Include="Sentry" Version="4.12.1" /> <PackageReference Include="Sentry" Version="4.13.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. --> <!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
<PackageReference Include="SharpCompress" Version="0.38.0" /> <PackageReference Include="SharpCompress" Version="0.38.0" />
<PackageReference Include="NUnit" Version="3.14.0" /> <PackageReference Include="NUnit" Version="3.14.0" />