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

Merge branch 'realm-nested-writes' into metadata-client

This commit is contained in:
Dean Herbert 2022-07-07 17:37:06 +09:00
commit 79bed0abdf
16 changed files with 220 additions and 69 deletions

View File

@ -120,10 +120,10 @@ namespace osu.Game.Rulesets.Catch.UI
lastHyperDashState = Catcher.HyperDashing; lastHyperDashState = Catcher.HyperDashing;
} }
public void SetCatcherPosition(float X) public void SetCatcherPosition(float x)
{ {
float lastPosition = Catcher.X; float lastPosition = Catcher.X;
float newPosition = Math.Clamp(X, 0, CatchPlayfield.WIDTH); float newPosition = Math.Clamp(x, 0, CatchPlayfield.WIDTH);
Catcher.X = newPosition; Catcher.X = newPosition;

View File

@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{ {
base.LoadComplete(); base.LoadComplete();
complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200)); complete.BindValueChanged(complete => updateDiscColour(complete.NewValue, 200));
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value); updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
@ -137,6 +137,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
this.ScaleTo(initial_scale); this.ScaleTo(initial_scale);
this.RotateTo(0); this.RotateTo(0);
updateDiscColour(false);
using (BeginDelayedSequence(spinner.TimePreempt / 2)) using (BeginDelayedSequence(spinner.TimePreempt / 2))
{ {
// constant ambient rotation to give the spinner "spinning" character. // constant ambient rotation to give the spinner "spinning" character.
@ -177,12 +179,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
} }
} }
// transforms we have from completing the spinner will be rolled back, so reapply immediately. if (drawableSpinner.Result?.TimeCompleted is double completionTime)
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt)) {
updateComplete(state == ArmedState.Hit, 0); using (BeginAbsoluteSequence(completionTime))
updateDiscColour(true, 200);
}
} }
private void updateComplete(bool complete, double duration) private void updateDiscColour(bool complete, double duration = 0)
{ {
var colour = complete ? completeColour : normalColour; var colour = complete ? completeColour : normalColour;

View File

@ -59,6 +59,25 @@ namespace osu.Game.Tests.Database
}); });
} }
[Test]
public void TestNestedWriteCalls()
{
RunTestWithRealm((realm, _) =>
{
var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata());
var liveBeatmap = beatmap.ToLive(realm);
realm.Run(r =>
r.Write(_ =>
r.Write(_ =>
r.Add(beatmap)))
);
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
});
}
[Test] [Test]
public void TestAccessAfterAttach() public void TestAccessAfterAttach()
{ {

View File

@ -9,7 +9,6 @@ using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -116,10 +115,10 @@ namespace osu.Game.Tests.Gameplay
AddStep($"set audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset)); AddStep($"set audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
AddStep("seek to 2500", () => gameplayClockContainer.Seek(2500)); AddStep("seek to 2500", () => gameplayClockContainer.Seek(2500));
AddAssert("gameplay clock time = 2500", () => Precision.AlmostEquals(gameplayClockContainer.CurrentTime, 2500, 10f)); AddStep("gameplay clock time = 2500", () => Assert.AreEqual(gameplayClockContainer.CurrentTime, 2500, 10f));
AddStep("seek to 10000", () => gameplayClockContainer.Seek(10000)); AddStep("seek to 10000", () => gameplayClockContainer.Seek(10000));
AddAssert("gameplay clock time = 10000", () => Precision.AlmostEquals(gameplayClockContainer.CurrentTime, 10000, 10f)); AddStep("gameplay clock time = 10000", () => Assert.AreEqual(gameplayClockContainer.CurrentTime, 10000, 10f));
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

@ -18,6 +18,7 @@ using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Leaderboards; using osu.Game.Online.Leaderboards;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -100,6 +101,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Rank = ScoreRank.XH, Rank = ScoreRank.XH,
User = new APIUser { Username = "TestUser" }, User = new APIUser { Username = "TestUser" },
Ruleset = new OsuRuleset().RulesetInfo, Ruleset = new OsuRuleset().RulesetInfo,
Files = { new RealmNamedFileUsage(new RealmFile { Hash = $"{i}" }, string.Empty) }
}; };
importedScores.Add(scoreManager.Import(score).Value); importedScores.Add(scoreManager.Import(score).Value);

View File

@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps.Formats
{ {
Section section = Section.General; Section section = Section.General;
string line; string? line;
while ((line = stream.ReadLine()) != null) while ((line = stream.ReadLine()) != null)
{ {

View File

@ -137,8 +137,17 @@ namespace osu.Game.Beatmaps
try try
{ {
using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path)))) string fileStorePath = BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path);
return Decoder.GetDecoder<Beatmap>(stream).Decode(stream); var stream = GetStream(fileStorePath);
if (stream == null)
{
Logger.Log($"Beatmap failed to load (file {BeatmapInfo.Path} not found on disk at expected location {fileStorePath}).", level: LogLevel.Error);
return null;
}
using (var reader = new LineBufferedReader(stream))
return Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
} }
catch (Exception e) catch (Exception e)
{ {
@ -154,7 +163,16 @@ namespace osu.Game.Beatmaps
try try
{ {
return resources.LargeTextureStore.Get(BeatmapSetInfo.GetPathForFile(Metadata.BackgroundFile)); string fileStorePath = BeatmapSetInfo.GetPathForFile(Metadata.BackgroundFile);
var texture = resources.LargeTextureStore.Get(fileStorePath);
if (texture == null)
{
Logger.Log($"Beatmap background failed to load (file {Metadata.BackgroundFile} not found on disk at expected location {fileStorePath}).", level: LogLevel.Error);
return null;
}
return texture;
} }
catch (Exception e) catch (Exception e)
{ {
@ -173,7 +191,16 @@ namespace osu.Game.Beatmaps
try try
{ {
return resources.Tracks.Get(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile)); string fileStorePath = BeatmapSetInfo.GetPathForFile(Metadata.AudioFile);
var track = resources.Tracks.Get(fileStorePath);
if (track == null)
{
Logger.Log($"Beatmap failed to load (file {Metadata.AudioFile} not found on disk at expected location {fileStorePath}).", level: LogLevel.Error);
return null;
}
return track;
} }
catch (Exception e) catch (Exception e)
{ {
@ -192,8 +219,17 @@ namespace osu.Game.Beatmaps
try try
{ {
var trackData = GetStream(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile)); string fileStorePath = BeatmapSetInfo.GetPathForFile(Metadata.AudioFile);
return trackData == null ? null : new Waveform(trackData);
var trackData = GetStream(fileStorePath);
if (trackData == null)
{
Logger.Log($"Beatmap waveform failed to load (file {Metadata.AudioFile} not found on disk at expected location {fileStorePath}).", level: LogLevel.Error);
return null;
}
return new Waveform(trackData);
} }
catch (Exception e) catch (Exception e)
{ {
@ -211,20 +247,38 @@ namespace osu.Game.Beatmaps
try try
{ {
using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path)))) string fileStorePath = BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path);
{ var beatmapFileStream = GetStream(fileStorePath);
var decoder = Decoder.GetDecoder<Storyboard>(stream);
string storyboardFilename = BeatmapSetInfo?.Files.FirstOrDefault(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename; if (beatmapFileStream == null)
// todo: support loading from both set-wide storyboard *and* beatmap specific.
if (string.IsNullOrEmpty(storyboardFilename))
storyboard = decoder.Decode(stream);
else
{ {
using (var secondaryStream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(storyboardFilename)))) Logger.Log($"Beatmap failed to load (file {BeatmapInfo.Path} not found on disk at expected location {fileStorePath})", level: LogLevel.Error);
storyboard = decoder.Decode(stream, secondaryStream); return null;
} }
using (var reader = new LineBufferedReader(beatmapFileStream))
{
var decoder = Decoder.GetDecoder<Storyboard>(reader);
Stream storyboardFileStream = null;
if (BeatmapSetInfo?.Files.FirstOrDefault(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename is string storyboardFilename)
{
string storyboardFileStorePath = BeatmapSetInfo?.GetPathForFile(storyboardFilename);
storyboardFileStream = GetStream(storyboardFileStorePath);
if (storyboardFileStream == null)
Logger.Log($"Storyboard failed to load (file {storyboardFilename} not found on disk at expected location {storyboardFileStorePath})", level: LogLevel.Error);
}
if (storyboardFileStream != null)
{
// Stand-alone storyboard was found, so parse in addition to the beatmap's local storyboard.
using (var secondaryReader = new LineBufferedReader(storyboardFileStream))
storyboard = decoder.Decode(reader, secondaryReader);
}
else
storyboard = decoder.Decode(reader);
} }
} }
catch (Exception e) catch (Exception e)

View File

@ -8,7 +8,9 @@ using System.Collections.Concurrent;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Statistics;
namespace osu.Game.Database namespace osu.Game.Database
{ {
@ -20,8 +22,16 @@ namespace osu.Game.Database
{ {
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;
protected virtual bool CacheNullValues => true; protected virtual bool CacheNullValues => true;
protected MemoryCachingComponent()
{
statistics = GlobalStatistics.Get<MemoryCachingStatistics>(nameof(MemoryCachingComponent<TLookup, TValue>), GetType().ReadableName());
statistics.Value = new MemoryCachingStatistics();
}
/// <summary> /// <summary>
/// Retrieve the cached value for the given lookup. /// Retrieve the cached value for the given lookup.
/// </summary> /// </summary>
@ -30,12 +40,20 @@ namespace osu.Game.Database
protected async Task<TValue> GetAsync([NotNull] TLookup lookup, CancellationToken token = default) protected async Task<TValue> GetAsync([NotNull] TLookup lookup, CancellationToken token = default)
{ {
if (CheckExists(lookup, out TValue performance)) if (CheckExists(lookup, out TValue performance))
{
statistics.Value.HitCount++;
return performance; return performance;
}
var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false); var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false);
statistics.Value.MissCount++;
if (computed != null || CacheNullValues) if (computed != null || CacheNullValues)
{
cache[lookup] = computed; cache[lookup] = computed;
statistics.Value.Usage = cache.Count;
}
return computed; return computed;
} }
@ -51,6 +69,8 @@ namespace osu.Game.Database
if (matchKeyPredicate(kvp.Key)) if (matchKeyPredicate(kvp.Key))
cache.TryRemove(kvp.Key, out _); cache.TryRemove(kvp.Key, out _);
} }
statistics.Value.Usage = cache.Count;
} }
protected bool CheckExists([NotNull] TLookup lookup, out TValue value) => protected bool CheckExists([NotNull] TLookup lookup, out TValue value) =>
@ -63,5 +83,31 @@ namespace osu.Game.Database
/// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param> /// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param>
/// <returns>The computed value.</returns> /// <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
{
/// <summary>
/// Total number of cache hits.
/// </summary>
public int HitCount;
/// <summary>
/// Total number of cache misses.
/// </summary>
public int MissCount;
/// <summary>
/// Total number of cached entities.
/// </summary>
public int Usage;
public override string ToString()
{
int totalAccesses = HitCount + MissCount;
double hitRate = totalAccesses == 0 ? 0 : (double)HitCount / totalAccesses;
return $"i:{Usage} h:{HitCount} m:{MissCount} {hitRate:0%}";
}
}
} }
} }

View File

@ -8,18 +8,45 @@ namespace osu.Game.Database
{ {
public static class RealmExtensions public static class RealmExtensions
{ {
/// <summary>
/// Perform a write operation against the provided realm instance.
/// </summary>
/// <remarks>
/// This will automatically start a transaction if not already in one.
/// </remarks>
/// <param name="realm">The realm to operate on.</param>
/// <param name="function">The write operation to run.</param>
public static void Write(this Realm realm, Action<Realm> function) public static void Write(this Realm realm, Action<Realm> function)
{ {
using var transaction = realm.BeginWrite(); Transaction? transaction = null;
if (!realm.IsInTransaction)
transaction = realm.BeginWrite();
function(realm); function(realm);
transaction.Commit();
transaction?.Commit();
} }
/// <summary>
/// Perform a write operation against the provided realm instance.
/// </summary>
/// <remarks>
/// This will automatically start a transaction if not already in one.
/// </remarks>
/// <param name="realm">The realm to operate on.</param>
/// <param name="function">The write operation to run.</param>
public static T Write<T>(this Realm realm, Func<Realm, T> function) public static T Write<T>(this Realm realm, Func<Realm, T> function)
{ {
using var transaction = realm.BeginWrite(); Transaction? transaction = null;
if (!realm.IsInTransaction)
transaction = realm.BeginWrite();
var result = function(realm); var result = function(realm);
transaction.Commit();
transaction?.Commit();
return result; return result;
} }

View File

@ -86,7 +86,7 @@ namespace osu.Game.Graphics.Containers
TimingControlPoint timingPoint; TimingControlPoint timingPoint;
EffectControlPoint effectPoint; EffectControlPoint effectPoint;
IsBeatSyncedWithTrack = BeatSyncSource.Clock?.IsRunning == true; IsBeatSyncedWithTrack = BeatSyncSource.Clock?.IsRunning == true && BeatSyncSource.ControlPoints != null;
double currentTrackTime; double currentTrackTime;

View File

@ -1,10 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
@ -17,34 +14,31 @@ namespace osu.Game.IO
public class LineBufferedReader : IDisposable public class LineBufferedReader : IDisposable
{ {
private readonly StreamReader streamReader; private readonly StreamReader streamReader;
private readonly Queue<string> lineBuffer;
private string? peekedLine;
public LineBufferedReader(Stream stream, bool leaveOpen = false) public LineBufferedReader(Stream stream, bool leaveOpen = false)
{ {
streamReader = new StreamReader(stream, Encoding.UTF8, true, 1024, leaveOpen); streamReader = new StreamReader(stream, Encoding.UTF8, true, -1, leaveOpen);
lineBuffer = new Queue<string>();
} }
/// <summary> /// <summary>
/// Reads the next line from the stream without consuming it. /// Reads the next line from the stream without consuming it.
/// Subsequent calls to <see cref="PeekLine"/> without a <see cref="ReadLine"/> will return the same string. /// Subsequent calls to <see cref="PeekLine"/> without a <see cref="ReadLine"/> will return the same string.
/// </summary> /// </summary>
public string PeekLine() public string? PeekLine() => peekedLine ??= streamReader.ReadLine();
{
if (lineBuffer.Count > 0)
return lineBuffer.Peek();
string line = streamReader.ReadLine();
if (line != null)
lineBuffer.Enqueue(line);
return line;
}
/// <summary> /// <summary>
/// Reads the next line from the stream and consumes it. /// Reads the next line from the stream and consumes it.
/// If a line was peeked, that same line will then be consumed and returned. /// If a line was peeked, that same line will then be consumed and returned.
/// </summary> /// </summary>
public string ReadLine() => lineBuffer.Count > 0 ? lineBuffer.Dequeue() : streamReader.ReadLine(); public string? ReadLine()
{
string? line = peekedLine ?? streamReader.ReadLine();
peekedLine = null;
return line;
}
/// <summary> /// <summary>
/// Reads the stream to its end and returns the text read. /// Reads the stream to its end and returns the text read.
@ -53,14 +47,13 @@ namespace osu.Game.IO
public string ReadToEnd() public string ReadToEnd()
{ {
string remainingText = streamReader.ReadToEnd(); string remainingText = streamReader.ReadToEnd();
if (lineBuffer.Count == 0) if (peekedLine == null)
return remainingText; return remainingText;
var builder = new StringBuilder(); var builder = new StringBuilder();
// this might not be completely correct due to varying platform line endings // this might not be completely correct due to varying platform line endings
while (lineBuffer.Count > 0) builder.AppendLine(peekedLine);
builder.AppendLine(lineBuffer.Dequeue());
builder.Append(remainingText); builder.Append(remainingText);
return builder.ToString(); return builder.ToString();
@ -68,7 +61,7 @@ namespace osu.Game.IO
public void Dispose() public void Dispose()
{ {
streamReader?.Dispose(); streamReader.Dispose();
} }
} }
} }

View File

@ -426,10 +426,10 @@ namespace osu.Game.Online.Leaderboards
items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods)); items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods));
if (Score.Files.Count > 0) if (Score.Files.Count > 0)
{
items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => new LegacyScoreExporter(storage).Export(Score))); items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => new LegacyScoreExporter(storage).Export(Score)));
if (!isOnlineScope)
items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(Score)))); items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(Score))));
}
return items.ToArray(); return items.ToArray();
} }

View File

@ -597,7 +597,7 @@ namespace osu.Game
Host.ExceptionThrown -= onExceptionThrown; Host.ExceptionThrown -= onExceptionThrown;
} }
ControlPointInfo IBeatSyncProvider.ControlPoints => Beatmap.Value.Beatmap.ControlPointInfo; ControlPointInfo IBeatSyncProvider.ControlPoints => Beatmap.Value.BeatmapLoaded ? Beatmap.Value.Beatmap.ControlPointInfo : null;
IClock IBeatSyncProvider.Clock => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track : (IClock)null; IClock IBeatSyncProvider.Clock => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track : (IClock)null;
ChannelAmplitudes? IBeatSyncProvider.Amplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : null; ChannelAmplitudes? IBeatSyncProvider.Amplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : null;
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Extensions.LocalisationExtensions;
@ -23,19 +21,19 @@ namespace osu.Game.Overlays.Login
{ {
public class LoginForm : FillFlowContainer public class LoginForm : FillFlowContainer
{ {
private TextBox username; private TextBox username = null!;
private TextBox password; private TextBox password = null!;
private ShakeContainer shakeSignIn; private ShakeContainer shakeSignIn = null!;
[Resolved(CanBeNull = true)] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; } = null!;
public Action RequestHide; public Action? RequestHide;
private void performLogin() private void performLogin()
{ {
if (!string.IsNullOrEmpty(username.Text) && !string.IsNullOrEmpty(password.Text)) if (!string.IsNullOrEmpty(username.Text) && !string.IsNullOrEmpty(password.Text))
api?.Login(username.Text, password.Text); api.Login(username.Text, password.Text);
else else
shakeSignIn.Shake(); shakeSignIn.Shake();
} }
@ -49,6 +47,7 @@ namespace osu.Game.Overlays.Login
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
ErrorTextFlowContainer errorText; ErrorTextFlowContainer errorText;
LinkFlowContainer forgottenPaswordLink;
Children = new Drawable[] Children = new Drawable[]
{ {
@ -56,7 +55,7 @@ namespace osu.Game.Overlays.Login
{ {
PlaceholderText = UsersStrings.LoginUsername.ToLower(), PlaceholderText = UsersStrings.LoginUsername.ToLower(),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Text = api?.ProvidedUsername ?? string.Empty, Text = api.ProvidedUsername,
TabbableContentContainer = this TabbableContentContainer = this
}, },
password = new OsuPasswordTextBox password = new OsuPasswordTextBox
@ -80,6 +79,12 @@ namespace osu.Game.Overlays.Login
LabelText = "Stay signed in", LabelText = "Stay signed in",
Current = config.GetBindable<bool>(OsuSetting.SavePassword), Current = config.GetBindable<bool>(OsuSetting.SavePassword),
}, },
forgottenPaswordLink = new LinkFlowContainer
{
Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
new Container new Container
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
@ -103,15 +108,17 @@ namespace osu.Game.Overlays.Login
Text = "Register", Text = "Register",
Action = () => Action = () =>
{ {
RequestHide(); RequestHide?.Invoke();
accountCreation.Show(); accountCreation.Show();
} }
} }
}; };
forgottenPaswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.WebsiteRootUrl}/home/password-reset");
password.OnCommit += (_, _) => performLogin(); password.OnCommit += (_, _) => performLogin();
if (api?.LastLoginError?.Message is string error) if (api.LastLoginError?.Message is string error)
errorText.AddErrors(new[] { error }); errorText.AddErrors(new[] { error });
} }

View File

@ -135,7 +135,7 @@ namespace osu.Game.Screens.Select
// TODO: Add realm queries to hint at which ruleset results are available in (and allow clicking to switch). // TODO: Add realm queries to hint at which ruleset results are available in (and allow clicking to switch).
// TODO: Make this message more certain by ensuring the osu! beatmaps exist before suggesting. // TODO: Make this message more certain by ensuring the osu! beatmaps exist before suggesting.
if (filter?.Ruleset?.OnlineID > 0 && !filter.AllowConvertedBeatmaps) if (filter?.Ruleset?.OnlineID != 0 && filter?.AllowConvertedBeatmaps == false)
{ {
textFlow.AddParagraph("- Try"); textFlow.AddParagraph("- Try");
textFlow.AddLink(" enabling ", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true)); textFlow.AddLink(" enabling ", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));

View File

@ -18,7 +18,7 @@ namespace osu.Game.Tests
} }
public FlakyTestAttribute(int tryCount) public FlakyTestAttribute(int tryCount)
: base(Environment.GetEnvironmentVariable("OSU_TESTS_FAIL_FLAKY") == "1" ? 0 : tryCount) : base(Environment.GetEnvironmentVariable("OSU_TESTS_FAIL_FLAKY") == "1" ? 1 : tryCount)
{ {
} }
} }