1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 08:23:00 +08:00

Merge branch 'master' into fix-multiplayer-mods-cheesing

This commit is contained in:
Bartłomiej Dach 2024-01-15 12:19:29 +01:00
commit 6dee2860d2
No known key found for this signature in database
36 changed files with 406 additions and 258 deletions

View File

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

View File

@ -30,12 +30,19 @@ namespace osu.Desktop
[STAThread] [STAThread]
public static void Main(string[] args) 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. * WARNING: DO NOT PLACE **ANY** CODE ABOVE THE FOLLOWING BLOCK!
// 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; * 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,
// run Squirrel first, as the app may exit after these run * 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()) if (OperatingSystem.IsWindows())
{ {
var windowsVersion = Environment.OSVersion.Version; var windowsVersion = Environment.OSVersion.Version;
@ -59,6 +66,11 @@ namespace osu.Desktop
setupSquirrel(); 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 // Back up the cwd before DesktopGameHost changes it
string cwd = Environment.CurrentDirectory; string cwd = Environment.CurrentDirectory;

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); moveMouseToObject(() => slider);
AddStep("seek after end", () => EditorClock.Seek(750)); AddStep("seek after end", () => EditorClock.Seek(750));
AddUntilStep("wait for seek", () => !EditorClock.IsSeeking);
AddStep("left click", () => InputManager.Click(MouseButton.Left)); AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddAssert("slider not selected", () => EditorBeatmap.SelectedHitObjects.Count == 0); AddAssert("slider not selected", () => EditorBeatmap.SelectedHitObjects.Count == 0);
AddStep("seek to visible", () => EditorClock.Seek(650)); AddStep("seek to visible", () => EditorClock.Seek(650));
AddUntilStep("wait for seek", () => !EditorClock.IsSeeking);
AddStep("left click", () => InputManager.Click(MouseButton.Left)); AddStep("left click", () => InputManager.Click(MouseButton.Left));
AddUntilStep("slider selected", () => EditorBeatmap.SelectedHitObjects.Single() == slider); 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. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; 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.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 namespace osu.Game.Rulesets.Osu.Tests.Mods
{ {
@ -21,5 +31,51 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
[Test] [Test]
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true }); 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,
});
}
} }
} }

View File

@ -4,20 +4,15 @@
#nullable disable #nullable disable
using System; using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; 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<DragEvent> DragInProgress;
public Action DragEnded; public Action DragEnded;
public List<PathControlPoint> PointsInSegment;
public readonly BindableBool IsSelected = new BindableBool(); public readonly BindableBool IsSelected = new BindableBool();
public readonly PathControlPoint ControlPoint; public readonly PathControlPoint ControlPoint;
@ -56,27 +49,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private IBindable<Vector2> hitObjectPosition; private IBindable<Vector2> hitObjectPosition;
private IBindable<float> hitObjectScale; private IBindable<float> hitObjectScale;
[UsedImplicitly]
private readonly IBindable<int> hitObjectVersion;
public PathControlPointPiece(T hitObject, PathControlPoint controlPoint) public PathControlPointPiece(T hitObject, PathControlPoint controlPoint)
{ {
this.hitObject = hitObject; this.hitObject = hitObject;
ControlPoint = controlPoint; 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; controlPoint.Changed += updateMarkerDisplay;
Origin = Anchor.Centre; Origin = Anchor.Centre;
@ -214,28 +191,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
protected override void OnDragEnd(DragEndEvent e) => DragEnded?.Invoke(); 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> /// <summary>
/// Updates the state of the circular control point marker. /// Updates the state of the circular control point marker.
/// </summary> /// </summary>

View File

@ -14,10 +14,12 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -76,6 +78,50 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
controlPoints.BindTo(hitObject.Path.ControlPoints); 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> /// <summary>
/// Selects the <see cref="PathControlPointPiece{T}"/> corresponding to the given <paramref name="pathControlPoint"/>, /// Selects the <see cref="PathControlPointPiece{T}"/> corresponding to the given <paramref name="pathControlPoint"/>,
/// and deselects all other <see cref="PathControlPointPiece{T}"/>s. /// 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> /// <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) 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) 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. // and one segment of the previous type.
int thirdPointIndex = indexInSegment + 2; int thirdPointIndex = indexInSegment + 2;
if (piece.PointsInSegment.Count > thirdPointIndex + 1) if (pointsInSegment.Count > thirdPointIndex + 1)
piece.PointsInSegment[thirdPointIndex].Type = piece.PointsInSegment[0].Type; pointsInSegment[thirdPointIndex].Type = pointsInSegment[0].Type;
} }
hitObject.Path.ExpectedDistance.Value = null; 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. // 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++) for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++)
hitObject.Path.ControlPoints[i].Type = dragPathTypes[i]; hitObject.Path.ControlPoints[i].Type = dragPathTypes[i];
EnsureValidPathTypes();
} }
public void DragEnded() => changeHandler?.EndChange(); 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)) foreach (var p in Pieces.Where(p => p.IsSelected.Value))
updatePathType(p, type); updatePathType(p, type);
EnsureValidPathTypes();
}); });
if (countOfState == totalCount) if (countOfState == totalCount)

View File

@ -267,6 +267,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
segmentStart.Type = PathType.BEZIER; segmentStart.Type = PathType.BEZIER;
break; break;
} }
controlPointVisualiser.EnsureValidPathTypes();
} }
private void updateCursor() 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 // Move the control points from the insertion index onwards to make room for the insertion
controlPoints.Insert(insertionIndex, pathControlPoint); controlPoints.Insert(insertionIndex, pathControlPoint);
ControlPointVisualiser?.EnsureValidPathTypes();
HitObject.SnapTo(distanceSnapProvider); HitObject.SnapTo(distanceSnapProvider);
return pathControlPoint; return pathControlPoint;
@ -275,6 +277,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
controlPoints.Remove(c); controlPoints.Remove(c);
} }
ControlPointVisualiser?.EnsureValidPathTypes();
// Snap the slider to the current beat divisor before checking length validity. // Snap the slider to the current beat divisor before checking length validity.
HitObject.SnapTo(distanceSnapProvider); HitObject.SnapTo(distanceSnapProvider);

View File

@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public void ApplyToDrawableHitObject(DrawableHitObject drawable) public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{ {
if (drawable is DrawableSlider s) if (drawable is DrawableSlider s)
s.Tracking.ValueChanged += flashlight.OnSliderTrackingChange; s.Tracking.ValueChanged += _ => flashlight.OnSliderTrackingChange(s);
} }
private partial class OsuFlashlight : Flashlight, IRequireHighFrequencyMousePosition private partial class OsuFlashlight : Flashlight, IRequireHighFrequencyMousePosition
@ -66,10 +66,10 @@ namespace osu.Game.Rulesets.Osu.Mods
FlashlightSmoothness = 1.4f; 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. // 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) protected override bool OnMouseMove(MouseMoveEvent e)

View File

@ -10,12 +10,14 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay;
using osu.Game.Utils;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
@ -23,6 +25,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public partial class TestSceneFreeModSelectOverlay : MultiplayerTestScene public partial class TestSceneFreeModSelectOverlay : MultiplayerTestScene
{ {
private FreeModSelectOverlay freeModSelectOverlay; private FreeModSelectOverlay freeModSelectOverlay;
private FooterButtonFreeMods footerButtonFreeMods;
private readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> availableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>(); private readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> availableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -119,11 +122,46 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("select all button enabled", () => this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value); 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() 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", AddUntilStep("all column content loaded",
() => freeModSelectOverlay.ChildrenOfType<ModColumn>().Any() () => freeModSelectOverlay.ChildrenOfType<ModColumn>().Any()
@ -134,10 +172,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
var allAvailableMods = availableMods.Value var allAvailableMods = availableMods.Value
.Where(pair => pair.Key != ModType.System) .Where(pair => pair.Key != ModType.System)
.SelectMany(pair => pair.Value) .SelectMany(pair => ModUtils.FlattenMods(pair.Value))
.Where(mod => mod.UserPlayable && mod.HasImplementation) .Where(mod => mod.UserPlayable && mod.HasImplementation)
.ToList(); .ToList();
if (freeModSelectOverlay.SelectedMods.Value.Count != allAvailableMods.Count)
return false;
foreach (var availableMod in allAvailableMods) foreach (var availableMod in allAvailableMods)
{ {
if (freeModSelectOverlay.SelectedMods.Value.All(selectedMod => selectedMod.GetType() != availableMod.GetType())) if (freeModSelectOverlay.SelectedMods.Value.All(selectedMod => selectedMod.GetType() != availableMod.GetType()))

View File

@ -268,6 +268,26 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("update not received", () => update == null); 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 int nextUserId = 2000;
private long nextScoreId = 50000; private long nextScoreId = 50000;

View File

@ -59,7 +59,8 @@ namespace osu.Game.Extensions
/// <returns>A short relative string representing the input time.</returns> /// <returns>A short relative string representing the input time.</returns>
public static string ToShortRelativeTime(this DateTimeOffset time, TimeSpan lowerCutoff) 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 "-"; return "-";
var now = DateTime.Now; var now = DateTime.Now;

View File

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

View File

@ -22,6 +22,7 @@ namespace osu.Game.Input
{ {
private Bindable<ConfineMouseMode> frameworkConfineMode; private Bindable<ConfineMouseMode> frameworkConfineMode;
private Bindable<WindowMode> frameworkWindowMode; private Bindable<WindowMode> frameworkWindowMode;
private Bindable<bool> frameworkMinimiseOnFocusLossInFullscreen;
private Bindable<OsuConfineMouseMode> osuConfineMode; private Bindable<OsuConfineMouseMode> osuConfineMode;
private IBindable<bool> localUserPlaying; private IBindable<bool> localUserPlaying;
@ -31,7 +32,9 @@ namespace osu.Game.Input
{ {
frameworkConfineMode = frameworkConfigManager.GetBindable<ConfineMouseMode>(FrameworkSetting.ConfineMouseMode); frameworkConfineMode = frameworkConfigManager.GetBindable<ConfineMouseMode>(FrameworkSetting.ConfineMouseMode);
frameworkWindowMode = frameworkConfigManager.GetBindable<WindowMode>(FrameworkSetting.WindowMode); frameworkWindowMode = frameworkConfigManager.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
frameworkMinimiseOnFocusLossInFullscreen = frameworkConfigManager.GetBindable<bool>(FrameworkSetting.MinimiseOnFocusLossInFullscreen);
frameworkWindowMode.BindValueChanged(_ => updateConfineMode()); frameworkWindowMode.BindValueChanged(_ => updateConfineMode());
frameworkMinimiseOnFocusLossInFullscreen.BindValueChanged(_ => updateConfineMode());
osuConfineMode = osuConfigManager.GetBindable<OsuConfineMouseMode>(OsuSetting.ConfineMouseMode); osuConfineMode = osuConfigManager.GetBindable<OsuConfineMouseMode>(OsuSetting.ConfineMouseMode);
localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy(); localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy();
@ -46,7 +49,8 @@ namespace osu.Game.Input
if (frameworkConfineMode.Disabled) if (frameworkConfineMode.Disabled)
return; 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; frameworkConfineMode.Value = ConfineMouseMode.Fullscreen;
return; return;

View File

@ -152,9 +152,13 @@ namespace osu.Game.Localisation
/// <summary> /// <summary>
/// "In order to change the renderer, the game will close. Please open it again." /// "In order to change the renderer, the game will close. Please open it again."
/// </summary> /// </summary>
public static LocalisableString ChangeRendererConfirmation => public static LocalisableString ChangeRendererConfirmation => new TranslatableString(getKey(@"change_renderer_configuration"), @"In order to change the renderer, the game will close. Please open it again.");
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

@ -53,6 +53,7 @@ namespace osu.Game.Online.API
public IBindable<APIUser> LocalUser => localUser; public IBindable<APIUser> LocalUser => localUser;
public IBindableList<APIUser> Friends => friends; public IBindableList<APIUser> Friends => friends;
public IBindable<UserActivity> Activity => activity; public IBindable<UserActivity> Activity => activity;
public IBindable<UserStatistics> Statistics => statistics;
public Language Language => game.CurrentLanguage.Value; 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?> configStatus { get; } = new Bindable<UserStatus?>();
private Bindable<UserStatus?> localUserStatus { 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)); protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password));
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
@ -517,9 +520,21 @@ namespace osu.Game.Online.API
flushQueue(); flushQueue();
} }
public void UpdateStatistics(UserStatistics newStatistics)
{
statistics.Value = newStatistics;
if (IsLoggedIn)
localUser.Value.Statistics = newStatistics;
}
private static APIUser createGuestUser() => new GuestUser(); 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) 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<UserActivity> Activity { get; } = new Bindable<UserActivity>();
public Bindable<UserStatistics?> Statistics { get; } = new Bindable<UserStatistics?>();
public Language Language => Language.en; public Language Language => Language.en;
public string AccessToken => "token"; public string AccessToken => "token";
@ -115,6 +117,12 @@ namespace osu.Game.Online.API
Id = DUMMY_USER_ID, Id = DUMMY_USER_ID,
}; };
Statistics.Value = new UserStatistics
{
GlobalRank = 1,
CountryRank = 1
};
state.Value = APIState.Online; state.Value = APIState.Online;
} }
@ -126,6 +134,14 @@ namespace osu.Game.Online.API
LocalUser.Value = new GuestUser(); 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 IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null;
public NotificationsClientConnector GetNotificationsConnector() => new PollingNotificationsClientConnector(this); public NotificationsClientConnector GetNotificationsConnector() => new PollingNotificationsClientConnector(this);
@ -141,6 +157,7 @@ namespace osu.Game.Online.API
IBindable<APIUser> IAPIProvider.LocalUser => LocalUser; IBindable<APIUser> IAPIProvider.LocalUser => LocalUser;
IBindableList<APIUser> IAPIProvider.Friends => Friends; IBindableList<APIUser> IAPIProvider.Friends => Friends;
IBindable<UserActivity> IAPIProvider.Activity => Activity; IBindable<UserActivity> IAPIProvider.Activity => Activity;
IBindable<UserStatistics?> IAPIProvider.Statistics => Statistics;
/// <summary> /// <summary>
/// During the next simulated login, the process will fail immediately. /// During the next simulated login, the process will fail immediately.

View File

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

View File

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

View File

@ -447,7 +447,7 @@ namespace osu.Game.Overlays.Mods
private void filterMods() private void filterMods()
{ {
foreach (var modState in AllAvailableMods) 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() private void updateMultiplier()

View File

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

View File

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

View File

@ -28,6 +28,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
private Bindable<double> localSensitivity; private Bindable<double> localSensitivity;
private Bindable<WindowMode> windowMode; private Bindable<WindowMode> windowMode;
private Bindable<bool> minimiseOnFocusLoss;
private SettingsEnumDropdown<OsuConfineMouseMode> confineMouseModeSetting; private SettingsEnumDropdown<OsuConfineMouseMode> confineMouseModeSetting;
private Bindable<bool> relativeMode; private Bindable<bool> relativeMode;
@ -47,6 +48,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
relativeMode = mouseHandler.UseRelativeMode.GetBoundCopy(); relativeMode = mouseHandler.UseRelativeMode.GetBoundCopy();
windowMode = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode); windowMode = config.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
minimiseOnFocusLoss = config.GetBindable<bool>(FrameworkSetting.MinimiseOnFocusLossInFullscreen);
Children = new Drawable[] Children = new Drawable[]
{ {
@ -98,21 +100,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
localSensitivity.BindValueChanged(val => handlerSensitivity.Value = val.NewValue); localSensitivity.BindValueChanged(val => handlerSensitivity.Value = val.NewValue);
windowMode.BindValueChanged(mode => windowMode.BindValueChanged(_ => updateConfineMouseModeSettingVisibility());
{ minimiseOnFocusLoss.BindValueChanged(_ => updateConfineMouseModeSettingVisibility(), true);
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);
highPrecisionMouse.Current.BindValueChanged(highPrecision => highPrecisionMouse.Current.BindValueChanged(highPrecision =>
{ {
@ -126,6 +115,25 @@ namespace osu.Game.Overlays.Settings.Sections.Input
}, true); }, 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 partial class SensitivitySetting : SettingsSlider<double, SensitivitySlider>
{ {
public SensitivitySetting() public SensitivitySetting()

View File

@ -26,6 +26,8 @@ namespace osu.Game.Overlays.Toolbar
{ {
public abstract partial class ToolbarButton : OsuClickableContainer, IKeyBindingHandler<GlobalAction> public abstract partial class ToolbarButton : OsuClickableContainer, IKeyBindingHandler<GlobalAction>
{ {
public const float PADDING = 3;
protected GlobalAction? Hotkey { get; set; } protected GlobalAction? Hotkey { get; set; }
public void SetIcon(Drawable icon) public void SetIcon(Drawable icon)
@ -63,6 +65,7 @@ namespace osu.Game.Overlays.Toolbar
protected virtual Anchor TooltipAnchor => Anchor.TopLeft; protected virtual Anchor TooltipAnchor => Anchor.TopLeft;
protected readonly Container ButtonContent;
protected ConstrainedIconContainer IconContainer; protected ConstrainedIconContainer IconContainer;
protected SpriteText DrawableText; protected SpriteText DrawableText;
protected Box HoverBackground; protected Box HoverBackground;
@ -80,59 +83,66 @@ namespace osu.Game.Overlays.Toolbar
protected ToolbarButton() protected ToolbarButton()
{ {
Width = Toolbar.HEIGHT; AutoSizeAxes = Axes.X;
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;
Padding = new MarginPadding(3);
Children = new Drawable[] Children = new Drawable[]
{ {
BackgroundContent = new Container ButtonContent = new Container
{ {
RelativeSizeAxes = Axes.Both, Width = Toolbar.HEIGHT,
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 },
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X, Padding = new MarginPadding(PADDING),
Children = new Drawable[] Children = new Drawable[]
{ {
IconContainer = new ConstrainedIconContainer BackgroundContent = new Container
{ {
Anchor = Anchor.CentreLeft, RelativeSizeAxes = Axes.Both,
Origin = Anchor.CentreLeft, Masking = true,
Size = new Vector2(20), CornerRadius = 6,
Alpha = 0, 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, Direction = FillDirection.Horizontal,
Origin = Anchor.CentreLeft, 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); clockDisplayMode = config.GetBindable<ToolbarClockDisplayMode>(OsuSetting.ToolbarClockDisplayMode);
prefer24HourTime = config.GetBindable<bool>(OsuSetting.Prefer24HourTime); prefer24HourTime = config.GetBindable<bool>(OsuSetting.Prefer24HourTime);
Padding = new MarginPadding(3);
Children = new Drawable[] Children = new Drawable[]
{ {
new Container 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, RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X, AutoSizeAxes = Axes.X,
Direction = FillDirection.Horizontal, Padding = new MarginPadding(ToolbarButton.PADDING),
Spacing = new Vector2(5),
Padding = new MarginPadding(10),
Children = new Drawable[] Children = new Drawable[]
{ {
analog = new AnalogClockDisplay new Container
{ {
Anchor = Anchor.CentreLeft, RelativeSizeAxes = Axes.Both,
Origin = Anchor.CentreLeft, 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, RelativeSizeAxes = Axes.Y,
Origin = Anchor.CentreLeft, 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() public ToolbarHomeButton()
{ {
Width *= 1.4f; ButtonContent.Width *= 1.4f;
Hotkey = GlobalAction.Home; Hotkey = GlobalAction.Home;
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
@ -27,9 +26,6 @@ namespace osu.Game.Rulesets.Edit
[Resolved] [Resolved]
private EditorBeatmap beatmap { get; set; } = null!; private EditorBeatmap beatmap { get; set; } = null!;
[Resolved]
private EditorClock editorClock { get; set; } = null!;
public DrawableEditorRulesetWrapper(DrawableRuleset<TObject> drawableRuleset) public DrawableEditorRulesetWrapper(DrawableRuleset<TObject> drawableRuleset)
{ {
this.drawableRuleset = drawableRuleset; this.drawableRuleset = drawableRuleset;
@ -42,6 +38,7 @@ namespace osu.Game.Rulesets.Edit
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
drawableRuleset.FrameStablePlayback = false;
Playfield.DisplayJudgements.Value = false; Playfield.DisplayJudgements.Value = false;
} }
@ -68,22 +65,6 @@ namespace osu.Game.Rulesets.Edit
Scheduler.AddOnce(regenerateAutoplay); 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() private void regenerateAutoplay()
{ {
var autoplayMod = drawableRuleset.Mods.OfType<ModAutoplay>().Single(); var autoplayMod = drawableRuleset.Mods.OfType<ModAutoplay>().Single();

View File

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

View File

@ -146,14 +146,6 @@ namespace osu.Game.Screens.Select
} }
} }
public override void OnSuspending(ScreenTransitionEvent e)
{
// Scores will be refreshed on arriving at this screen.
// Clear them to avoid animation overload on returning to song select.
playBeatmapDetailArea.Leaderboard.ClearScores();
base.OnSuspending(e);
}
public override void OnResuming(ScreenTransitionEvent e) public override void OnResuming(ScreenTransitionEvent e)
{ {
base.OnResuming(e); base.OnResuming(e);

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="11.5.0" /> <PackageReference Include="Realm" Version="11.5.0" />
<PackageReference Include="ppy.osu.Framework" Version="2024.113.0" /> <PackageReference Include="ppy.osu.Framework" Version="2024.114.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.1228.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2023.1228.0" />
<PackageReference Include="Sentry" Version="3.40.0" /> <PackageReference Include="Sentry" Version="3.40.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. --> <!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->

View File

@ -23,6 +23,6 @@
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier> <RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.113.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2024.114.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>