1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 09:27:29 +08:00

Merge branch 'master' into limited-distance-spacing

This commit is contained in:
Dean Herbert 2023-06-16 16:56:22 +09:00
commit 003949ac4e
70 changed files with 1017 additions and 371 deletions

View File

@ -15,7 +15,7 @@
]
},
"codefilesanity": {
"version": "0.0.36",
"version": "0.0.37",
"commands": [
"CodeFileSanity"
]

View File

@ -1,22 +1,29 @@
// 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.Extensions.ObjectExtensions;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public partial class TestSceneOsuModAutoplay : OsuModTestScene
{
protected override bool AllowFail => true;
[Test]
public void TestCursorPositionStoredToJudgement()
{
@ -44,6 +51,36 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
FinalRate = { Value = 1.3 }
});
[Test]
public void TestPerfectScoreOnShortSliderWithRepeat()
{
AddStep("set score to standardised", () => LocalConfig.SetValue(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised));
CreateModTest(new ModTestData
{
Autoplay = true,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new Slider
{
StartTime = 500,
Position = new Vector2(256, 192),
Path = new SliderPath(new[]
{
new PathControlPoint(),
new PathControlPoint(new Vector2(0, 6.25f))
}),
RepeatCount = 1,
SliderVelocity = 10
}
}
},
PassCondition = () => Player.ScoreProcessor.TotalScore.Value == 1_000_000
});
}
private void runSpmTest(Mod mod)
{
SpinnerSpmCalculator? spmCalculator = null;

View File

@ -75,18 +75,24 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
[BackgroundDependencyLoader]
private void load()
{
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both };
AddRangeInternal(new Drawable[]
{
shakeContainer = new ShakeContainer
{
ShakeDuration = 30,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
Children = new[]
{
Body = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling),
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both },
// proxied here so that the tail is drawn under repeats/ticks - legacy skins rely on this
tailContainer.CreateProxy(),
tickContainer = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both },
repeatContainer = new Container<DrawableSliderRepeat> { RelativeSizeAxes = Axes.Both },
// actual tail container is placed here to ensure that tail hitobjects are processed after ticks/repeats.
// this is required for the correct operation of Score V2.
tailContainer,
}
},
// slider head is not included in shake as it handles hit detection, and handles its own shaking.

View File

@ -48,21 +48,26 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
private Bindable<bool> configHitLighting = null!;
private static readonly Vector2 circle_size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
[Resolved]
private DrawableHitObject drawableObject { get; set; } = null!;
public ArgonMainCirclePiece(bool withOuterFill)
{
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
Size = circle_size;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
InternalChildren = new Drawable[]
{
outerFill = new Circle // renders white outer border and dark fill
outerFill = new Circle // renders dark fill
{
Size = Size,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
// Slightly inset to prevent bleeding outside the ring
Size = circle_size - new Vector2(1),
Alpha = withOuterFill ? 1 : 0,
},
outerGradient = new Circle // renders the outer bright gradient
@ -88,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
Masking = true,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = Size,
Size = circle_size,
Child = new KiaiFlash
{
RelativeSizeAxes = Axes.Both,

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
private const double pre_beat_transition_time = 80;
private const float flash_opacity = 0.3f;
private const float kiai_flash_opacity = 0.15f;
private ColourInfo accentColour;
@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
if (drawableHitObject.State.Value == ArmedState.Idle)
{
flash
.FadeTo(flash_opacity)
.FadeTo(kiai_flash_opacity)
.Then()
.FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine);
}

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
private const double pre_beat_transition_time = 80;
private const float flash_opacity = 0.3f;
private const float kiai_flash_opacity = 0.15f;
[Resolved]
private DrawableHitObject drawableHitObject { get; set; } = null!;
@ -187,7 +187,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
if (drawableHitObject.State.Value == ArmedState.Idle)
{
flashBox
.FadeTo(flash_opacity)
.FadeTo(kiai_flash_opacity)
.Then()
.FadeOut(timingPoint.BeatLength * 0.75, Easing.OutSine);
}

View File

@ -7,12 +7,15 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Gameplay
{
@ -21,6 +24,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{
private GameplayClockContainer gameplayClockContainer = null!;
private Box background = null!;
private const double skip_target_time = -2000;
[BackgroundDependencyLoader]
@ -30,11 +35,20 @@ namespace osu.Game.Tests.Visual.Gameplay
FrameStabilityContainer frameStabilityContainer;
Add(gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time)
AddRange(new Drawable[]
{
Child = frameStabilityContainer = new FrameStabilityContainer
background = new Box
{
MaxCatchUpFrames = 1
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue
},
gameplayClockContainer = new MasterGameplayClockContainer(Beatmap.Value, skip_target_time)
{
Child = frameStabilityContainer = new FrameStabilityContainer
{
MaxCatchUpFrames = 1
}
}
});
@ -71,9 +85,20 @@ namespace osu.Game.Tests.Visual.Gameplay
applyToArgonProgress(s => s.ShowGraph.Value = b);
});
AddStep("set white background", () => background.FadeColour(Color4.White, 200, Easing.OutQuint));
AddStep("randomise background colour", () => background.FadeColour(new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1), 200, Easing.OutQuint));
AddStep("stop", gameplayClockContainer.Stop);
}
[Test]
public void TestSeekToKnownTime()
{
AddStep("seek to known time", () => gameplayClockContainer.Seek(60000));
AddWaitStep("wait some for seek", 15);
AddStep("stop", () => gameplayClockContainer.Stop());
}
private void applyToArgonProgress(Action<ArgonSongProgress> action) =>
this.ChildrenOfType<ArgonSongProgress>().ForEach(action);

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Linq;
using System.Threading;
@ -24,8 +22,8 @@ namespace osu.Game.Tests.Visual.Online
{
private readonly APIUser streamingUser = new APIUser { Id = 2, Username = "Test user" };
private TestSpectatorClient spectatorClient;
private CurrentlyPlayingDisplay currentlyPlaying;
private TestSpectatorClient spectatorClient = null!;
private CurrentlyPlayingDisplay currentlyPlaying = null!;
[SetUpSteps]
public void SetUpSteps()
@ -88,13 +86,13 @@ namespace osu.Game.Tests.Visual.Online
"pishifat"
};
protected override Task<APIUser> ComputeValueAsync(int lookup, CancellationToken token = default)
protected override Task<APIUser?> ComputeValueAsync(int lookup, CancellationToken token = default)
{
// tests against failed lookups
if (lookup == 13)
return Task.FromResult<APIUser>(null);
return Task.FromResult<APIUser?>(null);
return Task.FromResult(new APIUser
return Task.FromResult<APIUser?>(new APIUser
{
Id = lookup,
Username = usernames[lookup % usernames.Length],

View File

@ -453,6 +453,25 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
}
[Test]
public void TestRewindToDeletedBeatmap()
{
loadBeatmaps();
var firstAdded = TestResources.CreateTestBeatmapSetInfo();
AddStep("add new set", () => carousel.UpdateBeatmapSet(firstAdded));
AddStep("select set", () => carousel.SelectBeatmap(firstAdded.Beatmaps.First()));
nextRandom();
AddStep("delete set", () => carousel.RemoveBeatmapSet(firstAdded));
prevRandom();
AddAssert("deleted set not selected", () => carousel.SelectedBeatmapSet?.Equals(firstAdded) == false);
}
/// <summary>
/// Test adding and removing beatmap sets
/// </summary>

View File

@ -1,10 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@ -24,10 +23,10 @@ namespace osu.Game.Tests.Visual.SongSelect
{
public partial class TestSceneBeatmapMetadataDisplay : OsuTestScene
{
private BeatmapMetadataDisplay display;
private BeatmapMetadataDisplay display = null!;
[Resolved]
private BeatmapManager manager { get; set; }
private BeatmapManager manager { get; set; } = null!;
[Cached(typeof(BeatmapDifficultyCache))]
private readonly TestBeatmapDifficultyCache testDifficultyCache = new TestBeatmapDifficultyCache();
@ -121,7 +120,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private partial class TestBeatmapDifficultyCache : BeatmapDifficultyCache
{
private TaskCompletionSource<bool> calculationBlocker;
private TaskCompletionSource<bool>? calculationBlocker;
private bool blockCalculation;
@ -142,10 +141,13 @@ namespace osu.Game.Tests.Visual.SongSelect
}
}
public override async Task<StarDifficulty?> GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo rulesetInfo = null, IEnumerable<Mod> mods = null, CancellationToken cancellationToken = default)
public override async Task<StarDifficulty?> GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo? rulesetInfo = null, IEnumerable<Mod>? mods = null, CancellationToken cancellationToken = default)
{
if (blockCalculation)
{
Debug.Assert(calculationBlocker != null);
await calculationBlocker.Task.ConfigureAwait(false);
}
return await base.GetDifficultyAsync(beatmapInfo, rulesetInfo, mods, cancellationToken).ConfigureAwait(false);
}

View File

@ -24,6 +24,9 @@ namespace osu.Game.Tournament.Tests.Screens
Add(screen = new MapPoolScreen { Width = 0.7f });
}
[SetUp]
public void SetUp() => Schedule(() => Ladder.SplitMapPoolByMods.Value = true);
[Test]
public void TestFewMaps()
{
@ -92,7 +95,7 @@ namespace osu.Game.Tournament.Tests.Screens
Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear();
for (int i = 0; i < 11; i++)
addBeatmap(i > 4 ? $"M{i}" : "NM");
addBeatmap(i > 4 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM");
});
AddStep("reset match", () =>
@ -118,7 +121,7 @@ namespace osu.Game.Tournament.Tests.Screens
Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear();
for (int i = 0; i < 12; i++)
addBeatmap(i > 4 ? $"M{i}" : "NM");
addBeatmap(i > 4 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM");
});
AddStep("reset match", () =>
@ -130,7 +133,27 @@ namespace osu.Game.Tournament.Tests.Screens
assertThreeWide();
}
private void addBeatmap(string mods = "nm")
[Test]
public void TestSplitMapPoolByMods()
{
AddStep("load many maps", () =>
{
Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Clear();
for (int i = 0; i < 12; i++)
addBeatmap(i > 4 ? Ruleset.Value.CreateInstance().AllMods.ElementAt(i).Acronym : "NM");
});
AddStep("disable splitting map pool by mods", () => Ladder.SplitMapPoolByMods.Value = false);
AddStep("reset match", () =>
{
Ladder.CurrentMatch.Value = new TournamentMatch();
Ladder.CurrentMatch.Value = Ladder.Matches.First();
});
}
private void addBeatmap(string mods = "NM")
{
Ladder.CurrentMatch.Value.Round.Value.Beatmaps.Add(new RoundBeatmap
{

View File

@ -42,5 +42,7 @@ namespace osu.Game.Tournament.Models
};
public Bindable<bool> AutoProgressScreens = new BindableBool(true);
public Bindable<bool> SplitMapPoolByMods = new BindableBool(true);
}
}

View File

@ -24,7 +24,7 @@ namespace osu.Game.Tournament.Screens.MapPool
{
public partial class MapPoolScreen : TournamentMatchScreen
{
private readonly FillFlowContainer<FillFlowContainer<TournamentBeatmapPanel>> mapFlows;
private FillFlowContainer<FillFlowContainer<TournamentBeatmapPanel>> mapFlows;
[Resolved(canBeNull: true)]
private TournamentSceneManager sceneManager { get; set; }
@ -32,12 +32,13 @@ namespace osu.Game.Tournament.Screens.MapPool
private TeamColour pickColour;
private ChoiceType pickType;
private readonly OsuButton buttonRedBan;
private readonly OsuButton buttonBlueBan;
private readonly OsuButton buttonRedPick;
private readonly OsuButton buttonBluePick;
private OsuButton buttonRedBan;
private OsuButton buttonBlueBan;
private OsuButton buttonRedPick;
private OsuButton buttonBluePick;
public MapPoolScreen()
[BackgroundDependencyLoader]
private void load(MatchIPCInfo ipc)
{
InternalChildren = new Drawable[]
{
@ -98,15 +99,26 @@ namespace osu.Game.Tournament.Screens.MapPool
Action = reset
},
new ControlPanel.Spacer(),
new OsuCheckbox
{
LabelText = "Split display by mods",
Current = LadderInfo.SplitMapPoolByMods,
},
},
}
};
ipc.Beatmap.BindValueChanged(beatmapChanged);
}
[BackgroundDependencyLoader]
private void load(MatchIPCInfo ipc)
private Bindable<bool> splitMapPoolByMods;
protected override void LoadComplete()
{
ipc.Beatmap.BindValueChanged(beatmapChanged);
base.LoadComplete();
splitMapPoolByMods = LadderInfo.SplitMapPoolByMods.GetBoundCopy();
splitMapPoolByMods.BindValueChanged(_ => updateDisplay());
}
private void beatmapChanged(ValueChangedEvent<TournamentBeatmap> beatmap)
@ -213,24 +225,27 @@ namespace osu.Game.Tournament.Screens.MapPool
protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match)
{
base.CurrentMatchChanged(match);
updateDisplay();
}
private void updateDisplay()
{
mapFlows.Clear();
if (match.NewValue == null)
if (CurrentMatch.Value == null)
return;
int totalRows = 0;
if (match.NewValue.Round.Value != null)
if (CurrentMatch.Value.Round.Value != null)
{
FillFlowContainer<TournamentBeatmapPanel> currentFlow = null;
string currentMod = null;
string currentMods = null;
int flowCount = 0;
foreach (var b in match.NewValue.Round.Value.Beatmaps)
foreach (var b in CurrentMatch.Value.Round.Value.Beatmaps)
{
if (currentFlow == null || currentMod != b.Mods)
if (currentFlow == null || (LadderInfo.SplitMapPoolByMods.Value && currentMods != b.Mods))
{
mapFlows.Add(currentFlow = new FillFlowContainer<TournamentBeatmapPanel>
{
@ -240,7 +255,7 @@ namespace osu.Game.Tournament.Screens.MapPool
AutoSizeAxes = Axes.Y
});
currentMod = b.Mods;
currentMods = b.Mods;
totalRows++;
flowCount = 0;

View File

@ -17,9 +17,8 @@ namespace osu.Game.Beatmaps
// If issues are found it's worth checking to make sure similar issues exist there.
public class BeatmapPanelBackgroundTextureLoaderStore : IResourceStore<TextureUpload>
{
// These numbers are taken from the draw visualiser size requirements for song select panel textures at extreme aspect ratios.
private const int max_height = 130;
private const int max_width = 1280;
// The aspect ratio of SetPanelBackground at its maximum size (very tall window).
private const float minimum_display_ratio = 512 / 80f;
private readonly IResourceStore<TextureUpload>? textureStore;
@ -66,14 +65,21 @@ namespace osu.Game.Beatmaps
textureUpload.Dispose();
Size size = image.Size();
int usableWidth = Math.Min(max_width, size.Width);
int usableHeight = Math.Min(max_height, size.Height);
// Assume that panel backgrounds are always displayed using `FillMode.Fill`.
// Also assume that all backgrounds are wider than they are tall, so the
// fill is always going to be based on width.
//
// We need to include enough height to make this work for all ratio panels are displayed at.
int usableHeight = (int)Math.Ceiling(size.Width * 1 / minimum_display_ratio);
usableHeight = Math.Min(size.Height, usableHeight);
// Crop the centre region of the background for now.
Rectangle cropRectangle = new Rectangle(
(size.Width - usableWidth) / 2,
0,
(size.Height - usableHeight) / 2,
usableWidth,
size.Width,
usableHeight
);

View File

@ -106,12 +106,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
new Drawable[]
{
new OsuSpriteText
new TruncatingSpriteText
{
Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title),
Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
Truncate = true
},
titleBadgeArea = new FillFlowContainer
{
@ -140,21 +139,19 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
new[]
{
new OsuSpriteText
new TruncatingSpriteText
{
Text = createArtistText(),
Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
Truncate = true
},
Empty()
},
}
},
new OsuSpriteText
new TruncatingSpriteText
{
RelativeSizeAxes = Axes.X,
Truncate = true,
Text = BeatmapSet.Source,
Shadow = false,
Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold),

View File

@ -107,12 +107,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
new Drawable[]
{
new OsuSpriteText
new TruncatingSpriteText
{
Text = new RomanisableString(BeatmapSet.TitleUnicode, BeatmapSet.Title),
Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
Truncate = true
},
titleBadgeArea = new FillFlowContainer
{
@ -141,12 +140,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
new[]
{
new OsuSpriteText
new TruncatingSpriteText
{
Text = createArtistText(),
Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
Truncate = true
},
Empty()
},

View File

@ -264,7 +264,7 @@ namespace osu.Game.Beatmaps
if (beatmapFileStream == null)
{
Logger.Log($"Beatmap failed to load (file {BeatmapInfo.Path} not found on disk at expected location {fileStorePath})", level: LogLevel.Error);
return null;
return new Storyboard();
}
using (var reader = new LineBufferedReader(beatmapFileStream))

View File

@ -6,6 +6,7 @@
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
namespace osu.Game.Configuration
{
@ -21,6 +22,7 @@ namespace osu.Game.Configuration
SetDefault(Static.LowBatteryNotificationShownOnce, false);
SetDefault(Static.FeaturedArtistDisclaimerShownOnce, false);
SetDefault(Static.LastHoverSoundPlaybackTime, (double?)null);
SetDefault(Static.LastModSelectPanelSamplePlaybackTime, (double?)null);
SetDefault<APISeasonalBackgrounds>(Static.SeasonalBackgrounds, null);
}
@ -56,5 +58,11 @@ namespace osu.Game.Configuration
/// Used to debounce hover sounds game-wide to avoid volume saturation, especially in scrolling views with many UI controls like <see cref="SettingsOverlay"/>.
/// </summary>
LastHoverSoundPlaybackTime,
/// <summary>
/// The last playback time in milliseconds of an on/off sample (from <see cref="ModSelectPanel"/>).
/// Used to debounce <see cref="ModSelectPanel"/> on/off sounds game-wide to avoid volume saturation, especially in activating mod presets with many mods.
/// </summary>
LastModSelectPanelSamplePlaybackTime
}
}

View File

@ -1,13 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@ -21,8 +18,7 @@ namespace osu.Game.Database
/// <param name="beatmapId">The beatmap to lookup.</param>
/// <param name="token">An optional cancellation token.</param>
/// <returns>The populated beatmap, or null if the beatmap does not exist or the request could not be satisfied.</returns>
[ItemCanBeNull]
public Task<APIBeatmap> GetBeatmapAsync(int beatmapId, CancellationToken token = default) => LookupAsync(beatmapId, token);
public Task<APIBeatmap?> GetBeatmapAsync(int beatmapId, CancellationToken token = default) => LookupAsync(beatmapId, token);
/// <summary>
/// Perform an API lookup on the specified beatmaps, populating a <see cref="APIBeatmap"/> model.
@ -30,10 +26,10 @@ namespace osu.Game.Database
/// <param name="beatmapIds">The beatmaps to lookup.</param>
/// <param name="token">An optional cancellation token.</param>
/// <returns>The populated beatmaps. May include null results for failed retrievals.</returns>
public Task<APIBeatmap[]> GetBeatmapsAsync(int[] beatmapIds, CancellationToken token = default) => LookupAsync(beatmapIds, token);
public Task<APIBeatmap?[]> GetBeatmapsAsync(int[] beatmapIds, CancellationToken token = default) => LookupAsync(beatmapIds, token);
protected override GetBeatmapsRequest CreateRequest(IEnumerable<int> ids) => new GetBeatmapsRequest(ids.ToArray());
protected override IEnumerable<APIBeatmap> RetrieveResults(GetBeatmapsRequest request) => request.Response?.Beatmaps;
protected override IEnumerable<APIBeatmap>? RetrieveResults(GetBeatmapsRequest request) => request.Response?.Beatmaps;
}
}

View File

@ -1,13 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Statistics;
@ -19,8 +17,9 @@ namespace osu.Game.Database
/// Currently not persisted between game sessions.
/// </summary>
public abstract partial class MemoryCachingComponent<TLookup, TValue> : Component
where TLookup : notnull
{
private readonly ConcurrentDictionary<TLookup, TValue> cache = new ConcurrentDictionary<TLookup, TValue>();
private readonly ConcurrentDictionary<TLookup, TValue?> cache = new ConcurrentDictionary<TLookup, TValue?>();
private readonly GlobalStatistic<MemoryCachingStatistics> statistics;
@ -37,12 +36,12 @@ namespace osu.Game.Database
/// </summary>
/// <param name="lookup">The lookup to retrieve.</param>
/// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param>
protected async Task<TValue> GetAsync([NotNull] TLookup lookup, CancellationToken token = default)
protected async Task<TValue?> GetAsync(TLookup lookup, CancellationToken token = default)
{
if (CheckExists(lookup, out TValue performance))
if (CheckExists(lookup, out TValue? existing))
{
statistics.Value.HitCount++;
return performance;
return existing;
}
var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false);
@ -73,7 +72,7 @@ namespace osu.Game.Database
statistics.Value.Usage = cache.Count;
}
protected bool CheckExists([NotNull] TLookup lookup, out TValue value) =>
protected bool CheckExists(TLookup lookup, [MaybeNullWhen(false)] out TValue value) =>
cache.TryGetValue(lookup, out value);
/// <summary>
@ -82,7 +81,7 @@ namespace osu.Game.Database
/// <param name="lookup">The lookup to retrieve.</param>
/// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param>
/// <returns>The computed value.</returns>
protected abstract Task<TValue> ComputeValueAsync(TLookup lookup, CancellationToken token = default);
protected abstract Task<TValue?> ComputeValueAsync(TLookup lookup, CancellationToken token = default);
private class MemoryCachingStatistics
{

View File

@ -1,14 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Game.Online.API;
@ -21,7 +18,7 @@ namespace osu.Game.Database
where TRequest : APIRequest
{
[Resolved]
private IAPIProvider api { get; set; }
private IAPIProvider api { get; set; } = null!;
/// <summary>
/// Creates an <see cref="APIRequest"/> to retrieve the values for a given collection of <typeparamref name="TLookup"/>s.
@ -32,8 +29,7 @@ namespace osu.Game.Database
/// <summary>
/// Retrieves a list of <typeparamref name="TValue"/>s from a successful <typeparamref name="TRequest"/> created by <see cref="CreateRequest"/>.
/// </summary>
[CanBeNull]
protected abstract IEnumerable<TValue> RetrieveResults(TRequest request);
protected abstract IEnumerable<TValue>? RetrieveResults(TRequest request);
/// <summary>
/// Perform a lookup using the specified <paramref name="id"/>, populating a <typeparamref name="TValue"/>.
@ -41,8 +37,7 @@ namespace osu.Game.Database
/// <param name="id">The ID to lookup.</param>
/// <param name="token">An optional cancellation token.</param>
/// <returns>The populated <typeparamref name="TValue"/>, or null if the value does not exist or the request could not be satisfied.</returns>
[ItemCanBeNull]
protected Task<TValue> LookupAsync(TLookup id, CancellationToken token = default) => GetAsync(id, token);
protected Task<TValue?> LookupAsync(TLookup id, CancellationToken token = default) => GetAsync(id, token);
/// <summary>
/// Perform an API lookup on the specified <paramref name="ids"/>, populating a <typeparamref name="TValue"/>.
@ -50,9 +45,9 @@ namespace osu.Game.Database
/// <param name="ids">The IDs to lookup.</param>
/// <param name="token">An optional cancellation token.</param>
/// <returns>The populated values. May include null results for failed retrievals.</returns>
protected Task<TValue[]> LookupAsync(TLookup[] ids, CancellationToken token = default)
protected Task<TValue?[]> LookupAsync(TLookup[] ids, CancellationToken token = default)
{
var lookupTasks = new List<Task<TValue>>();
var lookupTasks = new List<Task<TValue?>>();
foreach (var id in ids)
{
@ -69,18 +64,18 @@ namespace osu.Game.Database
}
// cannot be sealed due to test usages (see TestUserLookupCache).
protected override async Task<TValue> ComputeValueAsync(TLookup lookup, CancellationToken token = default)
protected override async Task<TValue?> ComputeValueAsync(TLookup lookup, CancellationToken token = default)
=> await queryValue(lookup).ConfigureAwait(false);
private readonly Queue<(TLookup id, TaskCompletionSource<TValue>)> pendingTasks = new Queue<(TLookup, TaskCompletionSource<TValue>)>();
private Task pendingRequestTask;
private readonly Queue<(TLookup id, TaskCompletionSource<TValue?>)> pendingTasks = new Queue<(TLookup, TaskCompletionSource<TValue?>)>();
private Task? pendingRequestTask;
private readonly object taskAssignmentLock = new object();
private Task<TValue> queryValue(TLookup id)
private Task<TValue?> queryValue(TLookup id)
{
lock (taskAssignmentLock)
{
var tcs = new TaskCompletionSource<TValue>();
var tcs = new TaskCompletionSource<TValue?>();
// Add to the queue.
pendingTasks.Enqueue((id, tcs));
@ -96,14 +91,14 @@ namespace osu.Game.Database
private async Task performLookup()
{
// contains at most 50 unique IDs from tasks, which is used to perform the lookup.
var nextTaskBatch = new Dictionary<TLookup, List<TaskCompletionSource<TValue>>>();
var nextTaskBatch = new Dictionary<TLookup, List<TaskCompletionSource<TValue?>>>();
// Grab at most 50 unique IDs from the queue.
lock (taskAssignmentLock)
{
while (pendingTasks.Count > 0 && nextTaskBatch.Count < 50)
{
(TLookup id, TaskCompletionSource<TValue> task) next = pendingTasks.Dequeue();
(TLookup id, TaskCompletionSource<TValue?> task) next = pendingTasks.Dequeue();
// Perform a secondary check for existence, in case the value was queried in a previous batch.
if (CheckExists(next.id, out var existing))
@ -113,7 +108,7 @@ namespace osu.Game.Database
if (nextTaskBatch.TryGetValue(next.id, out var tasks))
tasks.Add(next.task);
else
nextTaskBatch[next.id] = new List<TaskCompletionSource<TValue>> { next.task };
nextTaskBatch[next.id] = new List<TaskCompletionSource<TValue?>> { next.task };
}
}
}

View File

@ -22,12 +22,15 @@ using osu.Framework.Statistics;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Input.Bindings;
using osu.Game.IO.Legacy;
using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Skinning;
using Realms;
using Realms.Exceptions;
@ -72,8 +75,10 @@ namespace osu.Game.Database
/// 25 2022-09-18 Remove skins to add with new naming.
/// 26 2023-02-05 Added BeatmapHash to ScoreInfo.
/// 27 2023-06-06 Added EditorTimestamp to BeatmapInfo.
/// 28 2023-06-08 Added IsLegacyScore to ScoreInfo, parsed from replay files.
/// 29 2023-06-12 Run migration of old lazer scores to be best-effort in the new scoring number space. No actual realm changes.
/// </summary>
private const int schema_version = 27;
private const int schema_version = 29;
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
@ -720,6 +725,11 @@ namespace osu.Game.Database
private void applyMigrationsForVersion(Migration migration, ulong targetVersion)
{
Logger.Log($"Running realm migration to version {targetVersion}...");
Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
switch (targetVersion)
{
case 7:
@ -880,6 +890,7 @@ namespace osu.Game.Database
break;
case 26:
{
// Add ScoreInfo.BeatmapHash property to ensure scores correspond to the correct version of beatmap.
var scores = migration.NewRealm.All<ScoreInfo>();
@ -887,7 +898,75 @@ namespace osu.Game.Database
score.BeatmapHash = score.BeatmapInfo.Hash;
break;
}
case 28:
{
var files = new RealmFileStore(this, storage);
var scores = migration.NewRealm.All<ScoreInfo>();
foreach (var score in scores)
{
string? replayFilename = score.Files.FirstOrDefault(f => f.Filename.EndsWith(@".osr", StringComparison.InvariantCultureIgnoreCase))?.File.GetStoragePath();
if (replayFilename == null)
continue;
try
{
using (var stream = files.Store.GetStream(replayFilename))
{
if (stream == null)
continue;
// Trimmed down logic from LegacyScoreDecoder to extract the version from replays.
using (SerializationReader sr = new SerializationReader(stream))
{
sr.ReadByte(); // Ruleset.
int version = sr.ReadInt32();
if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION)
score.IsLegacyScore = true;
}
}
}
catch (Exception e)
{
Logger.Error(e, $"Failed to read replay {replayFilename} during score migration", LoggingTarget.Database);
}
}
break;
}
case 29:
{
var scores = migration.NewRealm
.All<ScoreInfo>()
.Where(s => !s.IsLegacyScore);
foreach (var score in scores)
{
try
{
if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(score))
{
try
{
long calculatedNew = StandardisedScoreMigrationTools.GetNewStandardised(score);
score.TotalScore = calculatedNew;
}
catch
{
}
}
}
catch { }
}
break;
}
}
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
}
private string? getRulesetShortNameFromLegacyID(long rulesetId)

View File

@ -0,0 +1,210 @@
// 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 System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Database
{
public static class StandardisedScoreMigrationTools
{
public static bool ShouldMigrateToNewStandardised(ScoreInfo score)
{
if (score.IsLegacyScore)
return false;
// Recalculate the old-style standardised score to see if this was an old lazer score.
bool oldScoreMatchesExpectations = GetOldStandardised(score) == score.TotalScore;
// Some older scores don't have correct statistics populated, so let's give them benefit of doubt.
bool scoreIsVeryOld = score.Date < new DateTime(2023, 1, 1, 0, 0, 0);
return oldScoreMatchesExpectations || scoreIsVeryOld;
}
public static long GetNewStandardised(ScoreInfo score)
{
int maxJudgementIndex = 0;
// Avoid retrieving from realm inside loops.
int maxCombo = score.MaxCombo;
var ruleset = score.Ruleset.CreateInstance();
var processor = ruleset.CreateScoreProcessor();
processor.TrackHitEvents = false;
var beatmap = new Beatmap();
HitResult maxRulesetJudgement = ruleset.GetHitResults().First().result;
// This is a list of all results, ordered from best to worst.
// We are constructing a "best possible" score from the statistics provided because it's the best we can do.
List<HitResult> sortedHits = score.Statistics
.Where(kvp => kvp.Key.AffectsCombo())
.OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key))
.SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value))
.ToList();
// Attempt to use maximum statistics from the database.
var maximumJudgements = score.MaximumStatistics
.Where(kvp => kvp.Key.AffectsCombo())
.OrderByDescending(kvp => Judgement.ToNumericResult(kvp.Key))
.SelectMany(kvp => Enumerable.Repeat(new FakeJudgement(kvp.Key), kvp.Value))
.ToList();
// Some older scores may not have maximum statistics populated correctly.
// In this case we need to fill them with best-known-defaults.
if (maximumJudgements.Count != sortedHits.Count)
{
maximumJudgements = sortedHits
.Select(r => new FakeJudgement(getMaxJudgementFor(r, maxRulesetJudgement)))
.ToList();
}
// This is required to get the correct maximum combo portion.
foreach (var judgement in maximumJudgements)
beatmap.HitObjects.Add(new FakeHit(judgement));
processor.ApplyBeatmap(beatmap);
processor.Mods.Value = score.Mods;
// Insert all misses into a queue.
// These will be nibbled at whenever we need to reset the combo.
Queue<HitResult> misses = new Queue<HitResult>(score.Statistics
.Where(kvp => kvp.Key == HitResult.Miss || kvp.Key == HitResult.LargeTickMiss)
.SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value)));
foreach (var result in sortedHits)
{
// For the main part of this loop, ignore all misses, as they will be inserted from the queue.
if (result == HitResult.Miss || result == HitResult.LargeTickMiss)
continue;
// Reset combo if required.
if (processor.Combo.Value == maxCombo)
insertMiss();
processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++])
{
Type = result
});
}
// Ensure we haven't forgotten any misses.
while (misses.Count > 0)
insertMiss();
var bonusHits = score.Statistics
.Where(kvp => kvp.Key.IsBonus())
.SelectMany(kvp => Enumerable.Repeat(kvp.Key, kvp.Value));
foreach (var result in bonusHits)
processor.ApplyResult(new JudgementResult(null!, new FakeJudgement(result)) { Type = result });
// Not true for all scores for whatever reason. Oh well.
// Debug.Assert(processor.HighestCombo.Value == score.MaxCombo);
return processor.TotalScore.Value;
void insertMiss()
{
if (misses.Count > 0)
{
processor.ApplyResult(new JudgementResult(null!, maximumJudgements[maxJudgementIndex++])
{
Type = misses.Dequeue(),
});
}
else
{
// We ran out of misses. But we can't let max combo increase beyond the known value,
// so let's forge a miss.
processor.ApplyResult(new JudgementResult(null!, new FakeJudgement(getMaxJudgementFor(HitResult.Miss, maxRulesetJudgement)))
{
Type = HitResult.Miss,
});
}
}
}
private static HitResult getMaxJudgementFor(HitResult hitResult, HitResult max)
{
switch (hitResult)
{
case HitResult.Miss:
case HitResult.Meh:
case HitResult.Ok:
case HitResult.Good:
case HitResult.Great:
case HitResult.Perfect:
return max;
case HitResult.SmallTickMiss:
case HitResult.SmallTickHit:
return HitResult.SmallTickHit;
case HitResult.LargeTickMiss:
case HitResult.LargeTickHit:
return HitResult.LargeTickHit;
}
return HitResult.IgnoreHit;
}
public static long GetOldStandardised(ScoreInfo score)
{
double accuracyScore =
(double)score.Statistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value)
/ score.MaximumStatistics.Where(kvp => kvp.Key.AffectsAccuracy()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value);
double comboScore = (double)score.MaxCombo / score.MaximumStatistics.Where(kvp => kvp.Key.AffectsCombo()).Sum(kvp => kvp.Value);
double bonusScore = score.Statistics.Where(kvp => kvp.Key.IsBonus()).Sum(kvp => Judgement.ToNumericResult(kvp.Key) * kvp.Value);
double accuracyPortion = 0.3;
switch (score.RulesetID)
{
case 1:
accuracyPortion = 0.75;
break;
case 3:
accuracyPortion = 0.99;
break;
}
double modMultiplier = 1;
foreach (var mod in score.Mods)
modMultiplier *= mod.ScoreMultiplier;
return (long)((1000000 * (accuracyPortion * accuracyScore + (1 - accuracyPortion) * comboScore) + bonusScore) * modMultiplier);
}
private class FakeHit : HitObject
{
private readonly Judgement judgement;
public override Judgement CreateJudgement() => judgement;
public FakeHit(Judgement judgement)
{
this.judgement = judgement;
}
}
private class FakeJudgement : Judgement
{
public override HitResult MaxResult { get; }
public FakeJudgement(HitResult maxResult)
{
MaxResult = maxResult;
}
}
}
}

View File

@ -1,13 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@ -21,8 +18,7 @@ namespace osu.Game.Database
/// <param name="userId">The user to lookup.</param>
/// <param name="token">An optional cancellation token.</param>
/// <returns>The populated user, or null if the user does not exist or the request could not be satisfied.</returns>
[ItemCanBeNull]
public Task<APIUser> GetUserAsync(int userId, CancellationToken token = default) => LookupAsync(userId, token);
public Task<APIUser?> GetUserAsync(int userId, CancellationToken token = default) => LookupAsync(userId, token);
/// <summary>
/// Perform an API lookup on the specified users, populating a <see cref="APIUser"/> model.
@ -30,10 +26,10 @@ namespace osu.Game.Database
/// <param name="userIds">The users to lookup.</param>
/// <param name="token">An optional cancellation token.</param>
/// <returns>The populated users. May include null results for failed retrievals.</returns>
public Task<APIUser[]> GetUsersAsync(int[] userIds, CancellationToken token = default) => LookupAsync(userIds, token);
public Task<APIUser?[]> GetUsersAsync(int[] userIds, CancellationToken token = default) => LookupAsync(userIds, token);
protected override GetUsersRequest CreateRequest(IEnumerable<int> ids) => new GetUsersRequest(ids.ToArray());
protected override IEnumerable<APIUser> RetrieveResults(GetUsersRequest request) => request.Response?.Users;
protected override IEnumerable<APIUser>? RetrieveResults(GetUsersRequest request) => request.Response?.Users;
}
}

View File

@ -3,12 +3,19 @@
#nullable disable
using System;
using osu.Framework.Graphics.Sprites;
namespace osu.Game.Graphics.Sprites
{
public partial class OsuSpriteText : SpriteText
{
[Obsolete("Use TruncatingSpriteText instead.")]
public new bool Truncate
{
set => throw new InvalidOperationException($"Use {nameof(TruncatingSpriteText)} instead.");
}
public OsuSpriteText()
{
Shadow = true;

View File

@ -0,0 +1,29 @@
// 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.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
namespace osu.Game.Graphics.Sprites
{
/// <summary>
/// A derived version of <see cref="OsuSpriteText"/> which automatically shows non-truncated text in tooltip when required.
/// </summary>
public sealed partial class TruncatingSpriteText : OsuSpriteText, IHasTooltip
{
/// <summary>
/// Whether a tooltip should be shown with non-truncated text on hover.
/// </summary>
public bool ShowTooltip { get; init; } = true;
public LocalisableString TooltipText => Text;
public override bool HandlePositionalInput => IsTruncated && ShowTooltip;
public TruncatingSpriteText()
{
((SpriteText)this).Truncate = true;
}
}
}

View File

@ -335,12 +335,11 @@ namespace osu.Game.Graphics.UserInterface
{
new Drawable[]
{
Text = new OsuSpriteText
Text = new TruncatingSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
Truncate = true,
},
Icon = new SpriteIcon
{

View File

@ -16,6 +16,7 @@ using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Platform;
using osu.Game.Localisation;
namespace osu.Game.Graphics.UserInterface
{
@ -112,7 +113,7 @@ namespace osu.Game.Graphics.UserInterface
private partial class CapsWarning : SpriteIcon, IHasTooltip
{
public LocalisableString TooltipText => "caps lock is active";
public LocalisableString TooltipText => CommonStrings.CapsLockIsActive;
public CapsWarning()
{

View File

@ -0,0 +1,54 @@
// 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 AccountCreationStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.AccountCreation";
/// <summary>
/// "New player registration"
/// </summary>
public static LocalisableString NewPlayerRegistration => new TranslatableString(getKey(@"new_player_registration"), @"New player registration");
/// <summary>
/// "Let&#39;s get you started"
/// </summary>
public static LocalisableString LetsGetYouStarted => new TranslatableString(getKey(@"lets_get_you_started"), @"Let's get you started");
/// <summary>
/// "Let&#39;s create an account!"
/// </summary>
public static LocalisableString LetsCreateAnAccount => new TranslatableString(getKey(@"lets_create_an_account"), @"Let's create an account!");
/// <summary>
/// "Help, I can&#39;t access my account!"
/// </summary>
public static LocalisableString MultiAccountWarningHelp => new TranslatableString(getKey(@"multi_account_warning_help"), @"Help, I can't access my account!");
/// <summary>
/// "I understand. This account isn&#39;t for me."
/// </summary>
public static LocalisableString MultiAccountWarningAccept => new TranslatableString(getKey(@"multi_account_warning_accept"), @"I understand. This account isn't for me.");
/// <summary>
/// "This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!"
/// </summary>
public static LocalisableString UsernameDescription => new TranslatableString(getKey(@"username_description"), @"This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!");
/// <summary>
/// "Will be used for notifications, account verification and in the case you forget your password. No spam, ever."
/// </summary>
public static LocalisableString EmailDescription1 => new TranslatableString(getKey(@"email_description_1"), @"Will be used for notifications, account verification and in the case you forget your password. No spam, ever.");
/// <summary>
/// " Make sure to get it right!"
/// </summary>
public static LocalisableString EmailDescription2 => new TranslatableString(getKey(@"email_description_2"), @" Make sure to get it right!");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -154,6 +154,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString Exit => new TranslatableString(getKey(@"exit"), @"Exit");
/// <summary>
/// "Caps lock is active"
/// </summary>
public static LocalisableString CapsLockIsActive => new TranslatableString(getKey(@"caps_lock_is_active"), @"Caps lock is active");
/// <summary>
/// "Revert to default"
/// </summary>

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 LoginPanelStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.LoginPanel";
/// <summary>
/// "Do not disturb"
/// </summary>
public static LocalisableString DoNotDisturb => new TranslatableString(getKey(@"do_not_disturb"), @"Do not disturb");
/// <summary>
/// "Appear offline"
/// </summary>
public static LocalisableString AppearOffline => new TranslatableString(getKey(@"appear_offline"), @"Appear offline");
/// <summary>
/// "Signed in"
/// </summary>
public static LocalisableString SignedIn => new TranslatableString(getKey(@"signed_in"), @"Signed in");
/// <summary>
/// "Account"
/// </summary>
public static LocalisableString Account => new TranslatableString(getKey(@"account"), @"Account");
/// <summary>
/// "Remember username"
/// </summary>
public static LocalisableString RememberUsername => new TranslatableString(getKey(@"remember_username"), @"Remember username");
/// <summary>
/// "Stay signed in"
/// </summary>
public static LocalisableString StaySignedIn => new TranslatableString(getKey(@"stay_signed_in"), @"Stay signed in");
/// <summary>
/// "Register"
/// </summary>
public static LocalisableString Register => new TranslatableString(getKey(@"register"), @"Register");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
namespace osu.Game.Online.API.Requests

View File

@ -67,7 +67,7 @@ namespace osu.Game.Online.Rooms
{
var beatmap = task.GetResultSafely();
if (SelectedItem.Value?.Beatmap.OnlineID == beatmap.OnlineID)
if (beatmap != null && SelectedItem.Value?.Beatmap.OnlineID == beatmap.OnlineID)
{
selectedBeatmap = beatmap;
beginTracking();

View File

@ -17,6 +17,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Overlays.Settings;
using osu.Game.Resources.Localisation.Web;
@ -71,7 +72,7 @@ namespace osu.Game.Overlays.AccountCreation
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Font = OsuFont.GetFont(size: 20),
Text = "Let's create an account!",
Text = AccountCreationStrings.LetsCreateAnAccount
},
usernameTextBox = new OsuTextBox
{
@ -86,7 +87,7 @@ namespace osu.Game.Overlays.AccountCreation
},
emailTextBox = new OsuTextBox
{
PlaceholderText = "email address",
PlaceholderText = ModelValidationStrings.UserAttributesUserEmail.ToLower(),
RelativeSizeAxes = Axes.X,
TabbableContentContainer = this
},
@ -118,7 +119,7 @@ namespace osu.Game.Overlays.AccountCreation
AutoSizeAxes = Axes.Y,
Child = new SettingsButton
{
Text = "Register",
Text = LoginPanelStrings.Register,
Margin = new MarginPadding { Vertical = 20 },
Action = performRegistration
}
@ -132,10 +133,10 @@ namespace osu.Game.Overlays.AccountCreation
textboxes = new[] { usernameTextBox, emailTextBox, passwordTextBox };
usernameDescription.AddText("This will be your public presence. No profanity, no impersonation. Avoid exposing your own personal details, too!");
usernameDescription.AddText(AccountCreationStrings.UsernameDescription);
emailAddressDescription.AddText("Will be used for notifications, account verification and in the case you forget your password. No spam, ever.");
emailAddressDescription.AddText(" Make sure to get it right!", cp => cp.Font = cp.Font.With(Typeface.Torus, weight: FontWeight.Bold));
emailAddressDescription.AddText(AccountCreationStrings.EmailDescription1);
emailAddressDescription.AddText(AccountCreationStrings.EmailDescription2, cp => cp.Font = cp.Font.With(Typeface.Torus, weight: FontWeight.Bold));
passwordDescription.AddText("At least ");
characterCheckText = passwordDescription.AddText("8 characters long");

View File

@ -17,6 +17,7 @@ using osu.Game.Overlays.Settings;
using osu.Game.Screens.Menu;
using osuTK;
using osuTK.Graphics;
using osu.Game.Localisation;
namespace osu.Game.Overlays.AccountCreation
{
@ -101,13 +102,13 @@ namespace osu.Game.Overlays.AccountCreation
},
new SettingsButton
{
Text = "Help, I can't access my account!",
Text = AccountCreationStrings.MultiAccountWarningHelp,
Margin = new MarginPadding { Top = 50 },
Action = () => game?.OpenUrlExternally(help_centre_url)
},
new DangerousSettingsButton
{
Text = "I understand. This account isn't for me.",
Text = AccountCreationStrings.MultiAccountWarningAccept,
Action = () => this.Push(new ScreenEntry())
},
furtherAssistance = new LinkFlowContainer(cp => cp.Font = cp.Font.With(size: 12))

View File

@ -1,9 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
@ -12,6 +11,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Overlays.Settings;
using osu.Game.Screens.Menu;
using osuTK;
using osu.Game.Localisation;
namespace osu.Game.Overlays.AccountCreation
{
@ -46,18 +46,18 @@ namespace osu.Game.Overlays.AccountCreation
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light),
Text = "New Player Registration",
Text = AccountCreationStrings.NewPlayerRegistration.ToTitle(),
},
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Font = OsuFont.GetFont(size: 12),
Text = "let's get you started",
Text = AccountCreationStrings.LetsGetYouStarted.ToLower(),
},
new SettingsButton
{
Text = "Let's create an account!",
Text = AccountCreationStrings.LetsCreateAnAccount,
Margin = new MarginPadding { Vertical = 120 },
Action = () => this.Push(new ScreenWarning())
}

View File

@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Chat.ChannelList
new Drawable?[]
{
createIcon(),
text = new OsuSpriteText
text = new TruncatingSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
@ -94,7 +94,6 @@ namespace osu.Game.Overlays.Chat.ChannelList
Colour = colourProvider.Light3,
Margin = new MarginPadding { Bottom = 2 },
RelativeSizeAxes = Axes.X,
Truncate = true,
},
createMentionPill(),
close = createCloseButton(),

View File

@ -73,14 +73,13 @@ namespace osu.Game.Overlays.Chat
Width = chatting_text_width,
Masking = true,
Padding = new MarginPadding { Horizontal = padding },
Child = chattingText = new OsuSpriteText
Child = chattingText = new TruncatingSpriteText
{
MaxWidth = chatting_text_width - padding * 2,
Font = OsuFont.Torus.With(size: 20),
Colour = colourProvider.Background1,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Truncate = true,
},
},
searchIconContainer = new Container

View File

@ -83,11 +83,9 @@ namespace osu.Game.Overlays.Chat
Action = openUserProfile;
drawableText = new OsuSpriteText
drawableText = new TruncatingSpriteText
{
Shadow = false,
Truncate = true,
EllipsisString = "…",
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
};

View File

@ -100,17 +100,15 @@ namespace osu.Game.Overlays.Dashboard.Home
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new OsuSpriteText
new TruncatingSpriteText
{
RelativeSizeAxes = Axes.X,
Truncate = true,
Font = OsuFont.GetFont(weight: FontWeight.Regular),
Text = BeatmapSet.Title
},
new OsuSpriteText
new TruncatingSpriteText
{
RelativeSizeAxes = Axes.X,
Truncate = true,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular),
Text = BeatmapSet.Artist
},

View File

@ -16,6 +16,7 @@ using osu.Game.Online.API;
using osu.Game.Overlays.Settings;
using osu.Game.Resources.Localisation.Web;
using osuTK;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Login
{
@ -47,7 +48,7 @@ namespace osu.Game.Overlays.Login
RelativeSizeAxes = Axes.X;
ErrorTextFlowContainer errorText;
LinkFlowContainer forgottenPaswordLink;
LinkFlowContainer forgottenPasswordLink;
Children = new Drawable[]
{
@ -71,15 +72,15 @@ namespace osu.Game.Overlays.Login
},
new SettingsCheckbox
{
LabelText = "Remember username",
LabelText = LoginPanelStrings.RememberUsername,
Current = config.GetBindable<bool>(OsuSetting.SaveUsername),
},
new SettingsCheckbox
{
LabelText = "Stay signed in",
LabelText = LoginPanelStrings.StaySignedIn,
Current = config.GetBindable<bool>(OsuSetting.SavePassword),
},
forgottenPaswordLink = new LinkFlowContainer
forgottenPasswordLink = new LinkFlowContainer
{
Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS },
RelativeSizeAxes = Axes.X,
@ -105,7 +106,7 @@ namespace osu.Game.Overlays.Login
},
new SettingsButton
{
Text = "Register",
Text = LoginPanelStrings.Register,
Action = () =>
{
RequestHide?.Invoke();
@ -114,7 +115,7 @@ namespace osu.Game.Overlays.Login
}
};
forgottenPaswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.WebsiteRootUrl}/home/password-reset");
forgottenPasswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.WebsiteRootUrl}/home/password-reset");
password.OnCommit += (_, _) => performLogin();

View File

@ -6,6 +6,7 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
@ -81,7 +82,7 @@ namespace osu.Game.Overlays.Login
{
new OsuSpriteText
{
Text = "ACCOUNT",
Text = LoginPanelStrings.Account.ToUpper(),
Margin = new MarginPadding { Bottom = 5 },
Font = OsuFont.GetFont(weight: FontWeight.Bold),
},
@ -115,7 +116,7 @@ namespace osu.Game.Overlays.Login
},
};
linkFlow.AddLink("cancel", api.Logout, string.Empty);
linkFlow.AddLink(Resources.Localisation.Web.CommonStrings.ButtonsCancel.ToLower(), api.Logout, string.Empty);
break;
case APIState.Online:
@ -140,7 +141,7 @@ namespace osu.Game.Overlays.Login
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Signed in",
Text = LoginPanelStrings.SignedIn,
Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold),
Margin = new MarginPadding { Top = 5, Bottom = 5 },
},

View File

@ -1,11 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.ComponentModel;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Login
{
@ -14,13 +12,13 @@ namespace osu.Game.Overlays.Login
[LocalisableDescription(typeof(UsersStrings), nameof(UsersStrings.StatusOnline))]
Online,
[Description(@"Do not disturb")]
[LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.DoNotDisturb))]
DoNotDisturb,
[Description(@"Appear offline")]
[LocalisableDescription(typeof(LoginPanelStrings), nameof(LoginPanelStrings.AppearOffline))]
AppearOffline,
[Description(@"Sign out")]
[LocalisableDescription(typeof(UserVerificationStrings), nameof(UserVerificationStrings.BoxInfoLogoutLink))]
SignOut,
}
}

View File

@ -176,7 +176,7 @@ namespace osu.Game.Overlays.Mods
dequeuedAction();
// each time we play an animation, we decrease the time until the next animation (to ramp the visual and audible elements).
selectionDelay = Math.Max(30, selectionDelay * 0.8f);
selectionDelay = Math.Max(ModSelectPanel.SAMPLE_PLAYBACK_DELAY, selectionDelay * 0.8f);
lastSelection = Time.Current;
}
else

View File

@ -14,6 +14,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@ -45,6 +46,8 @@ namespace osu.Game.Overlays.Mods
public const float CORNER_RADIUS = 7;
public const float HEIGHT = 42;
public const double SAMPLE_PLAYBACK_DELAY = 30;
protected virtual float IdleSwitchWidth => 14;
protected virtual float ExpandedSwitchWidth => 30;
protected virtual Colour4 BackgroundColour => Active.Value ? AccentColour.Darken(0.3f) : ColourProvider.Background3;
@ -69,6 +72,8 @@ namespace osu.Game.Overlays.Mods
private Sample? sampleOff;
private Sample? sampleOn;
private Bindable<double?> lastPlaybackTime = null!;
protected ModSelectPanel()
{
RelativeSizeAxes = Axes.X;
@ -118,23 +123,23 @@ namespace osu.Game.Overlays.Mods
Direction = FillDirection.Vertical,
Children = new[]
{
titleText = new OsuSpriteText
titleText = new TruncatingSpriteText
{
Font = OsuFont.TorusAlternate.With(size: 18, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
Truncate = true,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Margin = new MarginPadding
{
Left = -18 * ShearedOverlayContainer.SHEAR
}
},
ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`.
},
descriptionText = new OsuSpriteText
descriptionText = new TruncatingSpriteText
{
Font = OsuFont.Default.With(size: 12),
RelativeSizeAxes = Axes.X,
Truncate = true,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0)
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
ShowTooltip = false, // Tooltip is handled by `IncompatibilityDisplayingModPanel`.
}
}
}
@ -163,13 +168,15 @@ namespace osu.Game.Overlays.Mods
protected abstract void Deselect();
[BackgroundDependencyLoader]
private void load(AudioManager audio, ISamplePlaybackDisabler? samplePlaybackDisabler)
private void load(AudioManager audio, SessionStatics statics, ISamplePlaybackDisabler? samplePlaybackDisabler)
{
sampleOn = audio.Samples.Get(@"UI/check-on");
sampleOff = audio.Samples.Get(@"UI/check-off");
if (samplePlaybackDisabler != null)
((IBindable<bool>)samplePlaybackDisabled).BindTo(samplePlaybackDisabler.SamplePlaybackDisabled);
lastPlaybackTime = statics.GetBindable<double?>(Static.LastHoverSoundPlaybackTime);
}
protected sealed override HoverSounds CreateHoverSounds(HoverSampleSet sampleSet) => new HoverSounds(sampleSet);
@ -192,10 +199,17 @@ namespace osu.Game.Overlays.Mods
if (samplePlaybackDisabled.Value)
return;
if (Active.Value)
sampleOn?.Play();
else
sampleOff?.Play();
bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= SAMPLE_PLAYBACK_DELAY;
if (enoughTimePassedSinceLastPlayback)
{
if (Active.Value)
sampleOn?.Play();
else
sampleOff?.Play();
lastPlaybackTime.Value = Time.Current;
}
}
protected override bool OnHover(HoverEvent e)

View File

@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
{
protected override LocalisableString Header => AudioSettingsStrings.OffsetHeader;
public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "universal", "uo", "timing" });
public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(new LocalisableString[] { "universal", "uo", "timing", "delay", "latency" });
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)

View File

@ -125,6 +125,7 @@ namespace osu.Game.Rulesets.Edit
if (currentSnap > DistanceSpacingMultiplier.MinValue)
{
currentDistanceSpacingButton.Enabled.Value = currentDistanceSpacingButton.Expanded.Value
&& !DistanceSpacingMultiplier.Disabled
&& !Precision.AlmostEquals(currentSnap, DistanceSpacingMultiplier.Value, DistanceSpacingMultiplier.Precision / 2);
currentDistanceSpacingButton.ContractedLabelText = $"current {currentSnap:N2}x";
currentDistanceSpacingButton.ExpandedLabelText = $"Use current ({currentSnap:N2}x)";
@ -141,28 +142,31 @@ namespace osu.Game.Rulesets.Edit
{
base.LoadComplete();
if (!DistanceSpacingMultiplier.Disabled)
if (DistanceSpacingMultiplier.Disabled)
{
DistanceSpacingMultiplier.Value = EditorBeatmap.BeatmapInfo.DistanceSpacing;
DistanceSpacingMultiplier.BindValueChanged(multiplier =>
{
distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})";
distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({multiplier.NewValue:0.##x})";
if (multiplier.NewValue != multiplier.OldValue)
onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier));
EditorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue;
}, true);
// Manual binding to handle enabling distance spacing when the slider is interacted with.
distanceSpacingSlider.Current.BindValueChanged(spacing =>
{
DistanceSpacingMultiplier.Value = spacing.NewValue;
DistanceSnapToggle.Value = TernaryState.True;
});
DistanceSpacingMultiplier.BindValueChanged(spacing => distanceSpacingSlider.Current.Value = spacing.NewValue);
distanceSpacingSlider.Hide();
return;
}
DistanceSpacingMultiplier.Value = EditorBeatmap.BeatmapInfo.DistanceSpacing;
DistanceSpacingMultiplier.BindValueChanged(multiplier =>
{
distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})";
distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({multiplier.NewValue:0.##x})";
if (multiplier.NewValue != multiplier.OldValue)
onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier));
EditorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue;
}, true);
// Manual binding to handle enabling distance spacing when the slider is interacted with.
distanceSpacingSlider.Current.BindValueChanged(spacing =>
{
DistanceSpacingMultiplier.Value = spacing.NewValue;
DistanceSnapToggle.Value = TernaryState.True;
});
DistanceSpacingMultiplier.BindValueChanged(spacing => distanceSpacingSlider.Current.Value = spacing.NewValue);
}
protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]

View File

@ -30,6 +30,11 @@ namespace osu.Game.Rulesets.Scoring
/// </summary>
protected int MaxHits { get; private set; }
/// <summary>
/// Whether <see cref="SimulateAutoplay"/> is currently running.
/// </summary>
protected bool IsSimulating { get; private set; }
/// <summary>
/// The total number of judged <see cref="HitObject"/>s at the current point in time.
/// </summary>
@ -146,6 +151,8 @@ namespace osu.Game.Rulesets.Scoring
/// <param name="beatmap">The <see cref="IBeatmap"/> to simulate.</param>
protected virtual void SimulateAutoplay(IBeatmap beatmap)
{
IsSimulating = true;
foreach (var obj in beatmap.HitObjects)
simulate(obj);
@ -163,6 +170,8 @@ namespace osu.Game.Rulesets.Scoring
result.Type = GetSimulatedHitResult(judgement);
ApplyResult(result);
}
IsSimulating = false;
}
protected override void Update()

View File

@ -30,6 +30,14 @@ namespace osu.Game.Rulesets.Scoring
private const double accuracy_cutoff_c = 0.7;
private const double accuracy_cutoff_d = 0;
/// <summary>
/// Whether <see cref="HitEvents"/> should be populated during application of results.
/// </summary>
/// <remarks>
/// Should only be disabled for special cases.
/// When disabled, <see cref="JudgementProcessor.RevertResult"/> cannot be used.</remarks>
internal bool TrackHitEvents = true;
/// <summary>
/// Invoked when this <see cref="ScoreProcessor"/> was reset from a replay frame.
/// </summary>
@ -226,10 +234,16 @@ namespace osu.Game.Rulesets.Scoring
ApplyScoreChange(result);
hitEvents.Add(CreateHitEvent(result));
lastHitObject = result.HitObject;
if (!IsSimulating)
{
if (TrackHitEvents)
{
hitEvents.Add(CreateHitEvent(result));
lastHitObject = result.HitObject;
}
updateScore();
updateScore();
}
}
/// <summary>
@ -242,6 +256,9 @@ namespace osu.Game.Rulesets.Scoring
protected sealed override void RevertResultInternal(JudgementResult result)
{
if (!TrackHitEvents)
throw new InvalidOperationException(@$"Rewind is not supported when {nameof(TrackHitEvents)} is disabled.");
Combo.Value = result.ComboAtJudgement;
HighestCombo.Value = result.HighestComboAtJudgement;
@ -311,6 +328,9 @@ namespace osu.Game.Rulesets.Scoring
/// <param name="storeResults">Whether to store the current state of the <see cref="ScoreProcessor"/> for future use.</param>
protected override void Reset(bool storeResults)
{
// Run one last time to store max values.
updateScore();
base.Reset(storeResults);
hitEvents.Clear();

View File

@ -46,6 +46,9 @@ namespace osu.Game.Scoring.Legacy
score.ScoreInfo = scoreInfo;
int version = sr.ReadInt32();
scoreInfo.IsLegacyScore = version < LegacyScoreEncoder.FIRST_LAZER_VERSION;
string beatmapHash = sr.ReadString();
workingBeatmap = GetBeatmap(beatmapHash);

View File

@ -28,9 +28,10 @@ namespace osu.Game.Scoring.Legacy
/// <remarks>
/// <list type="bullet">
/// <item><description>30000001: Appends <see cref="LegacyReplaySoloScoreInfo"/> to the end of scores.</description></item>
/// <item><description>30000002: Score stored to replay calculated using the Score V2 algorithm.</description></item>
/// </list>
/// </remarks>
public const int LATEST_VERSION = 30000001;
public const int LATEST_VERSION = 30000002;
/// <summary>
/// The first stable-compatible YYYYMMDD format version given to lazer usage of replays.

View File

@ -16,7 +16,13 @@ namespace osu.Game.Scoring.Legacy
=> getDisplayScore(scoreProcessor.Ruleset.RulesetInfo.OnlineID, scoreProcessor.TotalScore.Value, mode, scoreProcessor.MaximumStatistics);
public static long GetDisplayScore(this ScoreInfo scoreInfo, ScoringMode mode)
=> getDisplayScore(scoreInfo.Ruleset.OnlineID, scoreInfo.TotalScore, mode, scoreInfo.MaximumStatistics);
{
// Temporary to not scale stable scores that are already in the XX-millions with the classic scoring mode.
if (scoreInfo.IsLegacyScore)
return scoreInfo.TotalScore;
return getDisplayScore(scoreInfo.Ruleset.OnlineID, scoreInfo.TotalScore, mode, scoreInfo.MaximumStatistics);
}
private static long getDisplayScore(int rulesetId, long score, ScoringMode mode, IReadOnlyDictionary<HitResult, int> maximumStatistics)
{

View File

@ -83,6 +83,11 @@ namespace osu.Game.Scoring
if (string.IsNullOrEmpty(model.MaximumStatisticsJson))
model.MaximumStatisticsJson = JsonConvert.SerializeObject(model.MaximumStatistics);
// for pre-ScoreV2 lazer scores, apply a best-effort conversion of total score to ScoreV2.
// this requires: max combo, statistics, max statistics (where available), and mods to already be populated on the score.
if (StandardisedScoreMigrationTools.ShouldMigrateToNewStandardised(model))
model.TotalScore = StandardisedScoreMigrationTools.GetNewStandardised(model);
}
/// <summary>
@ -146,16 +151,48 @@ namespace osu.Game.Scoring
#pragma warning restore CS0618
}
// Very naive local caching to improve performance of large score imports (where the username is usually the same for most or all scores).
private readonly Dictionary<string, APIUser> usernameLookupCache = new Dictionary<string, APIUser>();
protected override void PostImport(ScoreInfo model, Realm realm, ImportParameters parameters)
{
base.PostImport(model, realm, parameters);
var userRequest = new GetUserRequest(model.RealmUser.Username);
populateUserDetails(model);
}
/// <summary>
/// Legacy replays only store a username.
/// This will populate a user ID during import.
/// </summary>
private void populateUserDetails(ScoreInfo model)
{
string username = model.RealmUser.Username;
if (usernameLookupCache.TryGetValue(username, out var existing))
{
model.User = existing;
return;
}
var userRequest = new GetUserRequest(username);
api.Perform(userRequest);
if (userRequest.Response is APIUser user)
{
usernameLookupCache.TryAdd(username, new APIUser
{
// Because this is a permanent cache, let's only store the pieces we're interested in,
// rather than the full API response. If we start to store more than these three fields
// in realm, this should be undone.
Id = user.Id,
Username = user.Username,
CountryCode = user.CountryCode,
});
model.User = user;
}
}
}
}

View File

@ -181,8 +181,7 @@ namespace osu.Game.Scoring
/// <summary>
/// Whether this <see cref="ScoreInfo"/> represents a legacy (osu!stable) score.
/// </summary>
[Ignored]
public bool IsLegacyScore => Mods.OfType<ModClassic>().Any();
public bool IsLegacyScore { get; set; }
private Dictionary<HitResult, int>? statistics;

View File

@ -1,12 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Game.Beatmaps;
using osu.Game.Database;
@ -21,7 +18,7 @@ namespace osu.Game.Scoring
public partial class ScorePerformanceCache : MemoryCachingComponent<ScorePerformanceCache.PerformanceCacheLookup, PerformanceAttributes>
{
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; }
private BeatmapDifficultyCache difficultyCache { get; set; } = null!;
protected override bool CacheNullValues => false;
@ -30,10 +27,10 @@ namespace osu.Game.Scoring
/// </summary>
/// <param name="score">The score to do the calculation on. </param>
/// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param>
public Task<PerformanceAttributes> CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) =>
public Task<PerformanceAttributes?> CalculatePerformanceAsync(ScoreInfo score, CancellationToken token = default) =>
GetAsync(new PerformanceCacheLookup(score), token);
protected override async Task<PerformanceAttributes> ComputeValueAsync(PerformanceCacheLookup lookup, CancellationToken token = default)
protected override async Task<PerformanceAttributes?> ComputeValueAsync(PerformanceCacheLookup lookup, CancellationToken token = default)
{
var score = lookup.ScoreInfo;

View File

@ -76,6 +76,9 @@ namespace osu.Game.Screens.Edit.Components
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Repeat)
return false;
switch (e.Key)
{
case Key.Space:

View File

@ -539,6 +539,9 @@ namespace osu.Game.Screens.Edit
// Track traversal keys.
// Matching osu-stable implementations.
case Key.Z:
if (e.Repeat)
return false;
// Seek to first object time, or track start if already there.
double? firstObjectTime = editorBeatmap.HitObjects.FirstOrDefault()?.StartTime;
@ -549,12 +552,18 @@ namespace osu.Game.Screens.Edit
return true;
case Key.X:
if (e.Repeat)
return false;
// Restart playback from beginning of track.
clock.Seek(0);
clock.Start();
return true;
case Key.C:
if (e.Repeat)
return false;
// Pause or resume.
if (clock.IsRunning)
clock.Stop();
@ -563,6 +572,9 @@ namespace osu.Game.Screens.Edit
return true;
case Key.V:
if (e.Repeat)
return false;
// Seek to last object time, or track end if already there.
// Note that in osu-stable subsequent presses when at track end won't return to last object.
// This has intentionally been changed to make it more useful.

View File

@ -103,118 +103,129 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
CornerRadius = CORNER_RADIUS,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Background5,
Width = 0.2f,
},
new Box
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f)),
Width = 0.8f,
},
new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Relative, 0.2f)
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
new Box
new Container
{
Name = @"Left details",
RelativeSizeAxes = Axes.Both,
Colour = colours.Background5,
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f))
},
}
}
},
new Container
{
Name = @"Left details",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Left = 20,
Vertical = 5
},
Children = new Drawable[]
{
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new FillFlowContainer
Padding = new MarginPadding
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Children = new Drawable[]
{
new RoomStatusPill
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
specialCategoryPill = new RoomSpecialCategoryPill
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
endDateInfo = new EndDateInfo
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
}
Left = 20,
Right = DrawableRoomParticipantsList.SHEAR_WIDTH,
Vertical = 5
},
new FillFlowContainer
Children = new Drawable[]
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Top = 3 },
Direction = FillDirection.Vertical,
Children = new Drawable[]
new FillFlowContainer
{
new RoomNameText(),
new RoomStatusText()
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Children = new Drawable[]
{
new RoomStatusPill
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
specialCategoryPill = new RoomSpecialCategoryPill
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
endDateInfo = new EndDateInfo
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
}
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Top = 3 },
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new TruncatingSpriteText
{
RelativeSizeAxes = Axes.X,
Font = OsuFont.GetFont(size: 28),
Current = { BindTarget = Room.Name }
},
new RoomStatusText()
}
}
},
},
new FillFlowContainer
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
ChildrenEnumerable = CreateBottomDetails()
}
}
},
new FillFlowContainer
{
Name = "Right content",
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Spacing = new Vector2(5),
Padding = new MarginPadding
{
Right = 10,
Vertical = 20,
},
Children = new Drawable[]
{
ButtonsContainer,
drawableRoomParticipantsList = new DrawableRoomParticipantsList
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
NumberOfCircles = NumberOfAvatars
}
}
},
},
new FillFlowContainer
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
ChildrenEnumerable = CreateBottomDetails()
}
}
},
new FillFlowContainer
{
Name = "Right content",
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Spacing = new Vector2(5),
Padding = new MarginPadding
{
Right = 10,
Vertical = 20,
},
Children = new Drawable[]
{
ButtonsContainer,
drawableRoomParticipantsList = new DrawableRoomParticipantsList
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
NumberOfCircles = NumberOfAvatars
}
}
},
@ -311,23 +322,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
return pills;
}
private partial class RoomNameText : OsuSpriteText
{
[Resolved(typeof(Room), nameof(Online.Rooms.Room.Name))]
private Bindable<string> name { get; set; }
public RoomNameText()
{
Font = OsuFont.GetFont(size: 28);
}
[BackgroundDependencyLoader]
private void load()
{
Current = name;
}
}
private partial class RoomStatusText : OnlinePlayComposite
{
[Resolved]
@ -343,7 +337,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Width = 0.5f;
}
[BackgroundDependencyLoader]

View File

@ -24,8 +24,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
public partial class DrawableRoomParticipantsList : OnlinePlayComposite
{
public const float SHEAR_WIDTH = 12f;
private const float avatar_size = 36;
private const float height = 60f;
private static readonly Vector2 shear = new Vector2(SHEAR_WIDTH / height, 0);
private FillFlowContainer<CircularAvatar> avatarFlow;
private CircularAvatar hostAvatar;
@ -36,7 +42,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
public DrawableRoomParticipantsList()
{
AutoSizeAxes = Axes.X;
Height = 60;
Height = height;
}
[BackgroundDependencyLoader]
@ -49,7 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 10,
Shear = new Vector2(0.2f, 0),
Shear = shear,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
@ -98,7 +104,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 10,
Shear = new Vector2(0.2f, 0),
Shear = shear,
Child = new Box
{
RelativeSizeAxes = Axes.Both,

View File

@ -95,7 +95,6 @@ namespace osu.Game.Screens.Play.HUD
private void updateGraphVisibility()
{
graph.FadeTo(ShowGraph.Value ? 1 : 0, 200, Easing.In);
bar.ShowBackground = !ShowGraph.Value;
}
protected override void Update()

View File

@ -14,7 +14,6 @@ using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Play.HUD
{
@ -32,18 +31,8 @@ namespace osu.Game.Screens.Play.HUD
private readonly Box background;
private readonly BindableBool showBackground = new BindableBool();
private readonly ColourInfo mainColour;
private readonly ColourInfo mainColourDarkened;
private ColourInfo catchUpColour;
private ColourInfo catchUpColourDarkened;
public bool ShowBackground
{
get => showBackground.Value;
set => showBackground.Value = value;
}
public double StartTime
{
@ -95,7 +84,7 @@ namespace osu.Game.Screens.Play.HUD
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
Colour = Colour4.White.Darken(1 + 1 / 4f)
Colour = OsuColour.Gray(0.2f),
},
catchupBar = new RoundedBar
{
@ -112,12 +101,10 @@ namespace osu.Game.Screens.Play.HUD
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
CornerRadius = 5,
AccentColour = mainColour = Color4.White,
AccentColour = mainColour = OsuColour.Gray(0.9f),
RelativeSizeAxes = Axes.Both
},
};
mainColourDarkened = Colour4.White.Darken(1 / 3f);
}
private void setupAlternateValue()
@ -141,16 +128,15 @@ namespace osu.Game.Screens.Play.HUD
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
catchUpColour = colours.BlueLight;
catchUpColourDarkened = colours.BlueDark;
showBackground.BindValueChanged(_ => updateBackground(), true);
catchUpColour = colours.BlueDark;
}
private void updateBackground()
protected override void LoadComplete()
{
background.FadeTo(showBackground.Value ? 1 / 4f : 0, 200, Easing.In);
playfieldBar.TransformTo(nameof(playfieldBar.AccentColour), ShowBackground ? mainColour : mainColourDarkened, 200, Easing.In);
base.LoadComplete();
background.FadeTo(0.3f, 200, Easing.In);
playfieldBar.TransformTo(nameof(playfieldBar.AccentColour), mainColour, 200, Easing.In);
}
protected override bool OnHover(HoverEvent e)
@ -190,8 +176,8 @@ namespace osu.Game.Screens.Play.HUD
catchupBar.AccentColour = Interpolation.ValueAt(
Math.Min(timeDelta, colour_transition_threshold),
ShowBackground ? mainColour : mainColourDarkened,
ShowBackground ? catchUpColour : catchUpColourDarkened,
mainColour,
catchUpColour,
0, colour_transition_threshold,
Easing.OutQuint);

View File

@ -4,8 +4,10 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects;
using osu.Game.Graphics.UserInterface;
@ -13,6 +15,10 @@ namespace osu.Game.Screens.Play.HUD
{
public partial class ArgonSongProgressGraph : SegmentedGraph<int>
{
private const int tier_count = 5;
private const int display_granularity = 200;
private IEnumerable<HitObject>? objects;
public IEnumerable<HitObject> Objects
@ -21,8 +27,7 @@ namespace osu.Game.Screens.Play.HUD
{
objects = value;
const int granularity = 200;
int[] values = new int[granularity];
int[] values = new int[display_granularity];
if (!objects.Any())
return;
@ -32,7 +37,7 @@ namespace osu.Game.Screens.Play.HUD
if (lastHit == 0)
lastHit = objects.Last().StartTime;
double interval = (lastHit - firstHit + 1) / granularity;
double interval = (lastHit - firstHit + 1) / display_granularity;
foreach (var h in objects)
{
@ -51,12 +56,12 @@ namespace osu.Game.Screens.Play.HUD
}
public ArgonSongProgressGraph()
: base(5)
: base(tier_count)
{
var colours = new List<Colour4>();
for (int i = 0; i < 5; i++)
colours.Add(Colour4.White.Darken(1 + 1 / 5f).Opacity(1 / 5f));
for (int i = 0; i < tier_count; i++)
colours.Add(OsuColour.Gray(0.2f).Opacity(0.1f));
TierColours = colours;
}

View File

@ -235,7 +235,7 @@ namespace osu.Game.Screens.Play.HUD
}
}
},
usernameText = new OsuSpriteText
usernameText = new TruncatingSpriteText
{
RelativeSizeAxes = Axes.X,
Width = 0.6f,
@ -244,7 +244,6 @@ namespace osu.Game.Screens.Play.HUD
Colour = Color4.White,
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
Text = User?.Username ?? string.Empty,
Truncate = true,
Shadow = false,
}
}

View File

@ -42,7 +42,8 @@ namespace osu.Game.Screens.Play.HUD
//CollectionSettings = new CollectionSettings(),
//DiscussionSettings = new DiscussionSettings(),
PlaybackSettings = new PlaybackSettings { Expanded = { Value = false } },
VisualSettings = new VisualSettings { Expanded = { Value = false } }
VisualSettings = new VisualSettings { Expanded = { Value = false } },
new AudioSettings { Expanded = { Value = false } }
}
};
}

View File

@ -101,23 +101,21 @@ namespace osu.Game.Screens.Ranking.Expanded
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new OsuSpriteText
new TruncatingSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = new RomanisableString(metadata.TitleUnicode, metadata.Title),
Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold),
MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
Truncate = true,
},
new OsuSpriteText
new TruncatingSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist),
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
Truncate = true,
},
new Container
{
@ -156,14 +154,13 @@ namespace osu.Game.Screens.Ranking.Expanded
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new OsuSpriteText
new TruncatingSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = beatmap.DifficultyName,
Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold),
MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
Truncate = true,
},
new OsuTextFlowContainer(s => s.Font = OsuFont.Torus.With(size: 12))
{

View File

@ -155,7 +155,7 @@ namespace osu.Game.Screens.Select
public Bindable<RandomSelectAlgorithm> RandomAlgorithm = new Bindable<RandomSelectAlgorithm>();
private readonly List<CarouselBeatmapSet> previouslyVisitedRandomSets = new List<CarouselBeatmapSet>();
private readonly Stack<CarouselBeatmap> randomSelectedBeatmaps = new Stack<CarouselBeatmap>();
private readonly List<CarouselBeatmap> randomSelectedBeatmaps = new List<CarouselBeatmap>();
private CarouselRoot root;
@ -348,6 +348,11 @@ namespace osu.Game.Screens.Select
if (!root.BeatmapSetsByID.TryGetValue(beatmapSetID, out var existingSet))
return;
foreach (var beatmap in existingSet.Beatmaps)
randomSelectedBeatmaps.Remove(beatmap);
previouslyVisitedRandomSets.Remove(existingSet);
root.RemoveItem(existingSet);
itemsCache.Invalidate();
@ -501,7 +506,7 @@ namespace osu.Game.Screens.Select
if (selectedBeatmap != null && selectedBeatmapSet != null)
{
randomSelectedBeatmaps.Push(selectedBeatmap);
randomSelectedBeatmaps.Add(selectedBeatmap);
// when performing a random, we want to add the current set to the previously visited list
// else the user may be "randomised" to the existing selection.
@ -538,9 +543,10 @@ namespace osu.Game.Screens.Select
{
while (randomSelectedBeatmaps.Any())
{
var beatmap = randomSelectedBeatmaps.Pop();
var beatmap = randomSelectedBeatmaps[^1];
randomSelectedBeatmaps.Remove(beatmap);
if (!beatmap.Filtered.Value)
if (!beatmap.Filtered.Value && beatmap.BeatmapInfo.BeatmapSet?.DeletePending != true)
{
if (selectedBeatmapSet != null)
{

View File

@ -233,12 +233,11 @@ namespace osu.Game.Screens.Select
RelativeSizeAxes = Axes.X,
Children = new Drawable[]
{
VersionLabel = new OsuSpriteText
VersionLabel = new TruncatingSpriteText
{
Text = beatmapInfo.DifficultyName,
Font = OsuFont.GetFont(size: 24, italics: true),
RelativeSizeAxes = Axes.X,
Truncate = true,
},
}
},
@ -286,19 +285,17 @@ namespace osu.Game.Screens.Select
RelativeSizeAxes = Axes.X,
Children = new Drawable[]
{
TitleLabel = new OsuSpriteText
TitleLabel = new TruncatingSpriteText
{
Current = { BindTarget = titleBinding },
Font = OsuFont.GetFont(size: 28, italics: true),
RelativeSizeAxes = Axes.X,
Truncate = true,
},
ArtistLabel = new OsuSpriteText
ArtistLabel = new TruncatingSpriteText
{
Current = { BindTarget = artistBinding },
Font = OsuFont.GetFont(size: 17, italics: true),
RelativeSizeAxes = Axes.X,
Truncate = true,
},
MapperContainer = new FillFlowContainer
{

View File

@ -112,11 +112,11 @@ namespace osu.Game.Screens.Select.Carousel
background = new DelayedLoadWrapper(() => new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.MinBy(b => b.OnlineID)))
{
RelativeSizeAxes = Axes.Both,
}, 300)
}, 200)
{
RelativeSizeAxes = Axes.Both
},
mainFlow = new DelayedLoadWrapper(() => new SetPanelContent((CarouselBeatmapSet)Item), 100)
mainFlow = new DelayedLoadWrapper(() => new SetPanelContent((CarouselBeatmapSet)Item), 50)
{
RelativeSizeAxes = Axes.Both
},
@ -126,7 +126,7 @@ namespace osu.Game.Screens.Select.Carousel
mainFlow.DelayedLoadComplete += fadeContentIn;
}
private void fadeContentIn(Drawable d) => d.FadeInFromZero(750, Easing.OutQuint);
private void fadeContentIn(Drawable d) => d.FadeInFromZero(150);
protected override void Deselected()
{

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Threading;
using System.Threading.Tasks;
using osu.Game.Database;
@ -18,12 +16,12 @@ namespace osu.Game.Tests.Visual
/// </summary>
public const int UNRESOLVED_USER_ID = -1;
protected override Task<APIUser> ComputeValueAsync(int lookup, CancellationToken token = default)
protected override Task<APIUser?> ComputeValueAsync(int lookup, CancellationToken token = default)
{
if (lookup == UNRESOLVED_USER_ID)
return Task.FromResult((APIUser)null);
return Task.FromResult<APIUser?>(null);
return Task.FromResult(new APIUser
return Task.FromResult<APIUser?>(new APIUser
{
Id = lookup,
Username = $"User {lookup}"

View File

@ -37,7 +37,7 @@
</PackageReference>
<PackageReference Include="Realm" Version="10.20.0" />
<PackageReference Include="ppy.osu.Framework" Version="2023.608.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.510.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.605.0" />
<PackageReference Include="Sentry" Version="3.28.1" />
<PackageReference Include="SharpCompress" Version="0.32.2" />
<PackageReference Include="NUnit" Version="3.13.3" />