1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 05:32:54 +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>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.113.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

@ -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,51 @@ 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,
});
}
}
}

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.Tracking.ValueChanged += _ => 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

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

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

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

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

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

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

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()

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

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

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

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

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

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

@ -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)
{
base.OnResuming(e);

View File

@ -36,7 +36,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<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="Sentry" Version="3.40.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->

View File

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