1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-13 21:53:22 +08:00

Merge branch 'master' into fix-storyboard-sprites-2

This commit is contained in:
Salman Ahmed 2024-01-17 18:43:25 +03:00 committed by GitHub
commit 06da7f74de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
138 changed files with 2555 additions and 798 deletions

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.110.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.114.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -30,12 +30,19 @@ namespace osu.Desktop
[STAThread]
public static void Main(string[] args)
{
// NVIDIA profiles are based on the executable name of a process.
// Lazer and stable share the same executable name.
// Stable sets this setting to "Off", which may not be what we want, so let's force it back to the default "Auto" on startup.
NVAPI.ThreadedOptimisations = NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT;
// run Squirrel first, as the app may exit after these run
/*
* WARNING: DO NOT PLACE **ANY** CODE ABOVE THE FOLLOWING BLOCK!
*
* Logic handling Squirrel MUST run before EVERYTHING if you do not want to break it.
* To be more precise: Squirrel is internally using a rather... crude method to determine whether it is running under NUnit,
* namely by checking loaded assemblies:
* https://github.com/clowd/Clowd.Squirrel/blob/24427217482deeeb9f2cacac555525edfc7bd9ac/src/Squirrel/SimpleSplat/PlatformModeDetector.cs#L17-L32
*
* If it finds ANY assembly from the ones listed above - REGARDLESS of the reason why it is loaded -
* the app will then do completely broken things like:
* - not creating system shortcuts (as the logic is if'd out if "running tests")
* - not exiting after the install / first-update / uninstall hooks are ran (as the `Environment.Exit()` calls are if'd out if "running tests")
*/
if (OperatingSystem.IsWindows())
{
var windowsVersion = Environment.OSVersion.Version;
@ -59,6 +66,11 @@ namespace osu.Desktop
setupSquirrel();
}
// NVIDIA profiles are based on the executable name of a process.
// Lazer and stable share the same executable name.
// Stable sets this setting to "Off", which may not be what we want, so let's force it back to the default "Auto" on startup.
NVAPI.ThreadedOptimisations = NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT;
// Back up the cwd before DesktopGameHost changes it
string cwd = Environment.CurrentDirectory;

View File

@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Scoring.Legacy;
@ -62,13 +63,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000;
}
int difficultyPeppyStars = (int)Math.Round(
(baseBeatmap.Difficulty.DrainRate
+ baseBeatmap.Difficulty.OverallDifficulty
+ baseBeatmap.Difficulty.CircleSize
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
scoreMultiplier = difficultyPeppyStars;
scoreMultiplier = LegacyRulesetExtensions.CalculateDifficultyPeppyStars(baseBeatmap.Difficulty, objectCount, drainLength);
LegacyScoreAttributes attributes = new LegacyScoreAttributes();

View File

@ -1,22 +1,23 @@
// 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.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.Mania.UI.Components;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Skinning;
@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.UI
private readonly ColumnFlow<Column> columnFlow;
private readonly JudgementContainer<DrawableManiaJudgement> judgements;
private readonly DrawablePool<DrawableManiaJudgement> judgementPool;
private readonly JudgementPooler<DrawableManiaJudgement> judgementPooler;
private readonly Drawable barLineContainer;
@ -48,6 +49,8 @@ namespace osu.Game.Rulesets.Mania.UI
private readonly int firstColumnIndex;
private ISkinSource currentSkin = null!;
public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction)
{
this.firstColumnIndex = firstColumnIndex;
@ -65,7 +68,6 @@ namespace osu.Game.Rulesets.Mania.UI
InternalChildren = new Drawable[]
{
judgementPool = new DrawablePool<DrawableManiaJudgement>(2),
new Container
{
Anchor = Anchor.TopCentre,
@ -104,7 +106,7 @@ namespace osu.Game.Rulesets.Mania.UI
{
RelativeSizeAxes = Axes.Y,
},
new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.StageForeground), _ => null)
new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.StageForeground))
{
RelativeSizeAxes = Axes.Both
},
@ -137,11 +139,13 @@ namespace osu.Game.Rulesets.Mania.UI
AddNested(column);
}
var hitWindows = new ManiaHitWindows();
AddInternal(judgementPooler = new JudgementPooler<DrawableManiaJudgement>(Enum.GetValues<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r))));
RegisterPool<BarLine, DrawableBarLine>(50, 200);
}
private ISkinSource currentSkin;
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
@ -170,7 +174,7 @@ namespace osu.Game.Rulesets.Mania.UI
base.Dispose(isDisposing);
if (currentSkin != null)
if (currentSkin.IsNotNull())
currentSkin.SourceChanged -= onSkinChanged;
}
@ -196,13 +200,13 @@ namespace osu.Game.Rulesets.Mania.UI
return;
judgements.Clear(false);
judgements.Add(judgementPool.Get(j =>
judgements.Add(judgementPooler.Get(result.Type, j =>
{
j.Apply(result, judgedObject);
j.Anchor = Anchor.Centre;
j.Origin = Anchor.Centre;
}));
})!);
}
protected override void Update()

View File

@ -1,51 +0,0 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Osu.Tests.Editor
{
/// <summary>
/// This test covers autoplay working correctly in the editor on fast streams.
/// Might seem like a weird test, but frame stability being toggled can cause autoplay to operation incorrectly.
/// This is clearly a bug with the autoplay algorithm, but is worked around at an editor level for now.
/// </summary>
public partial class TestSceneEditorAutoplayFastStreams : EditorTestScene
{
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var testBeatmap = new TestBeatmap(ruleset, false);
testBeatmap.HitObjects.AddRange(new[]
{
new HitCircle { StartTime = 500 },
new HitCircle { StartTime = 530 },
new HitCircle { StartTime = 560 },
new HitCircle { StartTime = 590 },
new HitCircle { StartTime = 620 },
});
return testBeatmap;
}
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
[Test]
public void TestAllHit()
{
AddStep("start playback", () => EditorClock.Start());
AddUntilStep("wait for all hit", () =>
{
DrawableHitCircle[] hitCircles = Editor.ChildrenOfType<DrawableHitCircle>().OrderBy(s => s.HitObject.StartTime).ToArray();
return hitCircles.Length == 5 && hitCircles.All(h => h.IsHit);
});
}
}
}

View File

@ -46,12 +46,10 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
moveMouseToObject(() => slider);
AddStep("seek after end", () => EditorClock.Seek(750));
AddUntilStep("wait for seek", () => !EditorClock.IsSeeking);
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("slider not selected", () => EditorBeatmap.SelectedHitObjects.Count == 0);
AddStep("seek to visible", () => EditorClock.Seek(650));
AddUntilStep("wait for seek", () => !EditorClock.IsSeeking);
AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddUntilStep("slider selected", () => EditorBeatmap.SelectedHitObjects.Single() == slider);
}

View File

@ -1,8 +1,18 @@
// 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.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
@ -21,5 +31,129 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
[Test]
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
[Test]
public void TestSliderDimsOnlyAfterStartTime()
{
bool sliderDimmedBeforeStartTime = false;
CreateModTest(new ModTestData
{
Mod = new OsuModFlashlight(),
PassCondition = () =>
{
sliderDimmedBeforeStartTime |=
Player.GameplayClockContainer.CurrentTime < 1000 && Player.ChildrenOfType<ModFlashlight<OsuHitObject>.Flashlight>().Single().FlashlightDim > 0;
return Player.GameplayState.HasPassed && !sliderDimmedBeforeStartTime;
},
Beatmap = new OsuBeatmap
{
HitObjects = new List<OsuHitObject>
{
new HitCircle { StartTime = 0, },
new Slider
{
StartTime = 1000,
Path = new SliderPath(new[]
{
new PathControlPoint(),
new PathControlPoint(new Vector2(100))
})
}
},
BeatmapInfo =
{
StackLeniency = 0,
}
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(0, new Vector2(), OsuAction.LeftButton),
new OsuReplayFrame(990, new Vector2()),
new OsuReplayFrame(1000, new Vector2(), OsuAction.LeftButton),
new OsuReplayFrame(2000, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(2001, new Vector2(100)),
},
Autoplay = false,
});
}
[Test]
public void TestSliderDoesDimAfterStartTimeIfHitEarly()
{
bool sliderDimmed = false;
CreateModTest(new ModTestData
{
Mod = new OsuModFlashlight(),
PassCondition = () =>
{
sliderDimmed |=
Player.GameplayClockContainer.CurrentTime >= 1000 && Player.ChildrenOfType<ModFlashlight<OsuHitObject>.Flashlight>().Single().FlashlightDim > 0;
return Player.GameplayState.HasPassed && sliderDimmed;
},
Beatmap = new OsuBeatmap
{
HitObjects = new List<OsuHitObject>
{
new Slider
{
StartTime = 1000,
Path = new SliderPath(new[]
{
new PathControlPoint(),
new PathControlPoint(new Vector2(100))
})
}
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(990, new Vector2(), OsuAction.LeftButton),
new OsuReplayFrame(2000, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(2001, new Vector2(100)),
},
Autoplay = false,
});
}
[Test]
public void TestSliderDoesDimAfterStartTimeIfHitLate()
{
bool sliderDimmed = false;
CreateModTest(new ModTestData
{
Mod = new OsuModFlashlight(),
PassCondition = () =>
{
sliderDimmed |=
Player.GameplayClockContainer.CurrentTime >= 1000 && Player.ChildrenOfType<ModFlashlight<OsuHitObject>.Flashlight>().Single().FlashlightDim > 0;
return Player.GameplayState.HasPassed && sliderDimmed;
},
Beatmap = new OsuBeatmap
{
HitObjects = new List<OsuHitObject>
{
new Slider
{
StartTime = 1000,
Path = new SliderPath(new[]
{
new PathControlPoint(),
new PathControlPoint(new Vector2(100))
})
}
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(1100, new Vector2(), OsuAction.LeftButton),
new OsuReplayFrame(2000, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(2001, new Vector2(100)),
},
Autoplay = false,
});
}
}
}

View File

@ -51,7 +51,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
public void TestUserAlreadyHasTouchDeviceActive()
{
loadPlayer();
// it is presumed that a previous screen (i.e. song select) will set this up
AddStep("set up touchscreen user", () =>
{
currentPlayer.Score.ScoreInfo.Mods = currentPlayer.Score.ScoreInfo.Mods.Append(new OsuModTouchDevice()).ToArray();
@ -69,6 +68,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
AddAssert("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf<OsuModTouchDevice>());
}
[Test]
public void TestTouchActivePriorToPlayerLoad()
{
AddStep("set touch input active", () => statics.SetValue(Static.TouchInputActive, true));
loadPlayer();
AddUntilStep("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf<OsuModTouchDevice>());
}
[Test]
public void TestTouchDuringBreak()
{

View File

@ -7,6 +7,7 @@ using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
@ -62,13 +63,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000;
}
int difficultyPeppyStars = (int)Math.Round(
(baseBeatmap.Difficulty.DrainRate
+ baseBeatmap.Difficulty.OverallDifficulty
+ baseBeatmap.Difficulty.CircleSize
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
scoreMultiplier = difficultyPeppyStars;
scoreMultiplier = LegacyRulesetExtensions.CalculateDifficultyPeppyStars(baseBeatmap.Difficulty, objectCount, drainLength);
LegacyScoreAttributes attributes = new LegacyScoreAttributes();

View File

@ -51,10 +51,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
base.LoadComplete();
hitObjectPosition = hitObject.PositionBindable.GetBoundCopy();
hitObjectPosition.BindValueChanged(_ => updateConnectingPath());
hitObjectPosition.BindValueChanged(_ => Scheduler.AddOnce(updateConnectingPath));
pathVersion = hitObject.Path.Version.GetBoundCopy();
pathVersion.BindValueChanged(_ => updateConnectingPath());
pathVersion.BindValueChanged(_ => Scheduler.AddOnce(updateConnectingPath));
updateConnectingPath();
}

View File

@ -4,20 +4,15 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
@ -41,8 +36,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public Action<DragEvent> DragInProgress;
public Action DragEnded;
public List<PathControlPoint> PointsInSegment;
public readonly BindableBool IsSelected = new BindableBool();
public readonly PathControlPoint ControlPoint;
@ -56,27 +49,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private IBindable<Vector2> hitObjectPosition;
private IBindable<float> hitObjectScale;
[UsedImplicitly]
private readonly IBindable<int> hitObjectVersion;
public PathControlPointPiece(T hitObject, PathControlPoint controlPoint)
{
this.hitObject = hitObject;
ControlPoint = controlPoint;
// we don't want to run the path type update on construction as it may inadvertently change the hit object.
cachePoints(hitObject);
hitObjectVersion = hitObject.Path.Version.GetBoundCopy();
// schedule ensure that updates are only applied after all operations from a single frame are applied.
// this avoids inadvertently changing the hit object path type for batch operations.
hitObjectVersion.BindValueChanged(_ => Scheduler.AddOnce(() =>
{
cachePoints(hitObject);
updatePathType();
}));
controlPoint.Changed += updateMarkerDisplay;
Origin = Anchor.Centre;
@ -214,28 +191,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
protected override void OnDragEnd(DragEndEvent e) => DragEnded?.Invoke();
private void cachePoints(T hitObject) => PointsInSegment = hitObject.Path.PointsInSegment(ControlPoint);
/// <summary>
/// Handles correction of invalid path types.
/// </summary>
private void updatePathType()
{
if (ControlPoint.Type != PathType.PERFECT_CURVE)
return;
if (PointsInSegment.Count > 3)
ControlPoint.Type = PathType.BEZIER;
if (PointsInSegment.Count != 3)
return;
ReadOnlySpan<Vector2> points = PointsInSegment.Select(p => p.Position).ToArray();
RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points);
if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
ControlPoint.Type = PathType.BEZIER;
}
/// <summary>
/// Updates the state of the circular control point marker.
/// </summary>

View File

@ -14,10 +14,12 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
@ -76,6 +78,50 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
controlPoints.BindTo(hitObject.Path.ControlPoints);
}
/// <summary>
/// Handles correction of invalid path types.
/// </summary>
public void EnsureValidPathTypes()
{
List<PathControlPoint> pointsInCurrentSegment = new List<PathControlPoint>();
foreach (var controlPoint in controlPoints)
{
if (controlPoint.Type != null)
{
pointsInCurrentSegment.Add(controlPoint);
ensureValidPathType(pointsInCurrentSegment);
pointsInCurrentSegment.Clear();
}
pointsInCurrentSegment.Add(controlPoint);
}
ensureValidPathType(pointsInCurrentSegment);
}
private void ensureValidPathType(IReadOnlyList<PathControlPoint> segment)
{
if (segment.Count == 0)
return;
var first = segment[0];
if (first.Type != PathType.PERFECT_CURVE)
return;
if (segment.Count > 3)
first.Type = PathType.BEZIER;
if (segment.Count != 3)
return;
ReadOnlySpan<Vector2> points = segment.Select(p => p.Position).ToArray();
RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points);
if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
first.Type = PathType.BEZIER;
}
/// <summary>
/// Selects the <see cref="PathControlPointPiece{T}"/> corresponding to the given <paramref name="pathControlPoint"/>,
/// and deselects all other <see cref="PathControlPointPiece{T}"/>s.
@ -240,7 +286,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
/// <param name="type">The path type we want to assign to the given control point piece.</param>
private void updatePathType(PathControlPointPiece<T> piece, PathType? type)
{
int indexInSegment = piece.PointsInSegment.IndexOf(piece.ControlPoint);
var pointsInSegment = hitObject.Path.PointsInSegment(piece.ControlPoint);
int indexInSegment = pointsInSegment.IndexOf(piece.ControlPoint);
if (type?.Type == SplineType.PerfectCurve)
{
@ -249,8 +296,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
// and one segment of the previous type.
int thirdPointIndex = indexInSegment + 2;
if (piece.PointsInSegment.Count > thirdPointIndex + 1)
piece.PointsInSegment[thirdPointIndex].Type = piece.PointsInSegment[0].Type;
if (pointsInSegment.Count > thirdPointIndex + 1)
pointsInSegment[thirdPointIndex].Type = pointsInSegment[0].Type;
}
hitObject.Path.ExpectedDistance.Value = null;
@ -339,6 +386,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
// Maintain the path types in case they got defaulted to bezier at some point during the drag.
for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++)
hitObject.Path.ControlPoints[i].Type = dragPathTypes[i];
EnsureValidPathTypes();
}
public void DragEnded() => changeHandler?.EndChange();
@ -412,6 +461,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
updatePathType(p, type);
EnsureValidPathTypes();
});
if (countOfState == totalCount)

View File

@ -267,6 +267,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
segmentStart.Type = PathType.BEZIER;
break;
}
controlPointVisualiser.EnsureValidPathTypes();
}
private void updateCursor()

View File

@ -254,6 +254,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
// Move the control points from the insertion index onwards to make room for the insertion
controlPoints.Insert(insertionIndex, pathControlPoint);
ControlPointVisualiser?.EnsureValidPathTypes();
HitObject.SnapTo(distanceSnapProvider);
return pathControlPoint;
@ -275,6 +277,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
controlPoints.Remove(c);
}
ControlPointVisualiser?.EnsureValidPathTypes();
// Snap the slider to the current beat divisor before checking length validity.
HitObject.SnapTo(distanceSnapProvider);

View File

@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{
if (drawable is DrawableSlider s)
s.Tracking.ValueChanged += flashlight.OnSliderTrackingChange;
s.OnUpdate += _ => flashlight.OnSliderTrackingChange(s);
}
private partial class OsuFlashlight : Flashlight, IRequireHighFrequencyMousePosition
@ -66,10 +66,10 @@ namespace osu.Game.Rulesets.Osu.Mods
FlashlightSmoothness = 1.4f;
}
public void OnSliderTrackingChange(ValueChangedEvent<bool> e)
public void OnSliderTrackingChange(DrawableSlider e)
{
// If a slider is in a tracking state, a further dim should be applied to the (remaining) visible portion of the playfield.
FlashlightDim = e.NewValue ? 0.8f : 0.0f;
FlashlightDim = Time.Current >= e.HitObject.StartTime && e.Tracking.Value ? 0.8f : 0.0f;
}
protected override bool OnMouseMove(MouseMoveEvent e)

View File

@ -244,6 +244,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
else if (slidingSample.IsPlaying)
slidingSample.Stop();
}
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
// During slider path editing, the PlaySliderBody is scheduled to refresh once on Update.
// It is crucial to perform the code below in UpdateAfterChildren. This ensures that the SliderBody has the opportunity
// to update its Size and PathOffset beforehand, ensuring correct placement.
double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1);

View File

@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects
set
{
repeatCount = value;
updateNestedPositions();
endPositionCache.Invalidate();
}
}
@ -165,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public Slider()
{
SamplesBindable.CollectionChanged += (_, _) => UpdateNestedSamples();
Path.Version.ValueChanged += _ => updateNestedPositions();
Path.Version.ValueChanged += _ => endPositionCache.Invalidate();
}
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)

View File

@ -14,16 +14,16 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary>
public abstract class SliderEndCircle : HitCircle
{
private readonly Slider slider;
protected readonly Slider Slider;
protected SliderEndCircle(Slider slider)
{
this.slider = slider;
Slider = slider;
}
public int RepeatIndex { get; set; }
public double SpanDuration => slider.SpanDuration;
public double SpanDuration => Slider.SpanDuration;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{
@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Objects
else
{
// The first end circle should fade in with the slider.
TimePreempt += StartTime - slider.StartTime;
TimePreempt += StartTime - Slider.StartTime;
}
}

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
ScaleBindable.BindValueChanged(scale => PathRadius = OsuHitObject.OBJECT_RADIUS * scale.NewValue, true);
pathVersion = drawableSlider.PathVersion.GetBoundCopy();
pathVersion.BindValueChanged(_ => Refresh());
pathVersion.BindValueChanged(_ => Scheduler.AddOnce(Refresh));
AccentColourBindable = drawableObject.AccentColour.GetBoundCopy();
AccentColourBindable.BindValueChanged(accent => AccentColour = GetBodyAccentColour(skin, accent.NewValue), true);

View File

@ -135,7 +135,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
protected override void Update()
{
base.Update();
spinningMiddle.Rotation = discTop.Rotation = DrawableSpinner.RotationTracker.Rotation;
float turnRatio = spinningMiddle.Texture != null ? 0.5f : 1;
discTop.Rotation = DrawableSpinner.RotationTracker.Rotation * turnRatio;
spinningMiddle.Rotation = DrawableSpinner.RotationTracker.Rotation;
discBottom.Rotation = discTop.Rotation / 3;
glow.Alpha = DrawableSpinner.Progress;

View File

@ -33,6 +33,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private void load(OsuRulesetConfigManager? rulesetConfig)
{
rulesetConfig?.BindWith(OsuRulesetSetting.ShowCursorRipples, showRipples);
AddInternal(ripplePool);
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)

View File

@ -57,17 +57,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
[BackgroundDependencyLoader]
private void load()
{
InternalChild = cursorScaleContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Child = cursorSprite = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.Cursor), _ => new DefaultCursor(), confineMode: ConfineMode.NoScaling)
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
}
};
InternalChild = CreateCursorContent();
userCursorScale = config.GetBindable<float>(OsuSetting.GameplayCursorSize);
userCursorScale.ValueChanged += _ => cursorScale.Value = CalculateCursorScale();
@ -84,6 +74,18 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
cursorScale.Value = CalculateCursorScale();
}
protected virtual Drawable CreateCursorContent() => cursorScaleContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Child = cursorSprite = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.Cursor), _ => new DefaultCursor(), confineMode: ConfineMode.NoScaling)
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
},
};
protected virtual float CalculateCursorScale()
{
float scale = userCursorScale.Value;

View File

@ -4,13 +4,11 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
@ -35,6 +33,8 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly ProxyContainer spinnerProxies;
private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer;
private readonly JudgementPooler<DrawableOsuJudgement> judgementPooler;
public SmokeContainer Smoke { get; }
public FollowPointRenderer FollowPoints { get; }
@ -42,8 +42,6 @@ namespace osu.Game.Rulesets.Osu.UI
protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer();
private readonly IDictionary<HitResult, DrawablePool<DrawableOsuJudgement>> poolDictionary = new Dictionary<HitResult, DrawablePool<DrawableOsuJudgement>>();
private readonly Container judgementAboveHitObjectLayer;
public OsuPlayfield()
@ -65,24 +63,15 @@ namespace osu.Game.Rulesets.Osu.UI
HitPolicy = new StartTimeOrderedHitPolicy();
foreach (var result in Enum.GetValues<HitResult>().Where(r =>
{
switch (r)
{
case HitResult.Great:
case HitResult.Ok:
case HitResult.Meh:
case HitResult.Miss:
case HitResult.LargeTickMiss:
case HitResult.IgnoreMiss:
return true;
}
return false;
}))
poolDictionary.Add(result, new DrawableJudgementPool(result, onJudgementLoaded));
AddRangeInternal(poolDictionary.Values);
AddInternal(judgementPooler = new JudgementPooler<DrawableOsuJudgement>(new[]
{
HitResult.Great,
HitResult.Ok,
HitResult.Meh,
HitResult.Miss,
HitResult.LargeTickMiss,
HitResult.IgnoreMiss,
}, onJudgementLoaded));
NewResult += onNewResult;
}
@ -182,10 +171,10 @@ namespace osu.Game.Rulesets.Osu.UI
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
return;
if (!poolDictionary.TryGetValue(result.Type, out var pool))
return;
var explosion = judgementPooler.Get(result.Type, doj => doj.Apply(result, judgedObject));
DrawableOsuJudgement explosion = pool.Get(doj => doj.Apply(result, judgedObject));
if (explosion == null)
return;
judgementLayer.Add(explosion);
@ -201,31 +190,6 @@ namespace osu.Game.Rulesets.Osu.UI
public void Add(Drawable proxy) => AddInternal(proxy);
}
private partial class DrawableJudgementPool : DrawablePool<DrawableOsuJudgement>
{
private readonly HitResult result;
private readonly Action<DrawableOsuJudgement> onLoaded;
public DrawableJudgementPool(HitResult result, Action<DrawableOsuJudgement> onLoaded)
: base(20)
{
this.result = result;
this.onLoaded = onLoaded;
}
protected override DrawableOsuJudgement CreateNewDrawable()
{
var judgement = base.CreateNewDrawable();
// just a placeholder to initialise the correct drawable hierarchy for this pool.
judgement.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null);
onLoaded?.Invoke(judgement);
return judgement;
}
}
private class OsuHitObjectLifetimeEntry : HitObjectLifetimeEntry
{
public OsuHitObjectLifetimeEntry(HitObject hitObject)

View File

@ -65,17 +65,24 @@ namespace osu.Game.Rulesets.Osu.UI
public override bool HandlePositionalInput => true;
public Action ResumeRequested;
private Container scaleTransitionContainer;
public OsuClickToResumeCursor()
{
RelativePositionAxes = Axes.Both;
}
protected override float CalculateCursorScale()
protected override Container CreateCursorContent() => scaleTransitionContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Child = base.CreateCursorContent(),
};
protected override float CalculateCursorScale() =>
// Force minimum cursor size so it's easily clickable
return Math.Max(1f, base.CalculateCursorScale());
}
Math.Max(1f, base.CalculateCursorScale());
protected override bool OnHover(HoverEvent e)
{
@ -98,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.UI
if (!IsHovered)
return false;
this.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint);
scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint);
ResumeRequested?.Invoke();
return true;
@ -114,7 +121,10 @@ namespace osu.Game.Rulesets.Osu.UI
public void Appear() => Schedule(() =>
{
updateColour();
this.ScaleTo(4).Then().ScaleTo(1, 1000, Easing.OutQuint);
// importantly, we perform the scale transition on an underlying container rather than the whole cursor
// to prevent attempts of abuse by the scale change in the cursor's hitbox (see: https://github.com/ppy/osu/issues/26477).
scaleTransitionContainer.ScaleTo(4).Then().ScaleTo(1, 1000, Easing.OutQuint);
});
private void updateColour()

View File

@ -0,0 +1,176 @@
// 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 NUnit.Framework;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.Taiko.Judgements;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Scoring;
namespace osu.Game.Rulesets.Taiko.Tests
{
[TestFixture]
public class TaikoHealthProcessorTest
{
[Test]
public void TestHitsOnlyGreat()
{
var beatmap = new TaikoBeatmap
{
HitObjects =
{
new Hit(),
new Hit { StartTime = 1000 },
new Hit { StartTime = 2000 },
new Hit { StartTime = 3000 },
new Hit { StartTime = 4000 },
}
};
var healthProcessor = new TaikoHealthProcessor();
healthProcessor.ApplyBeatmap(beatmap);
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Great });
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], new TaikoJudgement()) { Type = HitResult.Great });
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[2], new TaikoJudgement()) { Type = HitResult.Great });
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[3], new TaikoJudgement()) { Type = HitResult.Great });
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[4], new TaikoJudgement()) { Type = HitResult.Great });
Assert.Multiple(() =>
{
Assert.That(healthProcessor.Health.Value, Is.EqualTo(1));
Assert.That(healthProcessor.HasFailed, Is.False);
});
}
[Test]
public void TestHitsAboveThreshold()
{
var beatmap = new TaikoBeatmap
{
HitObjects =
{
new Hit(),
new Hit { StartTime = 1000 },
new Hit { StartTime = 2000 },
new Hit { StartTime = 3000 },
new Hit { StartTime = 4000 },
}
};
var healthProcessor = new TaikoHealthProcessor();
healthProcessor.ApplyBeatmap(beatmap);
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Great });
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], new TaikoJudgement()) { Type = HitResult.Ok });
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[2], new TaikoJudgement()) { Type = HitResult.Ok });
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[3], new TaikoJudgement()) { Type = HitResult.Ok });
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[4], new TaikoJudgement()) { Type = HitResult.Miss });
Assert.Multiple(() =>
{
Assert.That(healthProcessor.Health.Value, Is.GreaterThan(0.5));
Assert.That(healthProcessor.HasFailed, Is.False);
});
}
[Test]
public void TestHitsBelowThreshold()
{
var beatmap = new TaikoBeatmap
{
HitObjects =
{
new Hit(),
new Hit { StartTime = 1000 },
new Hit { StartTime = 2000 },
new Hit { StartTime = 3000 },
new Hit { StartTime = 4000 },
}
};
var healthProcessor = new TaikoHealthProcessor();
healthProcessor.ApplyBeatmap(beatmap);
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Miss });
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], new TaikoJudgement()) { Type = HitResult.Ok });
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[2], new TaikoJudgement()) { Type = HitResult.Ok });
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[3], new TaikoJudgement()) { Type = HitResult.Ok });
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[4], new TaikoJudgement()) { Type = HitResult.Miss });
Assert.Multiple(() =>
{
Assert.That(healthProcessor.Health.Value, Is.LessThan(0.5));
Assert.That(healthProcessor.HasFailed, Is.True);
});
}
[Test]
public void TestDrumRollOnly()
{
var beatmap = new TaikoBeatmap
{
HitObjects =
{
new DrumRoll { Duration = 2000 }
}
};
foreach (var ho in beatmap.HitObjects)
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
var healthProcessor = new TaikoHealthProcessor();
healthProcessor.ApplyBeatmap(beatmap);
foreach (var nested in beatmap.HitObjects[0].NestedHitObjects)
{
var nestedJudgement = nested.CreateJudgement();
healthProcessor.ApplyResult(new JudgementResult(nested, nestedJudgement) { Type = nestedJudgement.MaxResult });
}
var judgement = beatmap.HitObjects[0].CreateJudgement();
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], judgement) { Type = judgement.MaxResult });
Assert.Multiple(() =>
{
Assert.That(healthProcessor.Health.Value, Is.EqualTo(1));
Assert.That(healthProcessor.HasFailed, Is.False);
});
}
[Test]
public void TestSwellOnly()
{
var beatmap = new TaikoBeatmap
{
HitObjects =
{
new DrumRoll { Duration = 2000 }
}
};
foreach (var ho in beatmap.HitObjects)
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
var healthProcessor = new TaikoHealthProcessor();
healthProcessor.ApplyBeatmap(beatmap);
foreach (var nested in beatmap.HitObjects[0].NestedHitObjects)
{
var nestedJudgement = nested.CreateJudgement();
healthProcessor.ApplyResult(new JudgementResult(nested, nestedJudgement) { Type = nestedJudgement.MaxResult });
}
var judgement = beatmap.HitObjects[0].CreateJudgement();
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], judgement) { Type = judgement.MaxResult });
Assert.Multiple(() =>
{
Assert.That(healthProcessor.Health.Value, Is.EqualTo(1));
Assert.That(healthProcessor.HasFailed, Is.False);
});
}
}
}

View File

@ -7,6 +7,7 @@ using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Scoring.Legacy;
@ -65,11 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000;
}
difficultyPeppyStars = (int)Math.Round(
(baseBeatmap.Difficulty.DrainRate
+ baseBeatmap.Difficulty.OverallDifficulty
+ baseBeatmap.Difficulty.CircleSize
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
difficultyPeppyStars = LegacyRulesetExtensions.CalculateDifficultyPeppyStars(baseBeatmap.Difficulty, objectCount, drainLength);
LegacyScoreAttributes attributes = new LegacyScoreAttributes();

View File

@ -31,11 +31,39 @@ namespace osu.Game.Rulesets.Taiko.Scoring
/// </summary>
private double hpMissMultiplier;
/// <summary>
/// Sum of all achievable health increases throughout the map.
/// Used to determine if there are any objects that give health.
/// If there are none, health will be forcibly pulled up to 1 to avoid cases of impassable maps.
/// </summary>
private double sumOfMaxHealthIncreases;
public TaikoHealthProcessor()
: base(0.5)
{
}
protected override void ApplyResultInternal(JudgementResult result)
{
base.ApplyResultInternal(result);
sumOfMaxHealthIncreases += result.Judgement.MaxHealthIncrease;
}
protected override void RevertResultInternal(JudgementResult result)
{
base.RevertResultInternal(result);
sumOfMaxHealthIncreases -= result.Judgement.MaxHealthIncrease;
}
protected override void Reset(bool storeResults)
{
base.Reset(storeResults);
if (storeResults && sumOfMaxHealthIncreases == 0)
Health.Value = 1;
sumOfMaxHealthIncreases = 0;
}
public override void ApplyBeatmap(IBeatmap beatmap)
{
base.ApplyBeatmap(beatmap);

View File

@ -69,9 +69,9 @@ namespace osu.Game.Rulesets.Taiko
public override IEnumerable<KeyBinding> GetDefaultKeyBindings(int variant = 0) => new[]
{
new KeyBinding(InputKey.MouseLeft, TaikoAction.LeftCentre),
new KeyBinding(InputKey.MouseRight, TaikoAction.LeftRim),
new KeyBinding(InputKey.D, TaikoAction.LeftRim),
new KeyBinding(InputKey.MouseLeft, TaikoAction.LeftCentre),
new KeyBinding(InputKey.F, TaikoAction.LeftCentre),
new KeyBinding(InputKey.J, TaikoAction.RightCentre),
new KeyBinding(InputKey.K, TaikoAction.RightRim),

View File

@ -1,8 +1,6 @@
// 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.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
@ -10,7 +8,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Framework.Graphics.Primitives;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
@ -42,29 +39,29 @@ namespace osu.Game.Rulesets.Taiko.UI
public Container UnderlayElements { get; private set; } = null!;
private Container<HitExplosion> hitExplosionContainer;
private Container<KiaiHitExplosion> kiaiExplosionContainer;
private JudgementContainer<DrawableTaikoJudgement> judgementContainer;
private ScrollingHitObjectContainer drumRollHitContainer;
internal Drawable HitTarget;
private SkinnableDrawable mascot;
private Container<HitExplosion> hitExplosionContainer = null!;
private Container<KiaiHitExplosion> kiaiExplosionContainer = null!;
private JudgementContainer<DrawableTaikoJudgement> judgementContainer = null!;
private ScrollingHitObjectContainer drumRollHitContainer = null!;
internal Drawable HitTarget = null!;
private SkinnableDrawable mascot = null!;
private readonly IDictionary<HitResult, DrawablePool<DrawableTaikoJudgement>> judgementPools = new Dictionary<HitResult, DrawablePool<DrawableTaikoJudgement>>();
private JudgementPooler<DrawableTaikoJudgement> judgementPooler = null!;
private readonly IDictionary<HitResult, HitExplosionPool> explosionPools = new Dictionary<HitResult, HitExplosionPool>();
private ProxyContainer topLevelHitContainer;
private InputDrum inputDrum;
private Container rightArea;
private ProxyContainer topLevelHitContainer = null!;
private InputDrum inputDrum = null!;
private Container rightArea = null!;
/// <remarks>
/// <see cref="Playfield.AddNested"/> is purposefully not called on this to prevent i.e. being able to interact
/// with bar lines in the editor.
/// </remarks>
private BarLinePlayfield barLinePlayfield;
private BarLinePlayfield barLinePlayfield = null!;
private Container barLineContent;
private Container hitObjectContent;
private Container overlayContent;
private Container barLineContent = null!;
private Container hitObjectContent = null!;
private Container overlayContent = null!;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
@ -202,13 +199,12 @@ namespace osu.Game.Rulesets.Taiko.UI
var hitWindows = new TaikoHitWindows();
foreach (var result in Enum.GetValues<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r)))
{
judgementPools.Add(result, new DrawablePool<DrawableTaikoJudgement>(15));
explosionPools.Add(result, new HitExplosionPool(result));
}
HitResult[] usableHitResults = Enum.GetValues<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r)).ToArray();
AddRangeInternal(judgementPools.Values);
AddInternal(judgementPooler = new JudgementPooler<DrawableTaikoJudgement>(usableHitResults));
foreach (var result in usableHitResults)
explosionPools.Add(result, new HitExplosionPool(result));
AddRangeInternal(explosionPools.Values);
}
@ -339,7 +335,12 @@ namespace osu.Game.Rulesets.Taiko.UI
if (!result.Type.IsScorable())
break;
judgementContainer.Add(judgementPools[result.Type].Get(j => j.Apply(result, judgedObject)));
var judgement = judgementPooler.Get(result.Type, j => j.Apply(result, judgedObject));
if (judgement == null)
return;
judgementContainer.Add(judgement);
var type = (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre;
addExplosion(judgedObject, result.Type, type);

View File

@ -3,14 +3,18 @@
#nullable disable
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Legacy;
using osu.Game.IO.Legacy;
using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
@ -247,6 +251,123 @@ namespace osu.Game.Tests.Beatmaps.Formats
});
}
[Test]
public void AccuracyAndRankOfStableScorePreserved()
{
var memoryStream = new MemoryStream();
// local partial implementation of legacy score encoder
// this is done half for readability, half because `LegacyScoreEncoder` forces `LATEST_VERSION`
// and we want to emulate a stable score here
using (var sw = new SerializationWriter(memoryStream, true))
{
sw.Write((byte)0); // ruleset id (osu!)
sw.Write(20240116); // version (anything below `LegacyScoreEncoder.FIRST_LAZER_VERSION` is stable)
sw.Write(string.Empty.ComputeMD5Hash()); // beatmap hash, irrelevant to this test
sw.Write("username"); // irrelevant to this test
sw.Write(string.Empty.ComputeMD5Hash()); // score hash, irrelevant to this test
sw.Write((ushort)198); // count300
sw.Write((ushort)1); // count100
sw.Write((ushort)0); // count50
sw.Write((ushort)0); // countGeki
sw.Write((ushort)0); // countKatu
sw.Write((ushort)1); // countMiss
sw.Write(12345678); // total score, irrelevant to this test
sw.Write((ushort)1000); // max combo, irrelevant to this test
sw.Write(false); // full combo, irrelevant to this test
sw.Write((int)LegacyMods.Hidden); // mods
sw.Write(string.Empty); // hp graph, irrelevant
sw.Write(DateTime.Now); // date, irrelevant
sw.Write(Array.Empty<byte>()); // replay data, irrelevant
sw.Write((long)1234); // legacy online ID, irrelevant
}
memoryStream.Seek(0, SeekOrigin.Begin);
var decoded = new TestLegacyScoreDecoder().Parse(memoryStream);
Assert.Multiple(() =>
{
Assert.That(decoded.ScoreInfo.Accuracy, Is.EqualTo((double)(198 * 300 + 100) / (200 * 300)));
Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.A));
});
}
[Test]
public void AccuracyAndRankOfLazerScorePreserved()
{
var ruleset = new OsuRuleset().RulesetInfo;
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
scoreInfo.Mods = new Mod[] { new OsuModFlashlight() };
scoreInfo.Statistics = new Dictionary<HitResult, int>
{
[HitResult.Great] = 199,
[HitResult.Miss] = 1,
[HitResult.LargeTickHit] = 1,
};
scoreInfo.MaximumStatistics = new Dictionary<HitResult, int>
{
[HitResult.Great] = 200,
[HitResult.LargeTickHit] = 1,
};
var beatmap = new TestBeatmap(ruleset);
var score = new Score
{
ScoreInfo = scoreInfo,
};
var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
Assert.Multiple(() =>
{
Assert.That(decodedAfterEncode.ScoreInfo.Accuracy, Is.EqualTo((double)(199 * 300 + 30) / (200 * 300 + 30)));
Assert.That(decodedAfterEncode.ScoreInfo.Rank, Is.EqualTo(ScoreRank.SH));
});
}
[Test]
public void AccuracyAndRankOfLazerScoreWithoutLegacyReplaySoloScoreInfoUsesBestEffortFallbackToLegacy()
{
var memoryStream = new MemoryStream();
// local partial implementation of legacy score encoder
// this is done half for readability, half because we want to emulate an old lazer score here
// that does not have everything that `LegacyScoreEncoder` now writes to the replay
using (var sw = new SerializationWriter(memoryStream, true))
{
sw.Write((byte)0); // ruleset id (osu!)
sw.Write(LegacyScoreEncoder.FIRST_LAZER_VERSION); // version
sw.Write(string.Empty.ComputeMD5Hash()); // beatmap hash, irrelevant to this test
sw.Write("username"); // irrelevant to this test
sw.Write(string.Empty.ComputeMD5Hash()); // score hash, irrelevant to this test
sw.Write((ushort)198); // count300
sw.Write((ushort)0); // count100
sw.Write((ushort)1); // count50
sw.Write((ushort)0); // countGeki
sw.Write((ushort)0); // countKatu
sw.Write((ushort)1); // countMiss
sw.Write(12345678); // total score, irrelevant to this test
sw.Write((ushort)1000); // max combo, irrelevant to this test
sw.Write(false); // full combo, irrelevant to this test
sw.Write((int)LegacyMods.Hidden); // mods
sw.Write(string.Empty); // hp graph, irrelevant
sw.Write(DateTime.Now); // date, irrelevant
sw.Write(Array.Empty<byte>()); // replay data, irrelevant
sw.Write((long)1234); // legacy online ID, irrelevant
// importantly, no compressed `LegacyReplaySoloScoreInfo` here
}
memoryStream.Seek(0, SeekOrigin.Begin);
var decoded = new TestLegacyScoreDecoder().Parse(memoryStream);
Assert.Multiple(() =>
{
Assert.That(decoded.ScoreInfo.Accuracy, Is.EqualTo((double)(198 * 300 + 50) / (200 * 300)));
Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.A));
});
}
private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
{
var encodeStream = new MemoryStream();

View File

@ -7,7 +7,9 @@ using Moq;
using NUnit.Framework;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Utils;
namespace osu.Game.Tests.Mods
@ -310,6 +312,16 @@ namespace osu.Game.Tests.Mods
Assert.That(invalid?.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
}
[Test]
public void TestModBelongsToRuleset()
{
Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), Array.Empty<Mod>()));
Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), new Mod[] { new OsuModDoubleTime() }));
Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), new Mod[] { new OsuModDoubleTime(), new OsuModAccuracyChallenge() }));
Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), new Mod[] { new OsuModDoubleTime(), new ModAccuracyChallenge() }), Is.False);
Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), new Mod[] { new OsuModDoubleTime(), new TaikoModFlashlight() }), Is.False);
}
[Test]
public void TestFormatScoreMultiplier()
{

View File

@ -120,10 +120,31 @@ namespace osu.Game.Tests.NonVisual.Skinning
Assert.IsNotNull(texture);
Assert.That(texture.ScaleAdjust, Is.EqualTo(1));
texture = legacySkin.GetTexture("hitcircle@2x");
var twoTimesTexture = legacySkin.GetTexture("hitcircle@2x");
Assert.IsNotNull(twoTimesTexture);
Assert.That(twoTimesTexture.ScaleAdjust, Is.EqualTo(1));
Assert.AreNotEqual(texture, twoTimesTexture);
}
[Test]
public void TestAllowHighResolutionSprites()
{
var textureStore = new TestTextureStore("hitcircle", "hitcircle@2x");
var legacySkin = new TestLegacySkin(textureStore) { HighResolutionSprites = true };
var texture = legacySkin.GetTexture("hitcircle");
Assert.IsNotNull(texture);
Assert.That(texture.ScaleAdjust, Is.EqualTo(1));
Assert.That(texture.ScaleAdjust, Is.EqualTo(2));
var twoTimesTexture = legacySkin.GetTexture("hitcircle@2x");
Assert.IsNotNull(twoTimesTexture);
Assert.That(twoTimesTexture.ScaleAdjust, Is.EqualTo(2));
Assert.AreEqual(texture, twoTimesTexture);
}
private class TestLegacySkin : LegacySkin

View File

@ -0,0 +1,3 @@
[Mania]
Keys: 4
ColumnLineWidth: 3,,3,3,3

View File

@ -18,10 +18,12 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Tests.Beatmaps;
@ -385,6 +387,42 @@ namespace osu.Game.Tests.Rulesets.Scoring
Assert.That(scoreProcessor.Accuracy.Value, Is.Not.EqualTo(1));
}
[Test]
public void TestNormalGrades()
{
scoreProcessor.ApplyBeatmap(new Beatmap());
Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.X));
scoreProcessor.Accuracy.Value = 0.99f;
Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.S));
}
[Test]
public void TestSilverGrades()
{
scoreProcessor.ApplyBeatmap(new Beatmap());
Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.X));
scoreProcessor.Mods.Value = new[] { new OsuModHidden() };
Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.XH));
scoreProcessor.Accuracy.Value = 0.99f;
Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.SH));
}
[Test]
public void TestSilverGradesModsAppliedFirst()
{
scoreProcessor.Mods.Value = new[] { new OsuModHidden() };
scoreProcessor.ApplyBeatmap(new Beatmap());
Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.XH));
scoreProcessor.Accuracy.Value = 0.99f;
Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.SH));
}
private class TestJudgement : Judgement
{
public override HitResult MaxResult { get; }

View File

@ -114,5 +114,25 @@ namespace osu.Game.Tests.Skins
Assert.That(configs[0].MinimumColumnWidth, Is.EqualTo(16));
}
}
[Test]
public void TestParseArrayWithSomeEmptyElements()
{
var decoder = new LegacyManiaSkinDecoder();
using (var resStream = TestResources.OpenResource("mania-skin-broken-array.ini"))
using (var stream = new LineBufferedReader(resStream))
{
var configs = decoder.Decode(stream);
Assert.That(configs.Count, Is.EqualTo(1));
Assert.That(configs[0].ColumnLineWidth.Length, Is.EqualTo(5));
Assert.That(configs[0].ColumnLineWidth[0], Is.EqualTo(3));
Assert.That(configs[0].ColumnLineWidth[1], Is.EqualTo(0)); // malformed entry, should be parsed as zero
Assert.That(configs[0].ColumnLineWidth[2], Is.EqualTo(3));
Assert.That(configs[0].ColumnLineWidth[3], Is.EqualTo(3));
Assert.That(configs[0].ColumnLineWidth[4], Is.EqualTo(3));
}
}
}
}

View File

@ -4,11 +4,13 @@
#nullable disable
using System;
using System.Diagnostics;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
@ -177,6 +179,43 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("Scrolled to end", () => timingScreen.ChildrenOfType<OsuScrollContainer>().First().IsScrolledToEnd());
}
[Test]
public void TestEditThenClickAwayAppliesChanges()
{
AddStep("Add two control points", () =>
{
editorBeatmap.ControlPointInfo.Clear();
editorBeatmap.ControlPointInfo.Add(1000, new TimingControlPoint());
editorBeatmap.ControlPointInfo.Add(2000, new TimingControlPoint());
});
AddStep("Select second timing point", () =>
{
InputManager.MoveMouseTo(Child.ChildrenOfType<TimingRowAttribute>().Last());
InputManager.Click(MouseButton.Left);
});
AddStep("Scroll to end", () => timingScreen.ChildrenOfType<ControlPointSettings>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddStep("Modify time signature", () =>
{
var timeSignatureTextBox = Child.ChildrenOfType<LabelledTimeSignature.TimeSignatureBox>().Single().ChildrenOfType<TextBox>().Single();
InputManager.MoveMouseTo(timeSignatureTextBox);
InputManager.Click(MouseButton.Left);
Debug.Assert(!timeSignatureTextBox.Current.Value.Equals("1", StringComparison.Ordinal));
timeSignatureTextBox.Current.Value = "1";
});
AddStep("Select first timing point", () =>
{
InputManager.MoveMouseTo(Child.ChildrenOfType<TimingRowAttribute>().First());
InputManager.Click(MouseButton.Left);
});
AddAssert("Second timing point changed time signature", () => editorBeatmap.ControlPointInfo.TimingPoints.Last().TimeSignature.Numerator == 1);
AddAssert("First timing point preserved time signature", () => editorBeatmap.ControlPointInfo.TimingPoints.First().TimeSignature.Numerator == 4);
}
protected override void Dispose(bool isDisposing)
{
Beatmap.Disabled = false;

View File

@ -147,6 +147,16 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("Assert max judgement hidden", () => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().First().Alpha == 0);
}
[Test]
public void TestNoDuplicates()
{
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All);
AddAssert("Check no duplicates",
() => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().Count(),
() => Is.EqualTo(counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().Select(c => c.ResultName.Text).Distinct().Count()));
}
[Test]
public void TestCycleDisplayModes()
{
@ -163,7 +173,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private int hiddenCount()
{
var num = counterDisplay.CounterFlow.Children.First(child => child.Result.Type == HitResult.LargeTickHit);
var num = counterDisplay.CounterFlow.Children.First(child => child.Result.Types.Contains(HitResult.LargeTickHit));
return num.Result.ResultCount.Value;
}

View File

@ -13,7 +13,6 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Utils;
@ -487,13 +486,8 @@ namespace osu.Game.Tests.Visual.Gameplay
}
}
private class TestMod : Mod, IApplicableToScoreProcessor
private class TestMod : OsuModDoubleTime, IApplicableToScoreProcessor
{
public override string Name => string.Empty;
public override string Acronym => string.Empty;
public override double ScoreMultiplier => 1;
public override LocalisableString Description => string.Empty;
public bool Applied { get; private set; }
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)

View File

@ -15,13 +15,14 @@ using osu.Game.Online.Rooms;
using osu.Game.Online.Solo;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Tests.Beatmaps;
@ -35,12 +36,19 @@ namespace osu.Game.Tests.Visual.Gameplay
private Func<RulesetInfo, IBeatmap> createCustomBeatmap;
private Func<Ruleset> createCustomRuleset;
private Func<Mod[]> createCustomMods;
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
protected override bool HasCustomSteps => true;
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new FakeImportingPlayer(false);
protected override TestPlayer CreatePlayer(Ruleset ruleset)
{
if (createCustomMods != null)
SelectedMods.Value = SelectedMods.Value.Concat(createCustomMods()).ToList();
return new FakeImportingPlayer(false);
}
protected new FakeImportingPlayer Player => (FakeImportingPlayer)base.Player;
@ -278,13 +286,28 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
}
private void createPlayerTest(bool allowFail = false, Func<RulesetInfo, IBeatmap> createBeatmap = null, Func<Ruleset> createRuleset = null)
[Test]
public void TestNoSubmissionWithModsOfDifferentRuleset()
{
prepareTestAPI(true);
createPlayerTest(createRuleset: () => new OsuRuleset(), createMods: () => new Mod[] { new TaikoModHidden() });
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
AddAssert("gameplay not loaded", () => Player.DrawableRuleset == null);
AddStep("exit", () => Player.Exit());
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
}
private void createPlayerTest(bool allowFail = false, Func<RulesetInfo, IBeatmap> createBeatmap = null, Func<Ruleset> createRuleset = null, Func<Mod[]> createMods = null)
{
CreateTest(() => AddStep("set up requirements", () =>
{
this.allowFail = allowFail;
createCustomBeatmap = createBeatmap;
createCustomRuleset = createRuleset;
createCustomMods = createMods;
}));
}
@ -360,11 +383,6 @@ namespace osu.Game.Tests.Visual.Gameplay
AllowImportCompletion = new SemaphoreSlim(1);
}
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart)
{
ShouldValidatePlaybackRate = false,
};
protected override async Task ImportScore(Score score)
{
ScoreImportStarted = true;

View File

@ -8,6 +8,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
@ -29,7 +30,7 @@ namespace osu.Game.Tests.Visual.Mods
public void TestMaximumAchievableAccuracy() =>
CreateModTest(new ModTestData
{
Mod = new ModAccuracyChallenge
Mod = new OsuModAccuracyChallenge
{
MinimumAccuracy = { Value = 0.6 }
},
@ -49,7 +50,7 @@ namespace osu.Game.Tests.Visual.Mods
public void TestStandardAccuracy() =>
CreateModTest(new ModTestData
{
Mod = new ModAccuracyChallenge
Mod = new OsuModAccuracyChallenge
{
MinimumAccuracy = { Value = 0.6 },
AccuracyJudgeMode = { Value = ModAccuracyChallenge.AccuracyMode.Standard }

View File

@ -19,8 +19,10 @@ using osu.Game.Beatmaps.Drawables;
using osu.Game.Database;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Models;
using osu.Game.Online.API;
using osu.Game.Online.Chat;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
@ -302,6 +304,37 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
}
[Test]
public void TestSelectableMouseHandling()
{
bool resultsRequested = false;
AddStep("reset flag", () => resultsRequested = false);
createPlaylist(p =>
{
p.AllowSelection = true;
p.AllowShowingResults = true;
p.RequestResults = _ => resultsRequested = true;
});
AddStep("move mouse to first item title", () =>
{
var drawQuad = playlist.ChildrenOfType<LinkFlowContainer>().First().ScreenSpaceDrawQuad;
var location = (drawQuad.TopLeft + drawQuad.BottomLeft) / 2 + new Vector2(drawQuad.Width * 0.2f, 0);
InputManager.MoveMouseTo(location);
});
AddUntilStep("wait for text load", () => playlist.ChildrenOfType<DrawableLinkCompiler>().Any());
AddAssert("first item title not hovered", () => playlist.ChildrenOfType<DrawableLinkCompiler>().First().IsHovered, () => Is.False);
AddStep("click left mouse", () => InputManager.Click(MouseButton.Left));
AddUntilStep("first item selected", () => playlist.ChildrenOfType<DrawableRoomPlaylistItem>().First().IsSelectedItem, () => Is.True);
// implies being clickable.
AddUntilStep("first item title hovered", () => playlist.ChildrenOfType<DrawableLinkCompiler>().First().IsHovered, () => Is.True);
AddStep("move mouse to second item results button", () => InputManager.MoveMouseTo(playlist.ChildrenOfType<GrayButton>().ElementAt(5)));
AddStep("click left mouse", () => InputManager.Click(MouseButton.Left));
AddUntilStep("results requested", () => resultsRequested);
}
private void moveToItem(int index, Vector2? offset = null)
=> AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType<DrawableRoomPlaylistItem>().ElementAt(index), offset));

View File

@ -10,12 +10,14 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Utils;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
@ -23,6 +25,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public partial class TestSceneFreeModSelectOverlay : MultiplayerTestScene
{
private FreeModSelectOverlay freeModSelectOverlay;
private FooterButtonFreeMods footerButtonFreeMods;
private readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> availableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>();
[BackgroundDependencyLoader]
@ -119,11 +122,46 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("select all button enabled", () => this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
}
[Test]
public void TestSelectAllViaFooterButtonThenDeselectFromOverlay()
{
createFreeModSelect();
AddAssert("overlay select all button enabled", () => freeModSelectOverlay.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
AddAssert("footer button displays off", () => footerButtonFreeMods.ChildrenOfType<IHasText>().Any(t => t.Text == "off"));
AddStep("click footer select all button", () =>
{
InputManager.MoveMouseTo(footerButtonFreeMods);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("all mods selected", assertAllAvailableModsSelected);
AddAssert("footer button displays all", () => footerButtonFreeMods.ChildrenOfType<IHasText>().Any(t => t.Text == "all"));
AddStep("click deselect all button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<DeselectAllModsButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("all mods deselected", () => !freeModSelectOverlay.SelectedMods.Value.Any());
AddAssert("footer button displays off", () => footerButtonFreeMods.ChildrenOfType<IHasText>().Any(t => t.Text == "off"));
}
private void createFreeModSelect()
{
AddStep("create free mod select screen", () => Child = freeModSelectOverlay = new FreeModSelectOverlay
AddStep("create free mod select screen", () => Children = new Drawable[]
{
State = { Value = Visibility.Visible }
freeModSelectOverlay = new FreeModSelectOverlay
{
State = { Value = Visibility.Visible }
},
footerButtonFreeMods = new FooterButtonFreeMods(freeModSelectOverlay)
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Current = { BindTarget = freeModSelectOverlay.SelectedMods },
},
});
AddUntilStep("all column content loaded",
() => freeModSelectOverlay.ChildrenOfType<ModColumn>().Any()
@ -134,10 +172,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
var allAvailableMods = availableMods.Value
.Where(pair => pair.Key != ModType.System)
.SelectMany(pair => pair.Value)
.SelectMany(pair => ModUtils.FlattenMods(pair.Value))
.Where(mod => mod.UserPlayable && mod.HasImplementation)
.ToList();
if (freeModSelectOverlay.SelectedMods.Value.Count != allAvailableMods.Count)
return false;
foreach (var availableMod in allAvailableMods)
{
if (freeModSelectOverlay.SelectedMods.Value.All(selectedMod => selectedMod.GetType() != availableMod.GetType()))

View File

@ -29,7 +29,9 @@ using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko;
using osu.Game.Scoring;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
@ -1009,6 +1011,43 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
}
[Test]
public void TestGameplayStartsWhileInSongSelectWithDifferentRuleset()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
QueueMode = { Value = QueueMode.AllPlayers },
Playlist =
{
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
AllowedMods = new[] { new APIMod { Acronym = "HD" } },
},
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 1)).BeatmapInfo)
{
RulesetID = new TaikoRuleset().RulesetInfo.OnlineID,
AllowedMods = new[] { new APIMod { Acronym = "HD" } },
},
}
});
AddStep("select hidden", () => multiplayerClient.ChangeUserMods(new[] { new APIMod { Acronym = "HD" } }));
AddStep("make user ready", () => multiplayerClient.ChangeState(MultiplayerUserState.Ready));
AddStep("press edit on second item", () => this.ChildrenOfType<DrawableRoomPlaylistItem>().Single(i => i.Item.RulesetID == 1)
.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistEditButton>().Single().TriggerClick());
AddUntilStep("wait for song select", () => InputManager.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);
AddAssert("ruleset is taiko", () => Ruleset.Value.OnlineID == 1);
AddStep("start match", () => multiplayerClient.StartMatch().WaitSafely());
AddUntilStep("wait for loading", () => multiplayerClient.ClientRoom?.State == MultiplayerRoomState.WaitingForLoad);
AddUntilStep("wait for gameplay to start", () => multiplayerClient.ClientRoom?.State == MultiplayerRoomState.Playing);
AddAssert("hidden is selected", () => SelectedMods.Value, () => Has.One.TypeOf(typeof(OsuModHidden)));
}
private void enterGameplay()
{
pressReadyButton();

View File

@ -35,6 +35,7 @@ using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel;
@ -834,6 +835,24 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("exit dialog is shown", () => Game.Dependencies.Get<IDialogOverlay>().CurrentDialog is ConfirmExitDialog);
}
[Test]
public void TestQuickSkinEditorDoesntNukeSkin()
{
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddStep("open", () => InputManager.Key(Key.Space));
AddStep("skin", () => InputManager.Key(Key.E));
AddStep("editor", () => InputManager.Key(Key.S));
AddStep("and close immediately", () => InputManager.Key(Key.Escape));
AddStep("open again", () => InputManager.Key(Key.S));
Player player = null;
AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null);
AddUntilStep("wait for gameplay still has health bar", () => player.ChildrenOfType<ArgonHealthDisplay>().Any());
}
[Test]
public void TestTouchScreenDetectionAtSongSelect()
{

View File

@ -268,6 +268,26 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("update not received", () => update == null);
}
[Test]
public void TestGlobalStatisticsUpdatedAfterRegistrationAddedAndScoreProcessed()
{
int userId = getUserId();
long scoreId = getScoreId();
setUpUser(userId);
var ruleset = new OsuRuleset().RulesetInfo;
SoloStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
AddUntilStep("update received", () => update != null);
AddAssert("local user values are correct", () => dummyAPI.LocalUser.Value.Statistics.TotalScore, () => Is.EqualTo(5_000_000));
AddAssert("statistics values are correct", () => dummyAPI.Statistics.Value!.TotalScore, () => Is.EqualTo(5_000_000));
}
private int nextUserId = 2000;
private long nextScoreId = 50000;

View File

@ -9,8 +9,10 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
@ -29,6 +31,9 @@ namespace osu.Game.Tests.Visual.Online
private UserGridPanel boundPanel1;
private TestUserListPanel boundPanel2;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
[Resolved]
private IRulesetStore rulesetStore { get; set; }
@ -85,8 +90,25 @@ namespace osu.Game.Tests.Visual.Online
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
IsOnline = false,
LastVisit = DateTimeOffset.Now
})
},
}),
new UserRankPanel(new APIUser
{
Username = @"flyte",
Id = 3103765,
CountryCode = CountryCode.JP,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg",
Statistics = new UserStatistics { GlobalRank = 12345, CountryRank = 1234 }
}) { Width = 300 },
new UserRankPanel(new APIUser
{
Username = @"peppy",
Id = 2,
Colour = "99EB47",
CountryCode = CountryCode.AU,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
Statistics = new UserStatistics { GlobalRank = null, CountryRank = null }
}) { Width = 300 }
}
};
boundPanel1.Status.BindTo(status);
@ -136,6 +158,23 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("visit message is not visible", () => !boundPanel2.LastVisitMessage.IsPresent);
}
[Test]
public void TestUserStatisticsChange()
{
AddStep("update statistics", () =>
{
API.UpdateStatistics(new UserStatistics
{
GlobalRank = RNG.Next(100000),
CountryRank = RNG.Next(100000)
});
});
AddStep("set statistics to empty", () =>
{
API.UpdateStatistics(new UserStatistics());
});
}
private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!);
private ScoreInfo createScore(string name) => new ScoreInfo(new TestBeatmap(Ruleset.Value).BeatmapInfo)

View File

@ -17,6 +17,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Placeholders;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
@ -49,8 +50,8 @@ namespace osu.Game.Tests.Visual.Playlists
// Previous test instances of the results screen may still exist at this point so wait for
// those screens to be cleaned up by the base SetUpSteps before re-initialising test state.
// The the screen also holds a leased Beatmap bindable so reassigning it must happen after
// the screen as been exited.
// The screen also holds a leased Beatmap bindable so reassigning it must happen after
// the screen has been exited.
AddStep("initialise user scores and beatmap", () =>
{
lowestScoreId = 1;
@ -63,8 +64,6 @@ namespace osu.Game.Tests.Visual.Playlists
userScore.Statistics = new Dictionary<HitResult, int>();
userScore.MaximumStatistics = new Dictionary<HitResult, int>();
bindHandler();
// Beatmap is required to be an actual beatmap so the scores can get their scores correctly
// calculated for standardised scoring, else the tests that rely on ordering will fall over.
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
@ -77,6 +76,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
createResults(() => userScore);
waitForDisplay();
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded);
AddAssert($"score panel position is {real_user_position}",
@ -86,7 +86,10 @@ namespace osu.Game.Tests.Visual.Playlists
[Test]
public void TestShowNullUserScore()
{
AddStep("bind user score info handler", () => bindHandler());
createResults();
waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
}
@ -97,6 +100,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("bind user score info handler", () => bindHandler(true, userScore));
createResults(() => userScore);
waitForDisplay();
AddAssert("more than 1 panel displayed", () => this.ChildrenOfType<ScorePanel>().Count() > 1);
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded);
@ -108,6 +112,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("bind delayed handler", () => bindHandler(true));
createResults();
waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
}
@ -115,10 +120,11 @@ namespace osu.Game.Tests.Visual.Playlists
[Test]
public void TestFetchWhenScrolledToTheRight()
{
createResults();
AddStep("bind delayed handler", () => bindHandler(true));
createResults();
waitForDisplay();
for (int i = 0; i < 2; i++)
{
int beforePanelCount = 0;
@ -134,12 +140,44 @@ namespace osu.Game.Tests.Visual.Playlists
}
}
[Test]
public void TestNoMoreScoresToTheRight()
{
AddStep("bind delayed handler with scores", () => bindHandler(delayed: true));
createResults();
waitForDisplay();
int beforePanelCount = 0;
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() >= beforePanelCount + scores_per_result);
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("bind delayed handler with no scores", () => bindHandler(delayed: true, noScores: true));
AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay();
AddAssert("count not increased", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount);
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
AddAssert("no placeholders shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.Zero);
}
[Test]
public void TestFetchWhenScrolledToTheLeft()
{
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
createResults(() => userScore);
waitForDisplay();
AddStep("bind delayed handler", () => bindHandler(true));
@ -158,6 +196,15 @@ namespace osu.Game.Tests.Visual.Playlists
}
}
[Test]
public void TestShowWithNoScores()
{
AddStep("bind user score info handler", () => bindHandler(noScores: true));
createResults();
AddAssert("no scores visible", () => !resultsScreen.ScorePanelList.GetScorePanels().Any());
AddAssert("placeholder shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.EqualTo(1));
}
private void createResults(Func<ScoreInfo> getScore = null)
{
AddStep("load results", () =>
@ -169,7 +216,6 @@ namespace osu.Game.Tests.Visual.Playlists
});
AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded);
waitForDisplay();
}
private void waitForDisplay()
@ -183,7 +229,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddWaitStep("wait for display", 5);
}
private void bindHandler(bool delayed = false, ScoreInfo userScore = null, bool failRequests = false) => ((DummyAPIAccess)API).HandleRequest = request =>
private void bindHandler(bool delayed = false, ScoreInfo userScore = null, bool failRequests = false, bool noScores = false) => ((DummyAPIAccess)API).HandleRequest = request =>
{
// pre-check for requests we should be handling (as they are scheduled below).
switch (request)
@ -219,7 +265,7 @@ namespace osu.Game.Tests.Visual.Playlists
break;
case IndexPlaylistScoresRequest i:
triggerSuccess(i, createIndexResponse(i));
triggerSuccess(i, createIndexResponse(i, noScores));
break;
}
}, delay);
@ -301,10 +347,12 @@ namespace osu.Game.Tests.Visual.Playlists
return multiplayerUserScore;
}
private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req)
private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req, bool noScores = false)
{
var result = new IndexedMultiplayerScores();
if (noScores) return result;
string sort = req.IndexParams?.Properties["sort"].ToObject<string>() ?? "score_desc";
for (int i = 1; i <= scores_per_result; i++)

View File

@ -76,6 +76,20 @@ namespace osu.Game.Tests.Visual.SongSelect
assertCollectionDropdownContains("2");
}
[Test]
public void TestCollectionsCleared()
{
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3"))));
AddAssert("check count 5", () => control.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(5));
AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll<BeatmapCollection>()));
AddAssert("check count 2", () => control.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(2));
}
[Test]
public void TestCollectionRemovedFromDropdown()
{

View File

@ -542,10 +542,23 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddAssert("hidden selected", () => getPanelForMod(typeof(OsuModHidden)).Active.Value);
AddAssert("all text selected in textbox", () =>
{
var textBox = modSelectOverlay.ChildrenOfType<SearchTextBox>().Single();
return textBox.SelectedText == textBox.Text;
});
AddStep("press enter again", () => InputManager.Key(Key.Enter));
AddAssert("hidden deselected", () => !getPanelForMod(typeof(OsuModHidden)).Active.Value);
AddStep("apply search matching nothing", () => modSelectOverlay.SearchTerm = "ZZZ");
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddAssert("all text not selected in textbox", () =>
{
var textBox = modSelectOverlay.ChildrenOfType<SearchTextBox>().Single();
return textBox.SelectedText != textBox.Text;
});
AddStep("clear search", () => modSelectOverlay.SearchTerm = string.Empty);
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);

View File

@ -14,7 +14,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
VolumeMeter meter;
MuteButton mute;
Add(meter = new VolumeMeter("MASTER", 125, Color4.Blue) { Position = new Vector2(10) });
Add(meter = new VolumeMeter("MASTER", 125, Color4.Green) { Position = new Vector2(10) });
AddSliderStep("master volume", 0, 10, 0, i => meter.Bindable.Value = i * 0.1);
Add(new VolumeMeter("BIG", 250, Color4.Red)
@ -22,6 +22,15 @@ namespace osu.Game.Tests.Visual.UserInterface
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Position = new Vector2(10),
Margin = new MarginPadding { Left = 250 },
});
Add(new VolumeMeter("SML", 125, Color4.Blue)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Position = new Vector2(10),
Margin = new MarginPadding { Right = 500 },
});
Add(mute = new MuteButton

View File

@ -74,7 +74,7 @@ namespace osu.Game.Collections
}
else
{
foreach (int i in changes.DeletedIndices)
foreach (int i in changes.DeletedIndices.OrderByDescending(i => i))
filters.RemoveAt(i + 1);
foreach (int i in changes.InsertedIndices)

View File

@ -17,6 +17,7 @@ using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods.Input;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;
using osu.Game.Skinning;
@ -191,6 +192,8 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.EditorLimitedDistanceSnap, false);
SetDefault(OsuSetting.EditorShowSpeedChanges, false);
SetDefault(OsuSetting.MultiplayerRoomFilter, RoomPermissionsFilter.All);
SetDefault(OsuSetting.LastProcessedMetadataId, -1);
SetDefault(OsuSetting.ComboColourNormalisationAmount, 0.2f, 0f, 1f, 0.01f);
@ -423,5 +426,6 @@ namespace osu.Game.Configuration
TouchDisableGameplayTaps,
ModSelectTextSearchStartsActive,
UserOnlineStatus,
MultiplayerRoomFilter
}
}

View File

@ -59,7 +59,8 @@ namespace osu.Game.Extensions
/// <returns>A short relative string representing the input time.</returns>
public static string ToShortRelativeTime(this DateTimeOffset time, TimeSpan lowerCutoff)
{
if (time == default)
// covers all `DateTimeOffset` instances with the date portion of 0001-01-01.
if (time.Date == default)
return "-";
var now = DateTime.Now;

View File

@ -95,6 +95,7 @@ namespace osu.Game.Graphics
case HitResult.SmallTickHit:
case HitResult.LargeTickHit:
case HitResult.SliderTailHit:
case HitResult.Great:
return Blue;

View File

@ -5,95 +5,90 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osuTK;
namespace osu.Game.Graphics.Sprites
{
public partial class GlowingSpriteText : Container, IHasText
public partial class GlowingSpriteText : BufferedContainer, IHasText
{
private readonly OsuSpriteText spriteText, blurredText;
private const float blur_sigma = 3f;
// Inflate draw quad to prevent glow from trimming at the edges.
// Padding won't suffice since it will affect text position in cases when it's not centered.
protected override Quad ComputeScreenSpaceDrawQuad() => base.ComputeScreenSpaceDrawQuad().AABBFloat.Inflate(Blur.KernelSize(blur_sigma));
private readonly OsuSpriteText text;
public LocalisableString Text
{
get => spriteText.Text;
set => blurredText.Text = spriteText.Text = value;
get => text.Text;
set => text.Text = value;
}
public FontUsage Font
{
get => spriteText.Font;
set => blurredText.Font = spriteText.Font = value.With(fixedWidth: true);
get => text.Font;
set => text.Font = value.With(fixedWidth: true);
}
public Vector2 TextSize
{
get => spriteText.Size;
set => blurredText.Size = spriteText.Size = value;
get => text.Size;
set => text.Size = value;
}
public ColourInfo TextColour
{
get => spriteText.Colour;
set => spriteText.Colour = value;
get => text.Colour;
set => text.Colour = value;
}
public ColourInfo GlowColour
{
get => blurredText.Colour;
set => blurredText.Colour = value;
get => EffectColour;
set
{
EffectColour = value;
BackgroundColour = value.MultiplyAlpha(0f);
}
}
public Vector2 Spacing
{
get => spriteText.Spacing;
set => spriteText.Spacing = blurredText.Spacing = value;
get => text.Spacing;
set => text.Spacing = value;
}
public bool UseFullGlyphHeight
{
get => spriteText.UseFullGlyphHeight;
set => spriteText.UseFullGlyphHeight = blurredText.UseFullGlyphHeight = value;
get => text.UseFullGlyphHeight;
set => text.UseFullGlyphHeight = value;
}
public Bindable<string> Current
{
get => spriteText.Current;
set => spriteText.Current = value;
get => text.Current;
set => text.Current = value;
}
public GlowingSpriteText()
: base(cachedFrameBuffer: true)
{
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
BlurSigma = new Vector2(blur_sigma);
RedrawOnScale = false;
DrawOriginal = true;
EffectBlending = BlendingParameters.Additive;
EffectPlacement = EffectPlacement.InFront;
Child = text = new OsuSpriteText
{
new BufferedContainer(cachedFrameBuffer: true)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
BlurSigma = new Vector2(4),
RedrawOnScale = false,
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
Size = new Vector2(3f),
Children = new[]
{
blurredText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shadow = false,
},
},
},
spriteText = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shadow = false,
},
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shadow = false,
};
}
}

View File

@ -14,7 +14,7 @@ namespace osu.Game.Graphics.UserInterface
/// </summary>
public partial class PercentageCounter : RollingCounter<double>
{
protected override double RollingDuration => 750;
protected override double RollingDuration => 375;
private float epsilon => 1e-10f;

View File

@ -45,7 +45,7 @@ namespace osu.Game.Graphics.UserInterface
/// <summary>
/// Easing for the counter rollover animation.
/// </summary>
protected virtual Easing RollingEasing => Easing.OutQuint;
protected virtual Easing RollingEasing => Easing.OutQuad;
private T displayedCount;

View File

@ -48,6 +48,8 @@ namespace osu.Game.Graphics.UserInterface
public void KillFocus() => textBox.KillFocus();
public bool SelectAll() => textBox.SelectAll();
public ShearedSearchTextBox()
{
Height = 42;

View File

@ -22,6 +22,7 @@ namespace osu.Game.Input
{
private Bindable<ConfineMouseMode> frameworkConfineMode;
private Bindable<WindowMode> frameworkWindowMode;
private Bindable<bool> frameworkMinimiseOnFocusLossInFullscreen;
private Bindable<OsuConfineMouseMode> osuConfineMode;
private IBindable<bool> localUserPlaying;
@ -31,7 +32,9 @@ namespace osu.Game.Input
{
frameworkConfineMode = frameworkConfigManager.GetBindable<ConfineMouseMode>(FrameworkSetting.ConfineMouseMode);
frameworkWindowMode = frameworkConfigManager.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
frameworkMinimiseOnFocusLossInFullscreen = frameworkConfigManager.GetBindable<bool>(FrameworkSetting.MinimiseOnFocusLossInFullscreen);
frameworkWindowMode.BindValueChanged(_ => updateConfineMode());
frameworkMinimiseOnFocusLossInFullscreen.BindValueChanged(_ => updateConfineMode());
osuConfineMode = osuConfigManager.GetBindable<OsuConfineMouseMode>(OsuSetting.ConfineMouseMode);
localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy();
@ -46,7 +49,8 @@ namespace osu.Game.Input
if (frameworkConfineMode.Disabled)
return;
if (frameworkWindowMode.Value == WindowMode.Fullscreen)
// override confine mode only when clicking outside the window minimises it.
if (frameworkWindowMode.Value == WindowMode.Fullscreen && frameworkMinimiseOnFocusLossInFullscreen.Value)
{
frameworkConfineMode.Value = ConfineMouseMode.Fullscreen;
return;

View File

@ -152,9 +152,13 @@ namespace osu.Game.Localisation
/// <summary>
/// "In order to change the renderer, the game will close. Please open it again."
/// </summary>
public static LocalisableString ChangeRendererConfirmation =>
new TranslatableString(getKey(@"change_renderer_configuration"), @"In order to change the renderer, the game will close. Please open it again.");
public static LocalisableString ChangeRendererConfirmation => new TranslatableString(getKey(@"change_renderer_configuration"), @"In order to change the renderer, the game will close. Please open it again.");
private static string getKey(string key) => $"{prefix}:{key}";
/// <summary>
/// "Minimise osu! when switching to another app"
/// </summary>
public static LocalisableString MinimiseOnFocusLoss => new TranslatableString(getKey(@"minimise_on_focus_loss"), @"Minimise osu! when switching to another app");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -0,0 +1,24 @@
// 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 osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class PlayerSettingsOverlayStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.PlaybackSettings";
/// <summary>
/// "Seek backward {0} seconds"
/// </summary>
public static LocalisableString SeekBackwardSeconds(double arg0) => new TranslatableString(getKey(@"seek_backward_seconds"), @"Seek backward {0} seconds", arg0);
/// <summary>
/// "Seek forward {0} seconds"
/// </summary>
public static LocalisableString SeekForwardSeconds(double arg0) => new TranslatableString(getKey(@"seek_forward_seconds"), @"Seek forward {0} seconds", arg0);
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -53,6 +53,7 @@ namespace osu.Game.Online.API
public IBindable<APIUser> LocalUser => localUser;
public IBindableList<APIUser> Friends => friends;
public IBindable<UserActivity> Activity => activity;
public IBindable<UserStatistics> Statistics => statistics;
public Language Language => game.CurrentLanguage.Value;
@ -65,6 +66,8 @@ namespace osu.Game.Online.API
private Bindable<UserStatus?> configStatus { get; } = new Bindable<UserStatus?>();
private Bindable<UserStatus?> localUserStatus { get; } = new Bindable<UserStatus?>();
private Bindable<UserStatistics> statistics { get; } = new Bindable<UserStatistics>();
protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password));
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
@ -517,9 +520,21 @@ namespace osu.Game.Online.API
flushQueue();
}
public void UpdateStatistics(UserStatistics newStatistics)
{
statistics.Value = newStatistics;
if (IsLoggedIn)
localUser.Value.Statistics = newStatistics;
}
private static APIUser createGuestUser() => new GuestUser();
private void setLocalUser(APIUser user) => Scheduler.Add(() => localUser.Value = user, false);
private void setLocalUser(APIUser user) => Scheduler.Add(() =>
{
localUser.Value = user;
statistics.Value = user.Statistics;
}, false);
protected override void Dispose(bool isDisposing)
{

View File

@ -28,6 +28,8 @@ namespace osu.Game.Online.API
public Bindable<UserActivity> Activity { get; } = new Bindable<UserActivity>();
public Bindable<UserStatistics?> Statistics { get; } = new Bindable<UserStatistics?>();
public Language Language => Language.en;
public string AccessToken => "token";
@ -115,6 +117,12 @@ namespace osu.Game.Online.API
Id = DUMMY_USER_ID,
};
Statistics.Value = new UserStatistics
{
GlobalRank = 1,
CountryRank = 1
};
state.Value = APIState.Online;
}
@ -126,6 +134,14 @@ namespace osu.Game.Online.API
LocalUser.Value = new GuestUser();
}
public void UpdateStatistics(UserStatistics newStatistics)
{
Statistics.Value = newStatistics;
if (IsLoggedIn)
LocalUser.Value.Statistics = newStatistics;
}
public IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null;
public NotificationsClientConnector GetNotificationsConnector() => new PollingNotificationsClientConnector(this);
@ -141,6 +157,7 @@ namespace osu.Game.Online.API
IBindable<APIUser> IAPIProvider.LocalUser => LocalUser;
IBindableList<APIUser> IAPIProvider.Friends => Friends;
IBindable<UserActivity> IAPIProvider.Activity => Activity;
IBindable<UserStatistics?> IAPIProvider.Statistics => Statistics;
/// <summary>
/// During the next simulated login, the process will fail immediately.

View File

@ -28,6 +28,11 @@ namespace osu.Game.Online.API
/// </summary>
IBindable<UserActivity> Activity { get; }
/// <summary>
/// The current user's online statistics.
/// </summary>
IBindable<UserStatistics?> Statistics { get; }
/// <summary>
/// The language supplied by this provider to API requests.
/// </summary>
@ -111,6 +116,11 @@ namespace osu.Game.Online.API
/// </summary>
void Logout();
/// <summary>
/// Sets Statistics bindable.
/// </summary>
void UpdateStatistics(UserStatistics newStatistics);
/// <summary>
/// Constructs a new <see cref="IHubClientConnector"/>. May be null if not supported.
/// </summary>

View File

@ -164,7 +164,8 @@ namespace osu.Game.Online.Leaderboards
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
AutoSizeAxes = Axes.Both,
AutoSizeAxes = Axes.X,
Height = 28,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10f, 0f),
Children = new Drawable[]
@ -357,14 +358,12 @@ namespace osu.Game.Online.Leaderboards
},
},
},
new GlowingSpriteText
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
TextColour = Color4.White,
GlowColour = Color4Extensions.FromHex(@"83ccfa"),
Text = statistic.Value,
Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold),
Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold, fixedWidth: true)
},
},
};

View File

@ -127,6 +127,8 @@ namespace osu.Game.Online.Solo
{
string rulesetName = callback.Score.Ruleset.ShortName;
api.UpdateStatistics(updatedStatistics);
if (latestStatistics == null)
return;

View File

@ -30,7 +30,6 @@ namespace osu.Game.Overlays.Login
[Resolved]
private OsuColour colours { get; set; } = null!;
private UserGridPanel panel = null!;
private UserDropdown dropdown = null!;
/// <summary>
@ -39,6 +38,7 @@ namespace osu.Game.Overlays.Login
public Action? RequestHide;
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
private readonly Bindable<UserStatus?> userStatus = new Bindable<UserStatus?>();
[Resolved]
private IAPIProvider api { get; set; } = null!;
@ -131,7 +131,7 @@ namespace osu.Game.Overlays.Login
Text = LoginPanelStrings.SignedIn,
Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold),
},
panel = new UserGridPanel(api.LocalUser.Value)
new UserRankPanel(api.LocalUser.Value)
{
RelativeSizeAxes = Axes.X,
Action = RequestHide
@ -140,10 +140,8 @@ namespace osu.Game.Overlays.Login
},
};
panel.Status.BindTo(api.LocalUser.Value.Status);
panel.Activity.BindTo(api.LocalUser.Value.Activity);
panel.Status.BindValueChanged(_ => updateDropdownCurrent(), true);
userStatus.BindTo(api.LocalUser.Value.Status);
userStatus.BindValueChanged(e => updateDropdownCurrent(e.NewValue), true);
dropdown.Current.BindValueChanged(action =>
{
@ -176,9 +174,9 @@ namespace osu.Game.Overlays.Login
ScheduleAfterChildren(() => GetContainingInputManager()?.ChangeFocus(form));
});
private void updateDropdownCurrent()
private void updateDropdownCurrent(UserStatus? status)
{
switch (panel.Status.Value)
switch (status)
{
case UserStatus.Online:
dropdown.Current.Value = UserAction.Online;

View File

@ -196,7 +196,7 @@ namespace osu.Game.Overlays.Mods
private partial class BPMDisplay : RollingCounter<double>
{
protected override double RollingDuration => 500;
protected override double RollingDuration => 250;
protected override LocalisableString FormatCount(double count) => count.ToLocalisableString("0 BPM");

View File

@ -447,7 +447,7 @@ namespace osu.Game.Overlays.Mods
private void filterMods()
{
foreach (var modState in AllAvailableMods)
modState.ValidForSelection.Value = modState.Mod.HasImplementation && IsValidMod.Invoke(modState.Mod);
modState.ValidForSelection.Value = modState.Mod.Type != ModType.System && modState.Mod.HasImplementation && IsValidMod.Invoke(modState.Mod);
}
private void updateMultiplier()
@ -719,7 +719,10 @@ namespace osu.Game.Overlays.Mods
ModState? firstMod = columnFlow.Columns.OfType<ModColumn>().FirstOrDefault(m => m.IsPresent)?.AvailableMods.FirstOrDefault(x => x.Visible);
if (firstMod is not null)
{
firstMod.Active.Value = !firstMod.Active.Value;
SearchTextBox.SelectAll();
}
return true;
}

View File

@ -145,7 +145,7 @@ namespace osu.Game.Overlays.Mods
private partial class EffectCounter : RollingCounter<double>
{
protected override double RollingDuration => 500;
protected override double RollingDuration => 250;
protected override LocalisableString FormatCount(double count) => ModUtils.FormatScoreMultiplier(count);

View File

@ -41,8 +41,8 @@ namespace osu.Game.Overlays.Mods
private void updateEnabledState()
{
Enabled.Value = availableMods.Value
.Where(pair => pair.Key != ModType.System)
.SelectMany(pair => pair.Value)
.Where(modState => modState.ValidForSelection.Value)
.Any(modState => !modState.Active.Value && modState.Visible);
}
}

View File

@ -124,7 +124,7 @@ namespace osu.Game.Overlays.Mods
private partial class EffectCounter : RollingCounter<double>
{
protected override double RollingDuration => 500;
protected override double RollingDuration => 250;
protected override LocalisableString FormatCount(double count) => count.ToLocalisableString("0.0#");

View File

@ -51,6 +51,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
private SettingsDropdown<Size> resolutionDropdown = null!;
private SettingsDropdown<Display> displayDropdown = null!;
private SettingsDropdown<WindowMode> windowModeDropdown = null!;
private SettingsCheckbox minimiseOnFocusLossCheckbox = null!;
private SettingsCheckbox safeAreaConsiderationsCheckbox = null!;
private Bindable<float> scalingPositionX = null!;
@ -106,6 +107,12 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
ItemSource = resolutions,
Current = sizeFullscreen
},
minimiseOnFocusLossCheckbox = new SettingsCheckbox
{
LabelText = GraphicsSettingsStrings.MinimiseOnFocusLoss,
Current = config.GetBindable<bool>(FrameworkSetting.MinimiseOnFocusLossInFullscreen),
Keywords = new[] { "alt-tab", "minimize", "focus", "hide" },
},
safeAreaConsiderationsCheckbox = new SettingsCheckbox
{
LabelText = "Shrink game to avoid cameras and notches",
@ -255,6 +262,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
{
resolutionDropdown.CanBeShown.Value = resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen;
displayDropdown.CanBeShown.Value = displayDropdown.Items.Count() > 1;
minimiseOnFocusLossCheckbox.CanBeShown.Value = RuntimeInfo.IsDesktop && windowModeDropdown.Current.Value == WindowMode.Fullscreen;
safeAreaConsiderationsCheckbox.CanBeShown.Value = host.Window?.SafeAreaPadding.Value.Total != Vector2.Zero;
}

View File

@ -140,7 +140,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
/// <param name="fullState">A <see cref="KeyCombination"/> generated from the full input state.</param>
/// <param name="triggerKey">The key which triggered this update, and should be used as the binding.</param>
public void UpdateKeyCombination(KeyCombination fullState, InputKey triggerKey) =>
UpdateKeyCombination(new KeyCombination(fullState.Keys.Where(KeyCombination.IsModifierKey).Append(triggerKey)));
// TODO: Distinct() can be removed after https://github.com/ppy/osu-framework/pull/6130 is merged.
UpdateKeyCombination(new KeyCombination(fullState.Keys.Where(KeyCombination.IsModifierKey).Append(triggerKey).Distinct().ToArray()));
public void UpdateKeyCombination(KeyCombination newCombination)
{

View File

@ -28,6 +28,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
private Bindable<double> localSensitivity;
private Bindable<WindowMode> windowMode;
private Bindable<bool> minimiseOnFocusLoss;
private SettingsEnumDropdown<OsuConfineMouseMode> confineMouseModeSetting;
private Bindable<bool> relativeMode;
@ -47,6 +48,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
relativeMode = mouseHandler.UseRelativeMode.GetBoundCopy();
windowMode = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
minimiseOnFocusLoss = config.GetBindable<bool>(FrameworkSetting.MinimiseOnFocusLossInFullscreen);
Children = new Drawable[]
{
@ -98,21 +100,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
localSensitivity.BindValueChanged(val => handlerSensitivity.Value = val.NewValue);
windowMode.BindValueChanged(mode =>
{
bool isFullscreen = mode.NewValue == WindowMode.Fullscreen;
if (isFullscreen)
{
confineMouseModeSetting.Current.Disabled = true;
confineMouseModeSetting.TooltipText = MouseSettingsStrings.NotApplicableFullscreen;
}
else
{
confineMouseModeSetting.Current.Disabled = false;
confineMouseModeSetting.TooltipText = string.Empty;
}
}, true);
windowMode.BindValueChanged(_ => updateConfineMouseModeSettingVisibility());
minimiseOnFocusLoss.BindValueChanged(_ => updateConfineMouseModeSettingVisibility(), true);
highPrecisionMouse.Current.BindValueChanged(highPrecision =>
{
@ -126,6 +115,25 @@ namespace osu.Game.Overlays.Settings.Sections.Input
}, true);
}
/// <summary>
/// Updates disabled state and tooltip of <see cref="confineMouseModeSetting"/> to match when <see cref="ConfineMouseTracker"/> is overriding the confine mode.
/// </summary>
private void updateConfineMouseModeSettingVisibility()
{
bool confineModeOverriden = windowMode.Value == WindowMode.Fullscreen && minimiseOnFocusLoss.Value;
if (confineModeOverriden)
{
confineMouseModeSetting.Current.Disabled = true;
confineMouseModeSetting.TooltipText = MouseSettingsStrings.NotApplicableFullscreen;
}
else
{
confineMouseModeSetting.Current.Disabled = false;
confineMouseModeSetting.TooltipText = string.Empty;
}
}
public partial class SensitivitySetting : SettingsSlider<double, SensitivitySlider>
{
public SensitivitySetting()

View File

@ -47,7 +47,7 @@ namespace osu.Game.Overlays.SkinEditor
protected override bool StartHidden => true;
private Drawable targetScreen = null!;
private Drawable? targetScreen;
private OsuTextFlowContainer headerText = null!;
@ -541,8 +541,14 @@ namespace osu.Game.Overlays.SkinEditor
if (!hasBegunMutating)
return;
if (targetScreen?.IsLoaded != true)
return;
SkinComponentsContainer[] targetContainers = availableTargets.ToArray();
if (!targetContainers.All(c => c.ComponentsLoaded))
return;
foreach (var t in targetContainers)
currentSkin.Value.UpdateDrawableTarget(t);

View File

@ -136,10 +136,15 @@ namespace osu.Game.Overlays.SkinEditor
globallyReenableBeatmapSkinSetting();
}
public void PresentGameplay()
public void PresentGameplay() => presentGameplay(false);
private void presentGameplay(bool attemptedBeatmapSwitch)
{
performer?.PerformFromScreen(screen =>
{
if (State.Value != Visibility.Visible)
return;
if (beatmap.Value is DummyWorkingBeatmap)
{
// presume we don't have anything good to play and just bail.
@ -149,8 +154,12 @@ namespace osu.Game.Overlays.SkinEditor
// If we're playing the intro, switch away to another beatmap.
if (beatmap.Value.BeatmapSetInfo.Protected)
{
music.NextTrack();
Schedule(PresentGameplay);
if (!attemptedBeatmapSwitch)
{
music.NextTrack();
Schedule(() => presentGameplay(true));
}
return;
}

View File

@ -26,6 +26,8 @@ namespace osu.Game.Overlays.Toolbar
{
public abstract partial class ToolbarButton : OsuClickableContainer, IKeyBindingHandler<GlobalAction>
{
public const float PADDING = 3;
protected GlobalAction? Hotkey { get; set; }
public void SetIcon(Drawable icon)
@ -63,6 +65,7 @@ namespace osu.Game.Overlays.Toolbar
protected virtual Anchor TooltipAnchor => Anchor.TopLeft;
protected readonly Container ButtonContent;
protected ConstrainedIconContainer IconContainer;
protected SpriteText DrawableText;
protected Box HoverBackground;
@ -80,59 +83,66 @@ namespace osu.Game.Overlays.Toolbar
protected ToolbarButton()
{
Width = Toolbar.HEIGHT;
AutoSizeAxes = Axes.X;
RelativeSizeAxes = Axes.Y;
Padding = new MarginPadding(3);
Children = new Drawable[]
{
BackgroundContent = new Container
ButtonContent = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 6,
CornerExponent = 3f,
Children = new Drawable[]
{
HoverBackground = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(80).Opacity(180),
Blending = BlendingParameters.Additive,
Alpha = 0,
},
flashBackground = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Colour = Color4.White.Opacity(100),
Blending = BlendingParameters.Additive,
},
}
},
Flow = new FillFlowContainer
{
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Padding = new MarginPadding { Left = Toolbar.HEIGHT / 2, Right = Toolbar.HEIGHT / 2 },
Width = Toolbar.HEIGHT,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Padding = new MarginPadding(PADDING),
Children = new Drawable[]
{
IconContainer = new ConstrainedIconContainer
BackgroundContent = new Container
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(20),
Alpha = 0,
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 6,
CornerExponent = 3f,
Children = new Drawable[]
{
HoverBackground = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(80).Opacity(180),
Blending = BlendingParameters.Additive,
Alpha = 0,
},
flashBackground = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Colour = Color4.White.Opacity(100),
Blending = BlendingParameters.Additive,
},
}
},
DrawableText = new OsuSpriteText
Flow = new FillFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Padding = new MarginPadding { Left = Toolbar.HEIGHT / 2, Right = Toolbar.HEIGHT / 2 },
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Children = new Drawable[]
{
IconContainer = new ConstrainedIconContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(20),
Alpha = 0,
},
DrawableText = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
},
},
},
},

View File

@ -42,52 +42,59 @@ namespace osu.Game.Overlays.Toolbar
clockDisplayMode = config.GetBindable<ToolbarClockDisplayMode>(OsuSetting.ToolbarClockDisplayMode);
prefer24HourTime = config.GetBindable<bool>(OsuSetting.Prefer24HourTime);
Padding = new MarginPadding(3);
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 6,
CornerExponent = 3f,
Children = new Drawable[]
{
hoverBackground = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(80).Opacity(180),
Blending = BlendingParameters.Additive,
Alpha = 0,
},
flashBackground = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Colour = Color4.White.Opacity(100),
Blending = BlendingParameters.Additive,
},
}
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Padding = new MarginPadding(10),
Padding = new MarginPadding(ToolbarButton.PADDING),
Children = new Drawable[]
{
analog = new AnalogClockDisplay
new Container
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 6,
CornerExponent = 3f,
Children = new Drawable[]
{
hoverBackground = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(80).Opacity(180),
Blending = BlendingParameters.Additive,
Alpha = 0,
},
flashBackground = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Colour = Color4.White.Opacity(100),
Blending = BlendingParameters.Additive,
},
}
},
digital = new DigitalClockDisplay
new FillFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Padding = new MarginPadding(10),
Children = new Drawable[]
{
analog = new AnalogClockDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
digital = new DigitalClockDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
}
}
}
}
}

View File

@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Toolbar
{
public ToolbarHomeButton()
{
Width *= 1.4f;
ButtonContent.Width *= 1.4f;
Hotkey = GlobalAction.Home;
}

View File

@ -28,7 +28,7 @@ namespace osu.Game.Overlays.Toolbar
public ToolbarMusicButton()
{
Hotkey = GlobalAction.ToggleNowPlaying;
AutoSizeAxes = Axes.X;
ButtonContent.AutoSizeAxes = Axes.X;
}
[BackgroundDependencyLoader(true)]

View File

@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Toolbar
public RulesetButton()
{
Padding = new MarginPadding(3)
ButtonContent.Padding = new MarginPadding(PADDING)
{
Bottom = 5
};

View File

@ -10,7 +10,7 @@ namespace osu.Game.Overlays.Toolbar
{
public ToolbarSettingsButton()
{
Width *= 1.4f;
ButtonContent.Width *= 1.4f;
Hotkey = GlobalAction.ToggleSettings;
}

View File

@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Toolbar
public ToolbarUserButton()
{
AutoSizeAxes = Axes.X;
ButtonContent.AutoSizeAxes = Axes.X;
}
[BackgroundDependencyLoader]

View File

@ -313,6 +313,33 @@ namespace osu.Game.Overlays.Volume
private void resetAcceleration() => accelerationModifier = 1;
private float dragDelta;
protected override bool OnDragStart(DragStartEvent e)
{
dragDelta = 0;
adjustFromDrag(e.Delta);
return true;
}
protected override void OnDrag(DragEvent e)
{
adjustFromDrag(e.Delta);
base.OnDrag(e);
}
private void adjustFromDrag(Vector2 delta)
{
const float mouse_drag_divisor = 200;
dragDelta += delta.Y / mouse_drag_divisor;
if (Math.Abs(dragDelta) < 0.01) return;
Volume -= dragDelta;
dragDelta = 0;
}
private void adjust(double delta, bool isPrecise)
{
if (delta == 0)

View File

@ -1,7 +1,6 @@
// 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;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
@ -27,9 +26,6 @@ namespace osu.Game.Rulesets.Edit
[Resolved]
private EditorBeatmap beatmap { get; set; } = null!;
[Resolved]
private EditorClock editorClock { get; set; } = null!;
public DrawableEditorRulesetWrapper(DrawableRuleset<TObject> drawableRuleset)
{
this.drawableRuleset = drawableRuleset;
@ -42,6 +38,7 @@ namespace osu.Game.Rulesets.Edit
[BackgroundDependencyLoader]
private void load()
{
drawableRuleset.FrameStablePlayback = false;
Playfield.DisplayJudgements.Value = false;
}
@ -68,22 +65,6 @@ namespace osu.Game.Rulesets.Edit
Scheduler.AddOnce(regenerateAutoplay);
}
protected override void Update()
{
base.Update();
// Whenever possible, we want to stay in frame stability playback.
// Without doing so, we run into bugs with some gameplay elements not behaving as expected.
//
// Note that this is not using EditorClock.IsSeeking as that would exit frame stability
// on all seeks. The intention here is to retain frame stability for small seeks.
//
// I still think no gameplay elements should require frame stability in the first place, but maybe that ship has sailed already..
bool shouldBypassFrameStability = Math.Abs(drawableRuleset.FrameStableClock.CurrentTime - editorClock.CurrentTime) > 1000;
drawableRuleset.FrameStablePlayback = !shouldBypassFrameStability;
}
private void regenerateAutoplay()
{
var autoplayMod = drawableRuleset.Mods.OfType<ModAutoplay>().Single();

View File

@ -94,6 +94,11 @@ namespace osu.Game.Rulesets.Judgements
/// </summary>
public bool IsHit => Type.IsHit();
/// <summary>
/// The increase in health resulting from this judgement result.
/// </summary>
public double HealthIncrease => Judgement.HealthIncreaseFor(this);
/// <summary>
/// Creates a new <see cref="JudgementResult"/>.
/// </summary>

View File

@ -57,5 +57,40 @@ namespace osu.Game.Rulesets.Objects.Legacy
return (float)(1.0f - 0.7f * IBeatmapDifficultyInfo.DifficultyRange(circleSize)) / 2 * (applyFudge ? broken_gamefield_rounding_allowance : 1);
}
public static int CalculateDifficultyPeppyStars(BeatmapDifficulty difficulty, int objectCount, int drainLength)
{
/*
* WARNING: DO NOT TOUCH IF YOU DO NOT KNOW WHAT YOU ARE DOING
*
* It so happens that in stable, due to .NET Framework internals, float math would be performed
* using x87 registers and opcodes.
* .NET (Core) however uses SSE instructions on 32- and 64-bit words.
* x87 registers are _80 bits_ wide. Which is notably wider than _both_ float and double.
* Therefore, on a significant number of beatmaps, the rounding would not produce correct values.
*
* Thus, to crudely - but, seemingly *mostly* accurately, after checking across all ranked maps - emulate this,
* use `decimal`, which is slow, but has bigger precision than `double`.
* At the time of writing, there is _one_ ranked exception to this - namely https://osu.ppy.sh/beatmapsets/1156087#osu/2625853 -
* but it is considered an "acceptable casualty", since in that case scores aren't inflated by _that_ much compared to others.
*/
decimal objectToDrainRatio = drainLength != 0
? Math.Clamp((decimal)objectCount / drainLength * 8, 0, 16)
: 16;
/*
* Notably, THE `double` CASTS BELOW ARE IMPORTANT AND MUST REMAIN.
* Their goal is to trick the compiler / runtime into NOT promoting from single-precision float, as doing so would prompt it
* to attempt to "silently" fix the single-precision values when converting to decimal,
* which is NOT what the x87 FPU does.
*/
decimal drainRate = (decimal)(double)difficulty.DrainRate;
decimal overallDifficulty = (decimal)(double)difficulty.OverallDifficulty;
decimal circleSize = (decimal)(double)difficulty.CircleSize;
return (int)Math.Round((drainRate + overallDifficulty + circleSize + objectToDrainRatio) / 38 * 5);
}
}
}

View File

@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
/// <param name="result">The <see cref="JudgementResult"/>.</param>
/// <returns>The health increase.</returns>
protected virtual double GetHealthIncreaseFor(JudgementResult result) => result.Judgement.HealthIncreaseFor(result);
protected virtual double GetHealthIncreaseFor(JudgementResult result) => result.HealthIncrease;
/// <summary>
/// The default conditions for failing.

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
[Description(@"")]
[EnumMember(Value = "none")]
[Order(14)]
[Order(15)]
None,
/// <summary>
@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Scoring
/// Indicates small tick miss.
/// </summary>
[EnumMember(Value = "small_tick_miss")]
[Order(11)]
[Order(12)]
SmallTickMiss,
/// <summary>
@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
[EnumMember(Value = "large_tick_miss")]
[Description("-")]
[Order(10)]
[Order(11)]
LargeTickMiss,
/// <summary>
@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
[Description("S Bonus")]
[EnumMember(Value = "small_bonus")]
[Order(9)]
[Order(10)]
SmallBonus,
/// <summary>
@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
[Description("L Bonus")]
[EnumMember(Value = "large_bonus")]
[Order(8)]
[Order(9)]
LargeBonus,
/// <summary>
@ -119,14 +119,14 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
[EnumMember(Value = "ignore_miss")]
[Description("-")]
[Order(13)]
[Order(14)]
IgnoreMiss,
/// <summary>
/// Indicates a hit that should be ignored for scoring purposes.
/// </summary>
[EnumMember(Value = "ignore_hit")]
[Order(12)]
[Order(13)]
IgnoreHit,
/// <summary>
@ -136,14 +136,14 @@ namespace osu.Game.Rulesets.Scoring
/// May be paired with <see cref="IgnoreHit"/>.
/// </remarks>
[EnumMember(Value = "combo_break")]
[Order(15)]
[Order(16)]
ComboBreak,
/// <summary>
/// A special judgement similar to <see cref="LargeTickHit"/> that's used to increase the valuation of the final tick of a slider.
/// </summary>
[EnumMember(Value = "slider_tail_hit")]
[Order(16)]
[Order(8)]
SliderTailHit,
/// <summary>

View File

@ -186,16 +186,7 @@ namespace osu.Game.Rulesets.Scoring
Ruleset = ruleset;
Combo.ValueChanged += combo => HighestCombo.Value = Math.Max(HighestCombo.Value, combo.NewValue);
Accuracy.ValueChanged += accuracy =>
{
// Once failed, we shouldn't update the rank anymore.
if (rank.Value == ScoreRank.F)
return;
rank.Value = RankFromAccuracy(accuracy.NewValue);
foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>())
rank.Value = mod.AdjustRank(Rank.Value, accuracy.NewValue);
};
Accuracy.ValueChanged += _ => updateRank();
Mods.ValueChanged += mods =>
{
@ -205,6 +196,7 @@ namespace osu.Game.Rulesets.Scoring
scoreMultiplier *= m.ScoreMultiplier;
updateScore();
updateRank();
};
}
@ -372,6 +364,17 @@ namespace osu.Game.Rulesets.Scoring
TotalScore.Value = (long)Math.Round(ComputeTotalScore(comboProgress, accuracyProcess, currentBonusPortion) * scoreMultiplier);
}
private void updateRank()
{
// Once failed, we shouldn't update the rank anymore.
if (rank.Value == ScoreRank.F)
return;
rank.Value = RankFromAccuracy(Accuracy.Value);
foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>())
rank.Value = mod.AdjustRank(Rank.Value, Accuracy.Value);
}
protected virtual double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
{
return 500000 * Accuracy.Value * comboProgress +
@ -417,8 +420,8 @@ namespace osu.Game.Rulesets.Scoring
TotalScore.Value = 0;
Accuracy.Value = 1;
Combo.Value = 0;
rank.Value = ScoreRank.X;
HighestCombo.Value = 0;
updateRank();
}
/// <summary>

View File

@ -0,0 +1,77 @@
// 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;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.UI
{
/// <summary>
/// Handles the task of preparing poolable drawable judgements for gameplay usage.
/// </summary>
/// <typeparam name="T">The drawable judgement type.</typeparam>
public partial class JudgementPooler<T> : CompositeComponent
where T : DrawableJudgement, new()
{
private readonly IDictionary<HitResult, DrawablePool<T>> poolDictionary = new Dictionary<HitResult, DrawablePool<T>>();
private readonly IEnumerable<HitResult> usableHitResults;
private readonly Action<T>? onJudgementInitialLoad;
public JudgementPooler(IEnumerable<HitResult> usableHitResults, Action<T>? onJudgementInitialLoad = null)
{
this.usableHitResults = usableHitResults;
this.onJudgementInitialLoad = onJudgementInitialLoad;
}
public T? Get(HitResult result, Action<T>? setupAction)
{
if (!poolDictionary.TryGetValue(result, out var pool))
return null;
return pool.Get(setupAction);
}
[BackgroundDependencyLoader]
private void load()
{
foreach (HitResult result in usableHitResults)
{
var pool = new DrawableJudgementPool(result, onJudgementInitialLoad);
poolDictionary.Add(result, pool);
AddInternal(pool);
}
}
private partial class DrawableJudgementPool : DrawablePool<T>
{
private readonly HitResult result;
private readonly Action<T>? onLoaded;
public DrawableJudgementPool(HitResult result, Action<T>? onLoaded)
: base(20)
{
this.result = result;
this.onLoaded = onLoaded;
}
protected override T CreateNewDrawable()
{
var judgement = base.CreateNewDrawable();
// just a placeholder to initialise the correct drawable hierarchy for this pool.
judgement.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null);
onLoaded?.Invoke(judgement);
return judgement;
}
}
}
}

View File

@ -167,7 +167,6 @@ namespace osu.Game.Rulesets.UI
var triggers = KeyBindingContainer.DefaultKeyBindings
.Select(b => b.GetAction<T>())
.Distinct()
.OrderBy(action => action)
.Select(action => new KeyCounterActionTrigger<T>(action))
.ToArray();

View File

@ -11,6 +11,7 @@ using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Database;
using osu.Game.IO.Legacy;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Replays;
@ -37,6 +38,7 @@ namespace osu.Game.Scoring.Legacy
};
WorkingBeatmap workingBeatmap;
byte[] compressedScoreInfo = null;
using (SerializationReader sr = new SerializationReader(stream))
{
@ -105,8 +107,6 @@ namespace osu.Game.Scoring.Legacy
else if (version >= 20121008)
scoreInfo.LegacyOnlineID = sr.ReadInt32();
byte[] compressedScoreInfo = null;
if (version >= 30000001)
compressedScoreInfo = sr.ReadByteArray();
@ -130,7 +130,10 @@ namespace osu.Game.Scoring.Legacy
}
}
PopulateAccuracy(score.ScoreInfo);
if (score.ScoreInfo.IsLegacyScore || compressedScoreInfo == null)
PopulateLegacyAccuracyAndRank(score.ScoreInfo);
else
populateLazerAccuracyAndRank(score.ScoreInfo);
// before returning for database import, we must restore the database-sourced BeatmapInfo.
// if not, the clone operation in GetPlayableBeatmap will cause a dereference and subsequent database exception.
@ -174,7 +177,7 @@ namespace osu.Game.Scoring.Legacy
/// Legacy use only.
/// </remarks>
/// <param name="score">The <see cref="ScoreInfo"/> to populate.</param>
public static void PopulateAccuracy(ScoreInfo score)
public static void PopulateLegacyAccuracyAndRank(ScoreInfo score)
{
int countMiss = score.GetCountMiss() ?? 0;
int count50 = score.GetCount50() ?? 0;
@ -273,6 +276,18 @@ namespace osu.Game.Scoring.Legacy
}
}
private void populateLazerAccuracyAndRank(ScoreInfo scoreInfo)
{
scoreInfo.Accuracy = StandardisedScoreMigrationTools.ComputeAccuracy(scoreInfo);
var rank = currentRuleset.CreateScoreProcessor().RankFromAccuracy(scoreInfo.Accuracy);
foreach (var mod in scoreInfo.Mods.OfType<IApplicableToScoreProcessor>())
rank = mod.AdjustRank(rank, scoreInfo.Accuracy);
scoreInfo.Rank = rank;
}
private void readLegacyReplay(Replay replay, StreamReader reader)
{
float lastTime = beatmapOffset;

View File

@ -13,6 +13,7 @@ using osu.Game.Extensions;
using osu.Game.IO.Legacy;
using osu.Game.IO.Serialization;
using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types;
using SharpCompress.Compressors.LZMA;
@ -38,9 +39,13 @@ namespace osu.Game.Scoring.Legacy
/// <item><description>30000009: Fix edge cases in conversion for scores which have 0.0x mod multiplier on stable. Reconvert all scores.</description></item>
/// <item><description>30000010: Fix mania score V1 conversion using score V1 accuracy rather than V2 accuracy. Reconvert all scores.</description></item>
/// <item><description>30000011: Re-do catch scoring to mirror stable Score V2 as closely as feasible. Reconvert all scores.</description></item>
/// <item><description>
/// 30000012: Fix incorrect total score conversion on selected beatmaps after implementing the more correct
/// <see cref="LegacyRulesetExtensions.CalculateDifficultyPeppyStars"/> method. Reconvert all scores.
/// </description></item>
/// </list>
/// </remarks>
public const int LATEST_VERSION = 30000011;
public const int LATEST_VERSION = 30000012;
/// <summary>
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.

View File

@ -51,7 +51,7 @@ namespace osu.Game.Screens.Backgrounds
/// </summary>
public readonly Bindable<float> DimWhenUserSettingsIgnored = new Bindable<float>();
internal readonly IBindable<bool> IsBreakTime = new Bindable<bool>();
internal readonly Bindable<bool> IsBreakTime = new Bindable<bool>();
private readonly DimmableBackground dimmable;

View File

@ -44,11 +44,12 @@ namespace osu.Game.Screens.Edit.Timing
{
BackgroundFlow.Add(new RowBackground(group)
{
Action = () =>
// schedule to give time for any modified focused text box to lose focus and commit changes (e.g. BPM / time signature textboxes) before switching to new point.
Action = () => Schedule(() =>
{
SetSelectedRow(group);
clock.SeekSmoothlyTo(group.Time);
}
})
});
}

View File

@ -165,7 +165,11 @@ namespace osu.Game.Screens.OnlinePlay
{
d.SelectedItem.BindTarget = SelectedItem;
d.RequestDeletion = i => RequestDeletion?.Invoke(i);
d.RequestResults = i => RequestResults?.Invoke(i);
d.RequestResults = i =>
{
SelectedItem.Value = i;
RequestResults?.Invoke(i);
};
d.RequestEdit = i => RequestEdit?.Invoke(i);
d.AllowReordering = AllowReordering;
d.AllowDeletion = AllowDeletion;

View File

@ -118,8 +118,6 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved(CanBeNull = true)]
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
protected override bool ShouldBeConsideredForInput(Drawable child) => AllowReordering || AllowDeletion || !AllowSelection || SelectedItem.Value == Model;
public DrawableRoomPlaylistItem(PlaylistItem item)
: base(item)
{
@ -367,7 +365,7 @@ namespace osu.Game.Screens.OnlinePlay
AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Left = 8, Right = 8 },
},
mainFillFlow = new FillFlowContainer
mainFillFlow = new MainFlow(() => SelectedItem.Value == Model || !AllowSelection)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
@ -670,5 +668,17 @@ namespace osu.Game.Screens.OnlinePlay
public LocalisableString TooltipText => avatar.TooltipText;
}
}
public partial class MainFlow : FillFlowContainer
{
private readonly Func<bool> allowInteraction;
public override bool PropagatePositionalInputSubTree => allowInteraction();
public MainFlow(Func<bool> allowInteraction)
{
this.allowInteraction = allowInteraction;
}
}
}
}

View File

@ -4,8 +4,8 @@
#nullable disable
using System;
using System.Diagnostics;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
@ -19,6 +19,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input;
@ -77,6 +78,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
[CanBeNull]
private LeasedBindable<Room> selectionLease;
[Resolved]
protected OsuConfigManager Config { get; private set; }
private readonly Bindable<FilterCriteria> filter = new Bindable<FilterCriteria>(new FilterCriteria());
private readonly IBindable<bool> operationInProgress = new Bindable<bool>();
private readonly IBindable<bool> isIdle = new BindableBool();

Some files were not shown because too many files have changed in this diff Show More