1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 17:43:05 +08:00

Merge branch 'master' into fix-bpm-differences

This commit is contained in:
Bartłomiej Dach 2024-02-22 10:41:22 +01:00
commit 1a831145ce
No known key found for this signature in database
56 changed files with 850 additions and 473 deletions

View File

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

View File

@ -0,0 +1,48 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using BenchmarkDotNet.Attributes;
using osu.Game.Utils;
namespace osu.Game.Benchmarks
{
public class BenchmarkStringComparison
{
private string[] strings = null!;
[GlobalSetup]
public void GlobalSetUp()
{
strings = new string[10000];
for (int i = 0; i < strings.Length; ++i)
strings[i] = Guid.NewGuid().ToString();
for (int i = 0; i < strings.Length; ++i)
{
if (i % 2 == 0)
strings[i] = strings[i].ToUpperInvariant();
}
}
[Benchmark]
public void OrdinalIgnoreCase() => compare(StringComparer.OrdinalIgnoreCase);
[Benchmark]
public void OrdinalSortByCase() => compare(OrdinalSortByCaseStringComparer.DEFAULT);
[Benchmark]
public void InvariantCulture() => compare(StringComparer.InvariantCulture);
private void compare(IComparer<string> comparer)
{
for (int i = 0; i < strings.Length; ++i)
{
for (int j = i + 1; j < strings.Length; ++j)
_ = comparer.Compare(strings[i], strings[j]);
}
}
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects;
@ -17,9 +18,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
[Resolved]
private Playfield playfield { get; set; } = null!;
[Resolved]
private IScrollingInfo scrollingInfo { get; set; } = null!;
protected ScrollingHitObjectContainer HitObjectContainer => ((ManiaPlayfield)playfield).GetColumn(HitObject.Column).HitObjectContainer;
protected ManiaSelectionBlueprint(T hitObject)
@ -28,14 +26,31 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
RelativeSizeAxes = Axes.None;
}
protected override void Update()
{
base.Update();
private readonly IBindable<ScrollingDirection> directionBindable = new Bindable<ScrollingDirection>();
var anchor = scrollingInfo.Direction.Value == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
directionBindable.BindTo(scrollingInfo.Direction);
}
protected override void LoadComplete()
{
base.LoadComplete();
directionBindable.BindValueChanged(onDirectionChanged, true);
}
private void onDirectionChanged(ValueChangedEvent<ScrollingDirection> direction)
{
var anchor = direction.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre;
Anchor = Origin = anchor;
foreach (var child in InternalChildren)
child.Anchor = child.Origin = anchor;
}
protected override void Update()
{
base.Update();
Position = Parent!.ToLocalSpace(HitObjectContainer.ScreenSpacePositionAtTime(HitObject.StartTime)) - AnchorPosition;
Width = HitObjectContainer.DrawWidth;

View File

@ -7,7 +7,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Mania.Beatmaps;
@ -149,7 +148,18 @@ namespace osu.Game.Rulesets.Mania.UI
/// <summary>
/// Retrieves the total amount of columns across all stages in this playfield.
/// </summary>
public int TotalColumns => stages.Sum(s => s.Columns.Length);
public int TotalColumns
{
get
{
int sum = 0;
foreach (var stage in stages)
sum += stage.Columns.Length;
return sum;
}
}
private Stage getStageByColumn(int column)
{

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;

View File

@ -78,9 +78,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components
Scale = new Vector2(hitObject.Scale);
if (hitObject is IHasComboInformation combo)
ring.BorderColour = combo.GetComboColour(skin);
double editorTime = editorClock.CurrentTime;
double hitObjectTime = hitObject.StartTime;
bool hasReachedObject = editorTime >= hitObjectTime;
@ -92,6 +89,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components
ring.Scale = new Vector2(1 + 0.1f * ringScale);
content.Alpha = 0.9f * (1 - alpha);
// TODO: should only update colour on skin/combo/object change.
if (hitObject is IHasComboInformation combo && content.Alpha > 0)
ring.BorderColour = combo.GetComboColour(skin);
}
else
content.Alpha = 0;

View File

@ -416,8 +416,22 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
DrawableObject.SliderBody?.ToScreenSpace(DrawableObject.SliderBody.PathEndOffset) ?? BodyPiece.ToScreenSpace(BodyPiece.PathEndLocation)
};
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)) == true;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
{
if (BodyPiece.ReceivePositionalInputAt(screenSpacePos))
return true;
if (ControlPointVisualiser == null)
return false;
foreach (var p in ControlPointVisualiser.Pieces)
{
if (p.ReceivePositionalInputAt(screenSpacePos))
return true;
}
return false;
}
protected virtual SliderCircleOverlay CreateCircleOverlay(Slider slider, SliderPosition position) => new SliderCircleOverlay(slider, position);
}

View File

@ -1,13 +1,13 @@
// 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.ComponentModel;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Lists;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu
{
public partial class OsuInputManager : RulesetInputManager<OsuAction>
{
public IEnumerable<OsuAction> PressedActions => KeyBindingContainer.PressedActions;
public SlimReadOnlyListWrapper<OsuAction> PressedActions => KeyBindingContainer.PressedActions;
/// <summary>
/// Whether gameplay input buttons should be allowed.

View File

@ -25,8 +25,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
Texture = textures.Get(@"Gameplay/osu/approachcircle").WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS * 2);
// account for the sprite being used for the default approach circle being taken from stable,
// when hitcircles have 5px padding on each size. this should be removed if we update the sprite.
// In triangles and argon skins, we expanded hitcircles to take up the full 128 px which are clickable,
// but still use the old approach circle sprite. To make it feel correct (ie. disappear as it collides
// with the hitcircle, *not when it overlaps the border*) we need to expand it slightly.
Scale = new Vector2(128 / 118f);
}

View File

@ -8,7 +8,6 @@ using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
@ -26,10 +25,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
var texture = skin.GetTexture(@"approachcircle");
Debug.Assert(texture != null);
Texture = texture.WithMaximumSize(OsuHitObject.OBJECT_DIMENSIONS * 2);
// account for the sprite being used for the default approach circle being taken from stable,
// when hitcircles have 5px padding on each size. this should be removed if we update the sprite.
Scale = new Vector2(128 / 118f);
}
protected override void LoadComplete()

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
// Note that the scale adjust here is 2 instead of DrawableSliderBall.FOLLOW_AREA to match legacy behaviour.
// This means the actual tracking area for gameplay purposes is larger than the sprite (but skins may be accounting for this).
this.ScaleTo(0.5f).ScaleTo(2f, Math.Min(180f, remainingTime), Easing.Out)
this.ScaleTo(1f).ScaleTo(2f, Math.Min(180f, remainingTime), Easing.Out)
.FadeTo(0).FadeTo(1f, Math.Min(60f, remainingTime));
}

View File

@ -8,11 +8,13 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays;
using osu.Game.Overlays.Login;
using osu.Game.Overlays.Settings;
using osu.Game.Users.Drawables;
using osuTK.Input;
@ -25,6 +27,9 @@ namespace osu.Game.Tests.Visual.Menus
private LoginOverlay loginOverlay = null!;
[Resolved]
private OsuConfigManager configManager { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
@ -156,5 +161,36 @@ namespace osu.Game.Tests.Visual.Menus
});
AddAssert("login overlay is hidden", () => loginOverlay.State.Value == Visibility.Hidden);
}
[Test]
public void TestUncheckingRememberUsernameClearsIt()
{
AddStep("logout", () => API.Logout());
AddStep("set username", () => configManager.SetValue(OsuSetting.Username, "test_user"));
AddStep("set remember password", () => configManager.SetValue(OsuSetting.SavePassword, true));
AddStep("uncheck remember username", () =>
{
InputManager.MoveMouseTo(loginOverlay.ChildrenOfType<SettingsCheckbox>().First());
InputManager.Click(MouseButton.Left);
});
AddAssert("remember username off", () => configManager.Get<bool>(OsuSetting.SaveUsername), () => Is.False);
AddAssert("remember password off", () => configManager.Get<bool>(OsuSetting.SavePassword), () => Is.False);
AddAssert("username cleared", () => configManager.Get<string>(OsuSetting.Username), () => Is.Empty);
}
[Test]
public void TestUncheckingRememberPasswordClearsToken()
{
AddStep("logout", () => API.Logout());
AddStep("set token", () => configManager.SetValue(OsuSetting.Token, "test_token"));
AddStep("set remember password", () => configManager.SetValue(OsuSetting.SavePassword, true));
AddStep("uncheck remember token", () =>
{
InputManager.MoveMouseTo(loginOverlay.ChildrenOfType<SettingsCheckbox>().Last());
InputManager.Click(MouseButton.Left);
});
AddAssert("remember password off", () => configManager.Get<bool>(OsuSetting.SavePassword), () => Is.False);
AddAssert("token cleared", () => configManager.Get<string>(OsuSetting.Token), () => Is.Empty);
}
}
}

View File

@ -4,6 +4,7 @@
#nullable disable
using System;
using System.IO;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@ -18,6 +19,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
@ -221,6 +223,67 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("Overlay was shown", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
}
[Test]
public void TestAttemptPlayBeatmapWrongHashFails()
{
Screens.Select.SongSelect songSelect = null;
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely());
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
AddStep("change beatmap files", () =>
{
foreach (var file in Game.Beatmap.Value.BeatmapSetInfo.Files.Where(f => Path.GetExtension(f.Filename) == ".osu"))
{
using (var stream = Game.Storage.GetStream(Path.Combine("files", file.File.GetStoragePath()), FileAccess.ReadWrite))
stream.WriteByte(0);
}
});
AddStep("invalidate cache", () =>
{
((IWorkingBeatmapCache)Game.BeatmapManager).Invalidate(Game.Beatmap.Value.BeatmapSetInfo);
});
AddStep("select next difficulty", () => InputManager.Key(Key.Down));
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for player loader", () => Game.ScreenStack.CurrentScreen is PlayerLoader);
AddUntilStep("wait for song select", () => songSelect.IsCurrentScreen());
}
[Test]
public void TestAttemptPlayBeatmapMissingFails()
{
Screens.Select.SongSelect songSelect = null;
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely());
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
AddStep("delete beatmap files", () =>
{
foreach (var file in Game.Beatmap.Value.BeatmapSetInfo.Files.Where(f => Path.GetExtension(f.Filename) == ".osu"))
Game.Storage.Delete(Path.Combine("files", file.File.GetStoragePath()));
});
AddStep("invalidate cache", () =>
{
((IWorkingBeatmapCache)Game.BeatmapManager).Invalidate(Game.Beatmap.Value.BeatmapSetInfo);
});
AddStep("select next difficulty", () => InputManager.Key(Key.Down));
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for player loader", () => Game.ScreenStack.CurrentScreen is PlayerLoader);
AddUntilStep("wait for song select", () => songSelect.IsCurrentScreen());
}
[Test]
public void TestRetryCountIncrements()
{

View File

@ -9,6 +9,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Threading;
@ -301,6 +302,25 @@ namespace osu.Game.Tests.Visual.Navigation
switchToGameplayScene();
}
[Test]
public void TestRulesetInputDisabledWhenSkinEditorOpen()
{
advanceToSongSelect();
openSkinEditor();
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
switchToGameplayScene();
AddUntilStep("nested input disabled", () => ((Player)Game.ScreenStack.CurrentScreen).ChildrenOfType<PassThroughInputManager>().All(manager => !manager.UseParentInput));
toggleSkinEditor();
AddUntilStep("nested input enabled", () => ((Player)Game.ScreenStack.CurrentScreen).ChildrenOfType<PassThroughInputManager>().Any(manager => manager.UseParentInput));
toggleSkinEditor();
AddUntilStep("nested input disabled", () => ((Player)Game.ScreenStack.CurrentScreen).ChildrenOfType<PassThroughInputManager>().All(manager => !manager.UseParentInput));
}
private void advanceToSongSelect()
{
PushAndConfirm(() => songSelect = new TestPlaySongSelect());

View File

@ -81,16 +81,17 @@ namespace osu.Game.Tests.Visual.Online
},
// Taken from https://github.com/ppy/osu/issues/13993#issuecomment-885994077
new[] { "Problematic", @"My tablet doesn't work :( It's a Huion 420 and it's apparently incompatible with OpenTablet Driver. The warning I get is: ""DeviceInUseException: Device is currently in use by another kernel module. To fix this issue, please follow the instructions from https://github.com/OpenTabletDriver/OpenTabletDriver/wiki/Linux-FAQ#arg umentoutofrangeexception-value-0-15"" and it repeats 4 times on the notification before logging subsequent warnings. Checking the logs, it looks for other Huion tablets before sending the notification (e.g. ""2021-07-23 03:52:33 [verbose]: Detect: Searching for tablet 'Huion WH1409 V2' 20 2021-07-23 03:52:33 [error]: DeviceInUseException: Device is currently in use by another kernel module. To fix this issue, please follow the instructions from https://github.com/OpenTabletDriver/OpenTabletDriver/wiki/Linux-FAQ#arg umentoutofrangeexception-value-0-15"") I use an Arch based installation of Linux and the tablet runs perfectly with Digimend kernel driver, with area configuration, pen pressure, etc. On osu!lazer the cursor disappears until I set it to ""Borderless"" instead of ""Fullscreen"" and even after it shows up, it goes to the bottom left corner as soon as a map starts. I have honestly 0 idea of whats going on at this point.", },
new[]
{
"Problematic", @"My tablet doesn't work :(
It's a Huion 420 and it's apparently incompatible with OpenTablet Driver. The warning I get is: ""DeviceInUseException: Device is currently in use by another kernel module. To fix this issue, please follow the instructions from https://github.com/OpenTabletDriver/OpenTabletDriver/wiki/Linux-FAQ#arg umentoutofrangeexception-value-0-15"" and it repeats 4 times on the notification before logging subsequent warnings.
Checking the logs, it looks for other Huion tablets before sending the notification (e.g.
""2021-07-23 03:52:33 [verbose]: Detect: Searching for tablet 'Huion WH1409 V2'
20 2021-07-23 03:52:33 [error]: DeviceInUseException: Device is currently in use by another kernel module. To fix this issue, please follow the instructions from https://github.com/OpenTabletDriver/OpenTabletDriver/wiki/Linux-FAQ#arg umentoutofrangeexception-value-0-15"")
I use an Arch based installation of Linux and the tablet runs perfectly with Digimend kernel driver, with area configuration, pen pressure, etc. On osu!lazer the cursor disappears until I set it to ""Borderless"" instead of ""Fullscreen"" and even after it shows up, it goes to the bottom left corner as soon as a map starts.
I have honestly 0 idea of whats going on at this point."
}
"Code Block", @"User not found! ;_;
There are a few possible reasons for this:
They may have changed their username.
The account may be temporarily unavailable due to security or abuse issues.
You may have made a typo!"
},
};
}
}

View File

@ -137,6 +137,11 @@ namespace osu.Game.Tests.Visual.Online
@"top_ranks",
@"medals"
},
RankHighest = new APIUser.UserRankHighest
{
Rank = 1,
UpdatedAt = DateTimeOffset.Now,
},
Statistics = new UserStatistics
{
IsRanked = true,

View File

@ -629,7 +629,8 @@ namespace osu.Game.Tests.Visual.SongSelect
{
var sets = new List<BeatmapSetInfo>();
const string zzz_string = "zzzzz";
const string zzz_lowercase = "zzzzz";
const string zzz_uppercase = "ZZZZZ";
AddStep("Populuate beatmap sets", () =>
{
@ -640,10 +641,16 @@ namespace osu.Game.Tests.Visual.SongSelect
var set = TestResources.CreateTestBeatmapSetInfo();
if (i == 4)
set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_string);
set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_uppercase);
if (i == 8)
set.Beatmaps.ForEach(b => b.Metadata.Artist = zzz_lowercase);
if (i == 12)
set.Beatmaps.ForEach(b => b.Metadata.Author.Username = zzz_uppercase);
if (i == 16)
set.Beatmaps.ForEach(b => b.Metadata.Author.Username = zzz_string);
set.Beatmaps.ForEach(b => b.Metadata.Author.Username = zzz_lowercase);
sets.Add(set);
}
@ -652,9 +659,11 @@ namespace osu.Game.Tests.Visual.SongSelect
loadBeatmaps(sets);
AddStep("Sort by author", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Author }, false));
AddAssert($"Check {zzz_string} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Author.Username == zzz_string);
AddAssert($"Check {zzz_uppercase} is last", () => carousel.BeatmapSets.Last().Metadata.Author.Username == zzz_uppercase);
AddAssert($"Check {zzz_lowercase} is second last", () => carousel.BeatmapSets.SkipLast(1).Last().Metadata.Author.Username == zzz_lowercase);
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
AddAssert($"Check {zzz_string} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Artist == zzz_string);
AddAssert($"Check {zzz_uppercase} is last", () => carousel.BeatmapSets.Last().Metadata.Artist == zzz_uppercase);
AddAssert($"Check {zzz_lowercase} is second last", () => carousel.BeatmapSets.SkipLast(1).Last().Metadata.Artist == zzz_lowercase);
}
/// <summary>

View File

@ -4,6 +4,7 @@
using System.Diagnostics;
using ManagedBass.Fx;
using osu.Framework.Audio.Mixing;
using osu.Framework.Caching;
using osu.Framework.Graphics;
namespace osu.Game.Audio.Effects
@ -22,6 +23,8 @@ namespace osu.Game.Audio.Effects
private bool isAttached;
private readonly Cached filterApplication = new Cached();
private int cutoff;
/// <summary>
@ -36,7 +39,7 @@ namespace osu.Game.Audio.Effects
return;
cutoff = value;
updateFilter(cutoff);
filterApplication.Invalidate();
}
}
@ -61,6 +64,17 @@ namespace osu.Game.Audio.Effects
Cutoff = getInitialCutoff(type);
}
protected override void Update()
{
base.Update();
if (!filterApplication.IsValid)
{
updateFilter(cutoff);
filterApplication.Validate();
}
}
private int getInitialCutoff(BQFType type)
{
switch (type)

View File

@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps
private readonly OffsetCorrectionClock? userGlobalOffsetClock;
private readonly OffsetCorrectionClock? platformOffsetClock;
private readonly OffsetCorrectionClock? userBeatmapOffsetClock;
private readonly FramedOffsetClock? userBeatmapOffsetClock;
private readonly IFrameBasedClock finalClockSource;
@ -70,7 +70,7 @@ namespace osu.Game.Beatmaps
userGlobalOffsetClock = new OffsetCorrectionClock(platformOffsetClock);
// User per-beatmap offset will be applied to this final clock.
finalClockSource = userBeatmapOffsetClock = new OffsetCorrectionClock(userGlobalOffsetClock);
finalClockSource = userBeatmapOffsetClock = new FramedOffsetClock(userGlobalOffsetClock);
}
else
{
@ -122,7 +122,7 @@ namespace osu.Game.Beatmaps
Debug.Assert(userBeatmapOffsetClock != null);
Debug.Assert(platformOffsetClock != null);
return userGlobalOffsetClock.RateAdjustedOffset + userBeatmapOffsetClock.RateAdjustedOffset + platformOffsetClock.RateAdjustedOffset;
return userGlobalOffsetClock.RateAdjustedOffset + userBeatmapOffsetClock.Offset + platformOffsetClock.RateAdjustedOffset;
}
}

View File

@ -9,6 +9,7 @@ using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Dummy;
using osu.Framework.Graphics.Textures;
@ -143,8 +144,6 @@ namespace osu.Game.Beatmaps
{
string fileStorePath = BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path);
// TODO: check validity of file
var stream = GetStream(fileStorePath);
if (stream == null)
@ -153,6 +152,12 @@ namespace osu.Game.Beatmaps
return null;
}
if (stream.ComputeMD5Hash() != BeatmapInfo.MD5Hash)
{
Logger.Log($"Beatmap failed to load (file {BeatmapInfo.Path} does not have the expected hash).", level: LogLevel.Error);
return null;
}
using (var reader = new LineBufferedReader(stream))
return Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
}

View File

@ -77,12 +77,19 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.SavePassword, false).ValueChanged += enabled =>
{
if (enabled.NewValue) SetValue(OsuSetting.SaveUsername, true);
if (enabled.NewValue)
SetValue(OsuSetting.SaveUsername, true);
else
GetBindable<string>(OsuSetting.Token).SetDefault();
};
SetDefault(OsuSetting.SaveUsername, true).ValueChanged += enabled =>
{
if (!enabled.NewValue) SetValue(OsuSetting.SavePassword, false);
if (!enabled.NewValue)
{
GetBindable<string>(OsuSetting.Username).SetDefault();
SetValue(OsuSetting.SavePassword, false);
}
};
SetDefault(OsuSetting.ExternalLinkWarning, true);

View File

@ -10,11 +10,11 @@ using osu.Game.Overlays;
namespace osu.Game.Graphics.Containers.Markdown
{
public partial class OsuMarkdownFencedCodeBlock : MarkdownFencedCodeBlock
public partial class OsuMarkdownCodeBlock : MarkdownCodeBlock
{
// TODO : change to monospace font for this component
public OsuMarkdownFencedCodeBlock(FencedCodeBlock fencedCodeBlock)
: base(fencedCodeBlock)
public OsuMarkdownCodeBlock(CodeBlock codeBlock)
: base(codeBlock)
{
}

View File

@ -67,7 +67,7 @@ namespace osu.Game.Graphics.Containers.Markdown
protected override MarkdownHeading CreateHeading(HeadingBlock headingBlock) => new OsuMarkdownHeading(headingBlock);
protected override MarkdownFencedCodeBlock CreateFencedCodeBlock(FencedCodeBlock fencedCodeBlock) => new OsuMarkdownFencedCodeBlock(fencedCodeBlock);
protected override MarkdownCodeBlock CreateCodeBlock(CodeBlock codeBlock) => new OsuMarkdownCodeBlock(codeBlock);
protected override MarkdownSeparator CreateSeparator(ThematicBreakBlock thematicBlock) => new OsuMarkdownSeparator();

View File

@ -0,0 +1,49 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class StorageErrorDialogStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.StorageErrorDialog";
/// <summary>
/// "osu! storage error"
/// </summary>
public static LocalisableString StorageError => new TranslatableString(getKey(@"storage_error"), @"osu! storage error");
/// <summary>
/// "The specified osu! data location (&quot;{0}&quot;) is not accessible. If it is on external storage, please reconnect the device and try again."
/// </summary>
public static LocalisableString LocationIsNotAccessible(string? loc) => new TranslatableString(getKey(@"location_is_not_accessible"), @"The specified osu! data location (""{0}"") is not accessible. If it is on external storage, please reconnect the device and try again.", loc);
/// <summary>
/// "The specified osu! data location (&quot;{0}&quot;) is empty. If you have moved the files, please close osu! and move them back."
/// </summary>
public static LocalisableString LocationIsEmpty(string? loc2) => new TranslatableString(getKey(@"location_is_empty"), @"The specified osu! data location (""{0}"") is empty. If you have moved the files, please close osu! and move them back.", loc2);
/// <summary>
/// "Try again"
/// </summary>
public static LocalisableString TryAgain => new TranslatableString(getKey(@"try_again"), @"Try again");
/// <summary>
/// "Use default location until restart"
/// </summary>
public static LocalisableString UseDefaultLocation => new TranslatableString(getKey(@"use_default_location"), @"Use default location until restart");
/// <summary>
/// "Reset to default location"
/// </summary>
public static LocalisableString ResetToDefaultLocation => new TranslatableString(getKey(@"reset_to_default_location"), @"Reset to default location");
/// <summary>
/// "Start fresh at specified location"
/// </summary>
public static LocalisableString StartFresh => new TranslatableString(getKey(@"start_fresh"), @"Start fresh at specified location");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -34,6 +34,19 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"previous_usernames")]
public string[] PreviousUsernames;
[JsonProperty(@"rank_highest")]
[CanBeNull]
public UserRankHighest RankHighest;
public class UserRankHighest
{
[JsonProperty(@"rank")]
public int Rank;
[JsonProperty(@"updated_at")]
public DateTimeOffset UpdatedAt;
}
[JsonProperty(@"country_code")]
private string countryCodeString;

View File

@ -66,41 +66,37 @@ namespace osu.Game.Overlays.Chat.ChannelList
Colour = colourProvider.Background4,
Alpha = 0f,
},
new Container
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = 18, Right = 10 },
Child = new GridContainer
ColumnDimensions = new[]
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable?[]
{
createIcon(),
text = new TruncatingSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = Channel.Name,
Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold),
Colour = colourProvider.Light3,
Margin = new MarginPadding { Bottom = 2 },
RelativeSizeAxes = Axes.X,
},
createMentionPill(),
close = createCloseButton(),
}
},
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
},
},
Content = new[]
{
new Drawable?[]
{
createIcon(),
text = new TruncatingSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Text = Channel.Name,
Font = OsuFont.Torus.With(size: 17, weight: FontWeight.SemiBold),
Colour = colourProvider.Light3,
Margin = new MarginPadding { Bottom = 2 },
RelativeSizeAxes = Axes.X,
},
createMentionPill(),
close = createCloseButton(),
}
}
}
};
Action = () => OnRequestSelect?.Invoke(Channel);

View File

@ -143,6 +143,13 @@ namespace osu.Game.Overlays.Profile.Header.Components
scoreRankInfo.Value.RankCount = user?.Statistics?.GradesCount[scoreRankInfo.Key] ?? 0;
detailGlobalRank.Content = user?.Statistics?.GlobalRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-";
var rankHighest = user?.RankHighest;
detailGlobalRank.ContentTooltipText = rankHighest != null
? UsersStrings.ShowRankHighest(rankHighest.Rank.ToLocalisableString("\\##,##0"), rankHighest.UpdatedAt.ToLocalisableString(@"d MMM yyyy"))
: string.Empty;
detailCountryRank.Content = user?.Statistics?.CountryRank?.ToLocalisableString("\\##,##0") ?? (LocalisableString)"-";
rankGraph.Statistics.Value = user?.Statistics;

View File

@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
@ -13,7 +14,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
public partial class ProfileValueDisplay : CompositeDrawable
{
private readonly OsuSpriteText title;
private readonly OsuSpriteText content;
private readonly ContentText content;
public LocalisableString Title
{
@ -25,6 +26,11 @@ namespace osu.Game.Overlays.Profile.Header.Components
set => content.Text = value;
}
public LocalisableString ContentTooltipText
{
set => content.TooltipText = value;
}
public ProfileValueDisplay(bool big = false, int minimumWidth = 60)
{
AutoSizeAxes = Axes.Both;
@ -38,9 +44,9 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
Font = OsuFont.GetFont(size: 12)
},
content = new OsuSpriteText
content = new ContentText
{
Font = OsuFont.GetFont(size: big ? 30 : 20, weight: FontWeight.Light)
Font = OsuFont.GetFont(size: big ? 30 : 20, weight: FontWeight.Light),
},
new Container // Add a minimum size to the FillFlowContainer
{
@ -56,5 +62,10 @@ namespace osu.Game.Overlays.Profile.Header.Components
title.Colour = colourProvider.Content1;
content.Colour = colourProvider.Content2;
}
private partial class ContentText : OsuSpriteText, IHasTooltip
{
public LocalisableString TooltipText { get; set; }
}
}
}

View File

@ -5,25 +5,19 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Profile.Header.Components
{
public partial class TotalPlayTime : CompositeDrawable, IHasTooltip
public partial class TotalPlayTime : CompositeDrawable
{
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
public LocalisableString TooltipText { get; set; }
private ProfileValueDisplay info = null!;
public TotalPlayTime()
{
AutoSizeAxes = Axes.Both;
TooltipText = "0 hours";
}
[BackgroundDependencyLoader]
@ -32,6 +26,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
InternalChild = info = new ProfileValueDisplay(minimumWidth: 140)
{
Title = UsersStrings.ShowStatsPlayTime,
ContentTooltipText = "0 hours",
};
User.BindValueChanged(updateTime, true);
@ -40,7 +35,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
private void updateTime(ValueChangedEvent<UserProfileData?> user)
{
int? playTime = user.NewValue?.User.Statistics?.PlayTime;
TooltipText = (playTime ?? 0) / 3600 + " hours";
info.ContentTooltipText = (playTime ?? 0) / 3600 + " hours";
info.Content = formatTime(playTime);
}

View File

@ -26,47 +26,42 @@ namespace osu.Game.Overlays.Profile.Header
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
},
new Container
new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = WaveOverlayContainer.HORIZONTAL_PADDING, Vertical = 10 },
Child = new GridContainer
RowDimensions = new[]
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
RowDimensions = new[]
new Dimension(GridSizeMode.AutoSize),
},
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
new Dimension(GridSizeMode.AutoSize),
},
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
new MainDetails
{
new MainDetails
{
RelativeSizeAxes = Axes.X,
User = { BindTarget = User }
},
new Box
{
RelativeSizeAxes = Axes.Y,
Width = 2,
Colour = colourProvider.Background6,
Margin = new MarginPadding { Horizontal = 15 }
},
new ExtendedDetails
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
User = { BindTarget = User }
}
RelativeSizeAxes = Axes.X,
User = { BindTarget = User }
},
new Box
{
RelativeSizeAxes = Axes.Y,
Width = 2,
Colour = colourProvider.Background6,
Margin = new MarginPadding { Horizontal = 15 }
},
new ExtendedDetails
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
User = { BindTarget = User }
}
}
}

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Extensions;
@ -28,15 +27,16 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
var renderer = config.GetBindable<RendererType>(FrameworkSetting.Renderer);
automaticRendererInUse = renderer.Value == RendererType.Automatic;
SettingsEnumDropdown<RendererType> rendererDropdown;
Children = new Drawable[]
{
rendererDropdown = new RendererSettingsDropdown
new RendererSettingsDropdown
{
LabelText = GraphicsSettingsStrings.Renderer,
Current = renderer,
Items = host.GetPreferredRenderersForCurrentPlatform().Order().Where(t => t != RendererType.Vulkan),
Items = host.GetPreferredRenderersForCurrentPlatform().Order()
#pragma warning disable CS0612 // Type or member is obsolete
.Where(t => t != RendererType.Vulkan && t != RendererType.OpenGLLegacy),
#pragma warning restore CS0612 // Type or member is obsolete
Keywords = new[] { @"compatibility", @"directx" },
},
// TODO: this needs to be a custom dropdown at some point
@ -79,13 +79,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
}));
}
});
// TODO: remove this once we support SDL+android.
if (RuntimeInfo.OS == RuntimeInfo.Platform.Android)
{
rendererDropdown.Items = new[] { RendererType.Automatic, RendererType.OpenGLLegacy };
rendererDropdown.SetNoticeText("New renderer support for android is coming soon!", true);
}
}
private partial class RendererSettingsDropdown : SettingsEnumDropdown<RendererType>

View File

@ -10,9 +10,11 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
@ -66,6 +68,7 @@ namespace osu.Game.Overlays.SkinEditor
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
private OsuScreen? lastTargetScreen;
private InvokeOnDisposal? nestedInputManagerDisable;
private Vector2 lastDrawSize;
@ -105,6 +108,7 @@ namespace osu.Game.Overlays.SkinEditor
if (skinEditor != null)
{
disableNestedInputManagers();
skinEditor.Show();
return;
}
@ -132,6 +136,8 @@ namespace osu.Game.Overlays.SkinEditor
{
skinEditor?.Save(false);
skinEditor?.Hide();
nestedInputManagerDisable?.Dispose();
nestedInputManagerDisable = null;
globallyReenableBeatmapSkinSetting();
}
@ -243,6 +249,9 @@ namespace osu.Game.Overlays.SkinEditor
/// </summary>
public void SetTarget(OsuScreen screen)
{
nestedInputManagerDisable?.Dispose();
nestedInputManagerDisable = null;
lastTargetScreen = screen;
if (skinEditor == null) return;
@ -271,6 +280,7 @@ namespace osu.Game.Overlays.SkinEditor
{
skinEditor.Save(false);
skinEditor.UpdateTargetScreen(target);
disableNestedInputManagers();
}
else
{
@ -280,6 +290,21 @@ namespace osu.Game.Overlays.SkinEditor
}
}
private void disableNestedInputManagers()
{
if (lastTargetScreen == null)
return;
var nestedInputManagers = lastTargetScreen.ChildrenOfType<PassThroughInputManager>().Where(manager => manager.UseParentInput).ToArray();
foreach (var inputManager in nestedInputManagers)
inputManager.UseParentInput = false;
nestedInputManagerDisable = new InvokeOnDisposal(() =>
{
foreach (var inputManager in nestedInputManagers)
inputManager.UseParentInput = true;
});
}
private readonly Bindable<bool> beatmapSkins = new Bindable<bool>();
private LeasedBindable<bool>? leasedBeatmapSkins;

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
@ -124,12 +123,34 @@ namespace osu.Game.Rulesets.Edit
private (HitObject before, HitObject after)? getObjectsOnEitherSideOfCurrentTime()
{
HitObject? lastBefore = playfield.HitObjectContainer.AliveObjects.LastOrDefault(h => h.HitObject.StartTime < editorClock.CurrentTime)?.HitObject;
HitObject? lastBefore = null;
foreach (var entry in playfield.HitObjectContainer.AliveEntries)
{
double objTime = entry.Value.HitObject.StartTime;
if (objTime >= editorClock.CurrentTime)
continue;
if (objTime > lastBefore?.StartTime)
lastBefore = entry.Value.HitObject;
}
if (lastBefore == null)
return null;
HitObject? firstAfter = playfield.HitObjectContainer.AliveObjects.FirstOrDefault(h => h.HitObject.StartTime >= editorClock.CurrentTime)?.HitObject;
HitObject? firstAfter = null;
foreach (var entry in playfield.HitObjectContainer.AliveEntries)
{
double objTime = entry.Value.HitObject.StartTime;
if (objTime < editorClock.CurrentTime)
continue;
if (objTime < firstAfter?.StartTime)
firstAfter = entry.Value.HitObject;
}
if (firstAfter == null)
return null;

View File

@ -47,35 +47,31 @@ namespace osu.Game.Screens.Edit
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
},
new Container
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Child = new GridContainer
ColumnDimensions = new[]
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 170),
new Dimension(),
new Dimension(GridSizeMode.Absolute, 220),
new Dimension(GridSizeMode.Absolute, HitObjectComposer.TOOLBOX_CONTRACTED_SIZE_RIGHT),
},
Content = new[]
{
new Drawable[]
{
new TimeInfoContainer { RelativeSizeAxes = Axes.Both },
new SummaryTimeline { RelativeSizeAxes = Axes.Both },
new PlaybackControl { RelativeSizeAxes = Axes.Both },
TestGameplayButton = new TestGameplayButton
{
RelativeSizeAxes = Axes.Both,
Size = new Vector2(1),
Action = editor.TestGameplay,
}
},
}
new Dimension(GridSizeMode.Absolute, 170),
new Dimension(),
new Dimension(GridSizeMode.Absolute, 220),
new Dimension(GridSizeMode.Absolute, HitObjectComposer.TOOLBOX_CONTRACTED_SIZE_RIGHT),
},
Content = new[]
{
new Drawable[]
{
new TimeInfoContainer { RelativeSizeAxes = Axes.Both },
new SummaryTimeline { RelativeSizeAxes = Axes.Both },
new PlaybackControl { RelativeSizeAxes = Axes.Both },
TestGameplayButton = new TestGameplayButton
{
RelativeSizeAxes = Axes.Both,
Size = new Vector2(1),
Action = editor.TestGameplay,
}
},
}
}
};
}

View File

@ -97,11 +97,14 @@ namespace osu.Game.Screens.Edit.Components
editorClock.Start();
}
private static readonly IconUsage play_icon = FontAwesome.Regular.PlayCircle;
private static readonly IconUsage pause_icon = FontAwesome.Regular.PauseCircle;
protected override void Update()
{
base.Update();
playButton.Icon = editorClock.IsRunning ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle;
playButton.Icon = editorClock.IsRunning ? pause_icon : play_icon;
}
private partial class PlaybackTabControl : OsuTabControl<double>

View File

@ -47,11 +47,26 @@ namespace osu.Game.Screens.Edit.Components
};
}
private double? lastTime;
private double? lastBPM;
protected override void Update()
{
base.Update();
trackTimer.Text = editorClock.CurrentTime.ToEditorFormattedString();
bpm.Text = @$"{editorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime).BPM:0} BPM";
if (lastTime != editorClock.CurrentTime)
{
lastTime = editorClock.CurrentTime;
trackTimer.Text = editorClock.CurrentTime.ToEditorFormattedString();
}
double newBPM = editorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime).BPM;
if (lastBPM != newBPM)
{
lastBPM = newBPM;
bpm.Text = @$"{newBPM:0} BPM";
}
}
}
}

View File

@ -86,35 +86,31 @@ namespace osu.Game.Screens.Edit.Compose.Components
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background3
},
new Container
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Child = new GridContainer
Content = new[]
{
RelativeSizeAxes = Axes.Both,
Content = new[]
new Drawable[]
{
new Drawable[]
new ChevronButton
{
new ChevronButton
{
Icon = FontAwesome.Solid.ChevronLeft,
Action = beatDivisor.SelectPrevious
},
new DivisorDisplay { BeatDivisor = { BindTarget = beatDivisor } },
new ChevronButton
{
Icon = FontAwesome.Solid.ChevronRight,
Action = beatDivisor.SelectNext
}
Icon = FontAwesome.Solid.ChevronLeft,
Action = beatDivisor.SelectPrevious
},
new DivisorDisplay { BeatDivisor = { BindTarget = beatDivisor } },
new ChevronButton
{
Icon = FontAwesome.Solid.ChevronRight,
Action = beatDivisor.SelectNext
}
},
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 20),
new Dimension(),
new Dimension(GridSizeMode.Absolute, 20)
}
},
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 20),
new Dimension(),
new Dimension(GridSizeMode.Absolute, 20)
}
}
}
@ -122,42 +118,31 @@ namespace osu.Game.Screens.Edit.Compose.Components
},
new Drawable[]
{
new Container
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
Content = new[]
{
new Container
new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Child = new GridContainer
new ChevronButton
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[]
{
new ChevronButton
{
Icon = FontAwesome.Solid.ChevronLeft,
Action = () => cycleDivisorType(-1)
},
new DivisorTypeText { BeatDivisor = { BindTarget = beatDivisor } },
new ChevronButton
{
Icon = FontAwesome.Solid.ChevronRight,
Action = () => cycleDivisorType(1)
}
},
},
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 20),
new Dimension(),
new Dimension(GridSizeMode.Absolute, 20)
}
Icon = FontAwesome.Solid.ChevronLeft,
Action = () => cycleDivisorType(-1)
},
new DivisorTypeText { BeatDivisor = { BindTarget = beatDivisor } },
new ChevronButton
{
Icon = FontAwesome.Solid.ChevronRight,
Action = () => cycleDivisorType(1)
}
}
},
},
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 20),
new Dimension(),
new Dimension(GridSizeMode.Absolute, 20)
}
}
},

View File

@ -116,6 +116,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
updateStacking();
}
private readonly Stack<HitObject> currentConcurrentObjects = new Stack<HitObject>();
private void updateStacking()
{
// because only blueprints of objects which are alive (via pooling) are displayed in the timeline, it's feasible to do this every-update.
@ -125,10 +127,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
// after the stack gets this tall, we can presume there is space underneath to draw subsequent blueprints.
const int stack_reset_count = 3;
Stack<HitObject> currentConcurrentObjects = new Stack<HitObject>();
currentConcurrentObjects.Clear();
foreach (var b in SelectionBlueprints.Reverse())
for (int i = SelectionBlueprints.Count - 1; i >= 0; i--)
{
var b = SelectionBlueprints[i];
// remove objects from the stack as long as their end time is in the past.
while (currentConcurrentObjects.TryPeek(out HitObject hitObject))
{

View File

@ -57,37 +57,32 @@ namespace osu.Game.Screens.Edit
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4
},
new Container
new GridContainer
{
Name = "Timeline content",
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = PADDING, Top = PADDING },
Child = new GridContainer
Content = new[]
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Content = new[]
new Drawable[]
{
new Drawable[]
TimelineContent = new Container
{
TimelineContent = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
},
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.Absolute, 90),
}
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
},
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.Absolute, 90),
}
}
}
},

View File

@ -65,35 +65,28 @@ namespace osu.Game.Screens.Edit.Timing
{
new Drawable[]
{
new Container
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(padding),
Children = new Drawable[]
ColumnDimensions = new[]
{
new GridContainer
new Dimension(GridSizeMode.AutoSize),
new Dimension()
},
Content = new[]
{
new Drawable[]
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
metronome = new MetronomeDisplay
{
new Dimension(GridSizeMode.AutoSize),
new Dimension()
},
Content = new[]
{
new Drawable[]
{
metronome = new MetronomeDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
new WaveformComparisonDisplay()
}
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
new WaveformComparisonDisplay()
}
}
},
},
}
},
new Drawable[]
{

View File

@ -34,25 +34,21 @@ namespace osu.Game.Screens.Edit.Verify
InterpretedDifficulty.Default = StarDifficulty.GetDifficultyRating(EditorBeatmap.BeatmapInfo.StarRating);
InterpretedDifficulty.SetDefault();
Child = new Container
Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Child = new GridContainer
ColumnDimensions = new[]
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
new Dimension(),
new Dimension(GridSizeMode.Absolute, 250),
},
Content = new[]
{
new Drawable[]
{
new Dimension(),
new Dimension(GridSizeMode.Absolute, 250),
IssueList = new IssueList(),
new IssueSettings(),
},
Content = new[]
{
new Drawable[]
{
IssueList = new IssueList(),
new IssueSettings(),
},
}
}
};
}

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
using osu.Game.IO;
using osu.Game.Localisation;
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
@ -17,7 +18,7 @@ namespace osu.Game.Screens.Menu
public StorageErrorDialog(OsuStorage storage, OsuStorageError error)
{
HeaderText = "osu! storage error";
HeaderText = StorageErrorDialogStrings.StorageError;
Icon = FontAwesome.Solid.ExclamationTriangle;
var buttons = new List<PopupDialogButton>();
@ -25,13 +26,13 @@ namespace osu.Game.Screens.Menu
switch (error)
{
case OsuStorageError.NotAccessible:
BodyText = $"The specified osu! data location (\"{storage.CustomStoragePath}\") is not accessible. If it is on external storage, please reconnect the device and try again.";
BodyText = StorageErrorDialogStrings.LocationIsNotAccessible(storage.CustomStoragePath);
buttons.AddRange(new PopupDialogButton[]
{
new PopupDialogCancelButton
{
Text = "Try again",
Text = StorageErrorDialogStrings.TryAgain,
Action = () =>
{
if (!storage.TryChangeToCustomStorage(out var nextError))
@ -40,29 +41,29 @@ namespace osu.Game.Screens.Menu
},
new PopupDialogCancelButton
{
Text = "Use default location until restart",
Text = StorageErrorDialogStrings.UseDefaultLocation,
},
new PopupDialogOkButton
{
Text = "Reset to default location",
Text = StorageErrorDialogStrings.ResetToDefaultLocation,
Action = storage.ResetCustomStoragePath
},
});
break;
case OsuStorageError.AccessibleButEmpty:
BodyText = $"The specified osu! data location (\"{storage.CustomStoragePath}\") is empty. If you have moved the files, please close osu! and move them back.";
BodyText = StorageErrorDialogStrings.LocationIsEmpty(storage.CustomStoragePath);
// Todo: Provide the option to search for the files similar to migration.
buttons.AddRange(new PopupDialogButton[]
{
new PopupDialogCancelButton
{
Text = "Start fresh at specified location"
Text = StorageErrorDialogStrings.StartFresh
},
new PopupDialogOkButton
{
Text = "Reset to default location",
Text = StorageErrorDialogStrings.ResetToDefaultLocation,
Action = storage.ResetCustomStoragePath
},
});

View File

@ -26,48 +26,44 @@ namespace osu.Game.Screens.OnlinePlay.Components
[Resolved(typeof(Room))]
protected BindableList<PlaylistItem> Playlist { get; private set; }
private readonly Drawable playlistArea;
private readonly GridContainer playlistArea;
private readonly DrawableRoomPlaylist playlist;
public MatchBeatmapDetailArea()
{
Add(playlistArea = new Container
Add(playlistArea = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Vertical = 10 },
Child = new GridContainer
Content = new[]
{
RelativeSizeAxes = Axes.Both,
Content = new[]
new Drawable[]
{
new Drawable[]
new Container
{
new Container
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Bottom = 10 },
Child = playlist = new PlaylistsRoomSettingsPlaylist
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Bottom = 10 },
Child = playlist = new PlaylistsRoomSettingsPlaylist
{
RelativeSizeAxes = Axes.Both
}
RelativeSizeAxes = Axes.Both
}
},
new Drawable[]
{
new RoundedButton
{
Text = "Add new playlist entry",
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
Action = () => CreateNewItem?.Invoke()
}
},
}
},
RowDimensions = new[]
new Drawable[]
{
new Dimension(),
new Dimension(GridSizeMode.Absolute, 50),
}
new RoundedButton
{
Text = "Add new playlist entry",
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
Action = () => CreateNewItem?.Invoke()
}
},
},
RowDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.Absolute, 50),
}
});
}

View File

@ -40,35 +40,31 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
Colour = Color4.Black,
Alpha = 0.5f
},
new Container
new GridContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = padding },
Child = new GridContainer
ColumnDimensions = new[]
{
AutoSizeAxes = Axes.Both,
ColumnDimensions = new[]
new Dimension(GridSizeMode.AutoSize, minSize: 80 - 2 * padding)
},
Content = new[]
{
new[]
{
new Dimension(GridSizeMode.AutoSize, minSize: 80 - 2 * padding)
},
Content = new[]
{
new[]
new Container
{
new Container
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Padding = new MarginPadding { Bottom = 2 },
Child = content = new Container
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Padding = new MarginPadding { Bottom = 2 },
Child = content = new Container
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}
}
}

View File

@ -95,38 +95,34 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
new Drawable[]
{
// Playlist items column
new Container
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = 5 },
Child = new GridContainer
Content = new[]
{
RelativeSizeAxes = Axes.Both,
Content = new[]
new Drawable[] { new OverlinedPlaylistHeader(), },
new Drawable[]
{
new Drawable[] { new OverlinedPlaylistHeader(), },
new Drawable[]
new DrawableRoomPlaylist
{
new DrawableRoomPlaylist
RelativeSizeAxes = Axes.Both,
Items = { BindTarget = Room.Playlist },
SelectedItem = { BindTarget = SelectedItem },
AllowSelection = true,
AllowShowingResults = true,
RequestResults = item =>
{
RelativeSizeAxes = Axes.Both,
Items = { BindTarget = Room.Playlist },
SelectedItem = { BindTarget = SelectedItem },
AllowSelection = true,
AllowShowingResults = true,
RequestResults = item =>
{
Debug.Assert(RoomId.Value != null);
ParentScreen?.Push(new PlaylistsResultsScreen(null, RoomId.Value.Value, item, false));
}
Debug.Assert(RoomId.Value != null);
ParentScreen?.Push(new PlaylistsResultsScreen(null, RoomId.Value.Value, item, false));
}
},
}
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
}
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
}
},
// Spacer

View File

@ -52,7 +52,7 @@ namespace osu.Game.Screens.Play
Scores = { BindTarget = LeaderboardScores }
};
protected override bool HandleTokenRetrievalFailure(Exception exception) => false;
protected override bool ShouldExitOnTokenRetrievalFailure(Exception exception) => false;
protected override Task ImportScore(Score score)
{

View File

@ -118,7 +118,7 @@ namespace osu.Game.Screens.Play
token = r.ID;
tcs.SetResult(true);
};
req.Failure += handleTokenFailure;
req.Failure += ex => handleTokenFailure(ex, displayNotification: true);
api.Queue(req);
@ -128,40 +128,49 @@ namespace osu.Game.Screens.Play
return true;
void handleTokenFailure(Exception exception)
void handleTokenFailure(Exception exception, bool displayNotification = false)
{
tcs.SetResult(false);
if (HandleTokenRetrievalFailure(exception))
bool shouldExit = ShouldExitOnTokenRetrievalFailure(exception);
if (displayNotification || shouldExit)
{
string whatWillHappen = shouldExit
? "Play in this state is not permitted."
: "Your score will not be submitted.";
if (string.IsNullOrEmpty(exception.Message))
Logger.Error(exception, "Failed to retrieve a score submission token.");
Logger.Error(exception, $"Failed to retrieve a score submission token.\n\n{whatWillHappen}");
else
{
switch (exception.Message)
{
case "expired token":
Logger.Log("Score submission failed because your system clock is set incorrectly. Please check your system time, date and timezone.", level: LogLevel.Important);
case @"missing token header":
case @"invalid client hash":
case @"invalid verification hash":
Logger.Log($"Please ensure that you are using the latest version of the official game releases.\n\n{whatWillHappen}", level: LogLevel.Important);
break;
case @"expired token":
Logger.Log($"Your system clock is set incorrectly. Please check your system time, date and timezone.\n\n{whatWillHappen}", level: LogLevel.Important);
break;
default:
Logger.Log($"You are not able to submit a score: {exception.Message}", level: LogLevel.Important);
Logger.Log($"{whatWillHappen} {exception.Message}", level: LogLevel.Important);
break;
}
}
}
if (shouldExit)
{
Schedule(() =>
{
ValidForResume = false;
this.Exit();
});
}
else
{
// Gameplay is allowed to continue, but we still should keep track of the error.
// In the future, this should be visible to the user in some way.
Logger.Log($"Score submission token retrieval failed ({exception.Message})");
}
}
}
@ -170,7 +179,7 @@ namespace osu.Game.Screens.Play
/// </summary>
/// <param name="exception">The error causing the failure.</param>
/// <returns>Whether gameplay should be immediately exited as a result. Returning false allows the gameplay session to continue. Defaults to true.</returns>
protected virtual bool HandleTokenRetrievalFailure(Exception exception) => true;
protected virtual bool ShouldExitOnTokenRetrievalFailure(Exception exception) => true;
protected override async Task PrepareScoreForResultsAsync(Score score)
{
@ -231,7 +240,7 @@ namespace osu.Game.Screens.Play
/// <summary>
/// Construct a request to be used for retrieval of the score token.
/// Can return null, at which point <see cref="HandleTokenRetrievalFailure"/> will be fired.
/// Can return null, at which point <see cref="ShouldExitOnTokenRetrievalFailure"/> will be fired.
/// </summary>
[CanBeNull]
protected abstract APIRequest<APIScoreToken> CreateTokenRequest();

View File

@ -150,44 +150,40 @@ namespace osu.Game.Screens.Ranking.Contracted
},
new Drawable[]
{
new Container
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Vertical = 5 },
Child = new GridContainer
Content = new[]
{
RelativeSizeAxes = Axes.Both,
Content = new[]
new Drawable[]
{
new Drawable[]
new OsuSpriteText
{
new OsuSpriteText
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Current = scoreManager.GetBindableTotalScoreString(score),
Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, fixedWidth: true),
Spacing = new Vector2(-1, 0)
},
},
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 2 },
Child = new DrawableRank(score.Rank)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Current = scoreManager.GetBindableTotalScoreString(score),
Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, fixedWidth: true),
Spacing = new Vector2(-1, 0)
},
},
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 2 },
Child = new DrawableRank(score.Rank)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}
},
}
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
}
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
}
}
},

View File

@ -75,99 +75,92 @@ namespace osu.Game.Screens.Select
RelativeSizeAxes = Axes.Both,
Colour = Colour4.Black.Opacity(0.3f),
},
new Container
new GridContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Horizontal = spacing },
Children = new Drawable[]
RowDimensions = new[]
{
new GridContainer
new Dimension(GridSizeMode.AutoSize),
new Dimension()
},
Content = new[]
{
new Drawable[]
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
new FillFlowContainer
{
new Dimension(GridSizeMode.AutoSize),
new Dimension()
},
Content = new[]
{
new Drawable[]
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
Width = 0.5f,
Spacing = new Vector2(spacing),
Padding = new MarginPadding { Right = spacing / 2 },
Children = new[]
{
new FillFlowContainer
new DetailBox().WithChild(new OnlineViewContainer(string.Empty)
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Width = 0.5f,
Spacing = new Vector2(spacing),
Padding = new MarginPadding { Right = spacing / 2 },
Children = new[]
Height = 134,
Padding = new MarginPadding { Horizontal = spacing, Top = spacing },
Child = ratingsDisplay = new UserRatings
{
new DetailBox().WithChild(new OnlineViewContainer(string.Empty)
{
RelativeSizeAxes = Axes.X,
Height = 134,
Padding = new MarginPadding { Horizontal = spacing, Top = spacing },
Child = ratingsDisplay = new UserRatings
{
RelativeSizeAxes = Axes.Both,
},
}),
RelativeSizeAxes = Axes.Both,
},
},
new OsuScrollContainer
}),
},
},
new OsuScrollContainer
{
RelativeSizeAxes = Axes.X,
Height = 250,
Width = 0.5f,
ScrollbarVisible = false,
Padding = new MarginPadding { Left = spacing / 2 },
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
LayoutDuration = transition_duration,
LayoutEasing = Easing.OutQuad,
Children = new[]
{
RelativeSizeAxes = Axes.X,
Height = 250,
Width = 0.5f,
ScrollbarVisible = false,
Padding = new MarginPadding { Left = spacing / 2 },
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
LayoutDuration = transition_duration,
LayoutEasing = Easing.OutQuad,
Children = new[]
{
description = new MetadataSectionDescription(query => songSelect?.Search(query)),
source = new MetadataSectionSource(query => songSelect?.Search(query)),
tags = new MetadataSectionTags(query => songSelect?.Search(query)),
},
},
description = new MetadataSectionDescription(query => songSelect?.Search(query)),
source = new MetadataSectionSource(query => songSelect?.Search(query)),
tags = new MetadataSectionTags(query => songSelect?.Search(query)),
},
},
},
},
new Drawable[]
},
},
new Drawable[]
{
failRetryContainer = new OnlineViewContainer("Sign in to view more details")
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
failRetryContainer = new OnlineViewContainer("Sign in to view more details")
new OsuSpriteText
{
Text = BeatmapsetsStrings.ShowInfoPointsOfFailure,
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 14),
},
failRetryGraph = new FailRetryGraph
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new OsuSpriteText
{
Text = BeatmapsetsStrings.ShowInfoPointsOfFailure,
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 14),
},
failRetryGraph = new FailRetryGraph
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 14 + spacing / 2 },
},
},
Padding = new MarginPadding { Top = 14 + spacing / 2 },
},
}
}
},
},
},
},
}
}
},
loading = new LoadingLayer(true)
};

View File

@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Beatmaps;
using osu.Game.Screens.Select.Filter;
using osu.Game.Utils;
namespace osu.Game.Screens.Select.Carousel
{
@ -67,19 +68,19 @@ namespace osu.Game.Screens.Select.Carousel
{
default:
case SortMode.Artist:
comparison = string.Compare(BeatmapSet.Metadata.Artist, otherSet.BeatmapSet.Metadata.Artist, StringComparison.Ordinal);
comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(BeatmapSet.Metadata.Artist, otherSet.BeatmapSet.Metadata.Artist);
break;
case SortMode.Title:
comparison = string.Compare(BeatmapSet.Metadata.Title, otherSet.BeatmapSet.Metadata.Title, StringComparison.Ordinal);
comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(BeatmapSet.Metadata.Title, otherSet.BeatmapSet.Metadata.Title);
break;
case SortMode.Author:
comparison = string.Compare(BeatmapSet.Metadata.Author.Username, otherSet.BeatmapSet.Metadata.Author.Username, StringComparison.Ordinal);
comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(BeatmapSet.Metadata.Author.Username, otherSet.BeatmapSet.Metadata.Author.Username);
break;
case SortMode.Source:
comparison = string.Compare(BeatmapSet.Metadata.Source, otherSet.BeatmapSet.Metadata.Source, StringComparison.Ordinal);
comparison = OrdinalSortByCaseStringComparer.DEFAULT.Compare(BeatmapSet.Metadata.Source, otherSet.BeatmapSet.Metadata.Source);
break;
case SortMode.DateAdded:

View File

@ -153,6 +153,8 @@ namespace osu.Game.Tests.Visual
public new Bindable<IReadOnlyList<Mod>> SelectedMods => base.SelectedMods;
public new Storage Storage => base.Storage;
public new SpectatorClient SpectatorClient => base.SpectatorClient;
// if we don't apply these changes, when running under nUnit the version that gets populated is that of nUnit.
@ -166,7 +168,7 @@ namespace osu.Game.Tests.Visual
public TestOsuGame(Storage storage, IAPIProvider api, string[] args = null)
: base(args)
{
Storage = storage;
base.Storage = storage;
API = api;
}

View File

@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual
PauseOnFocusLost = pauseOnFocusLost;
}
protected override bool HandleTokenRetrievalFailure(Exception exception) => false;
protected override bool ShouldExitOnTokenRetrievalFailure(Exception exception) => false;
protected override APIRequest<APIScoreToken> CreateTokenRequest()
{

View File

@ -166,6 +166,9 @@ namespace osu.Game.Users
globalRankDisplay = new ProfileValueDisplay(true)
{
Title = UsersStrings.ShowRankGlobalSimple,
// TODO: implement highest rank tooltip
// `RankHighest` resides in `APIUser`, but `api.LocalUser` doesn't update
// maybe move to `UserStatistics` in api, so `SoloStatisticsWatcher` can update the value
},
countryRankDisplay = new ProfileValueDisplay(true)
{

View File

@ -0,0 +1,49 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
namespace osu.Game.Utils
{
/// <summary>
/// This string comparer is something of a cross-over between <see cref="StringComparer.Ordinal"/> and <see cref="StringComparer.OrdinalIgnoreCase"/>.
/// <see cref="StringComparer.OrdinalIgnoreCase"/> is used first, but <see cref="StringComparer.Ordinal"/> is used as a tie-breaker.
/// </summary>
/// <remarks>
/// This comparer's behaviour somewhat emulates <see cref="StringComparer.InvariantCulture"/>,
/// but non-ordinal comparers - both culture-aware and culture-invariant - have huge performance overheads due to i18n factors (up to 5x slower).
/// </remarks>
/// <example>
/// Given the following strings to sort: <c>[A, B, C, D, a, b, c, d, A]</c> and a stable sorting algorithm:
/// <list type="bullet">
/// <item>
/// <see cref="StringComparer.Ordinal"/> would return <c>[A, A, B, C, D, a, b, c, d]</c>.
/// This is undesirable as letters are interleaved.
/// </item>
/// <item>
/// <see cref="StringComparer.OrdinalIgnoreCase"/> would return <c>[A, a, A, B, b, C, c, D, d]</c>.
/// Different letters are not interleaved, but because case is ignored, the As are left in arbitrary order.
/// </item>
/// </list>
/// <item>
/// <see cref="OrdinalSortByCaseStringComparer"/> would return <c>[a, A, A, b, B, c, C, d, D]</c>, which is the expected behaviour.
/// </item>
/// </example>
public class OrdinalSortByCaseStringComparer : IComparer<string>
{
public static readonly OrdinalSortByCaseStringComparer DEFAULT = new OrdinalSortByCaseStringComparer();
private OrdinalSortByCaseStringComparer()
{
}
public int Compare(string? a, string? b)
{
int result = StringComparer.OrdinalIgnoreCase.Compare(a, b);
if (result == 0)
result = -StringComparer.Ordinal.Compare(a, b); // negative to place lowercase letters before uppercase.
return result;
}
}
}

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.217.0" />
<PackageReference Include="ppy.osu.Framework" Version="2024.221.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.207.0" />
<PackageReference Include="Sentry" Version="3.41.3" />
<!-- 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.217.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2024.221.0" />
</ItemGroup>
</Project>