1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 15:22:55 +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;
}
public void SetCatcherPosition(float X)
public void SetCatcherPosition(float x)
{
float lastPosition = Catcher.X;
float newPosition = Math.Clamp(X, 0, CatchPlayfield.WIDTH);
float newPosition = Math.Clamp(x, 0, CatchPlayfield.WIDTH);
Catcher.X = newPosition;

View File

@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
base.LoadComplete();
complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200));
complete.BindValueChanged(complete => updateDiscColour(complete.NewValue, 200));
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
@ -137,6 +137,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
this.ScaleTo(initial_scale);
this.RotateTo(0);
updateDiscColour(false);
using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
// 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.
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
updateComplete(state == ArmedState.Hit, 0);
if (drawableSpinner.Result?.TimeCompleted is double completionTime)
{
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;

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]
public void TestAccessAfterAttach()
{

View File

@ -9,7 +9,6 @@ using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu;
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("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));
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)

View File

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

View File

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

View File

@ -137,8 +137,17 @@ namespace osu.Game.Beatmaps
try
{
using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path))))
return Decoder.GetDecoder<Beatmap>(stream).Decode(stream);
string fileStorePath = BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path);
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)
{
@ -154,7 +163,16 @@ namespace osu.Game.Beatmaps
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)
{
@ -173,7 +191,16 @@ namespace osu.Game.Beatmaps
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)
{
@ -192,8 +219,17 @@ namespace osu.Game.Beatmaps
try
{
var trackData = GetStream(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile));
return trackData == null ? null : new Waveform(trackData);
string fileStorePath = BeatmapSetInfo.GetPathForFile(Metadata.AudioFile);
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)
{
@ -211,20 +247,38 @@ namespace osu.Game.Beatmaps
try
{
using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path))))
{
var decoder = Decoder.GetDecoder<Storyboard>(stream);
string fileStorePath = BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path);
var beatmapFileStream = GetStream(fileStorePath);
string storyboardFilename = BeatmapSetInfo?.Files.FirstOrDefault(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename;
// todo: support loading from both set-wide storyboard *and* beatmap specific.
if (string.IsNullOrEmpty(storyboardFilename))
storyboard = decoder.Decode(stream);
else
if (beatmapFileStream == null)
{
using (var secondaryStream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(storyboardFilename))))
storyboard = decoder.Decode(stream, secondaryStream);
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(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)

View File

@ -8,7 +8,9 @@ using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Statistics;
namespace osu.Game.Database
{
@ -20,8 +22,16 @@ namespace osu.Game.Database
{
private readonly ConcurrentDictionary<TLookup, TValue> cache = new ConcurrentDictionary<TLookup, TValue>();
private readonly GlobalStatistic<MemoryCachingStatistics> statistics;
protected virtual bool CacheNullValues => true;
protected MemoryCachingComponent()
{
statistics = GlobalStatistics.Get<MemoryCachingStatistics>(nameof(MemoryCachingComponent<TLookup, TValue>), GetType().ReadableName());
statistics.Value = new MemoryCachingStatistics();
}
/// <summary>
/// Retrieve the cached value for the given lookup.
/// </summary>
@ -30,12 +40,20 @@ namespace osu.Game.Database
protected async Task<TValue> GetAsync([NotNull] TLookup lookup, CancellationToken token = default)
{
if (CheckExists(lookup, out TValue performance))
{
statistics.Value.HitCount++;
return performance;
}
var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false);
statistics.Value.MissCount++;
if (computed != null || CacheNullValues)
{
cache[lookup] = computed;
statistics.Value.Usage = cache.Count;
}
return computed;
}
@ -51,6 +69,8 @@ namespace osu.Game.Database
if (matchKeyPredicate(kvp.Key))
cache.TryRemove(kvp.Key, out _);
}
statistics.Value.Usage = cache.Count;
}
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>
/// <returns>The computed value.</returns>
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
{
/// <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)
{
using var transaction = realm.BeginWrite();
Transaction? transaction = null;
if (!realm.IsInTransaction)
transaction = realm.BeginWrite();
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)
{
using var transaction = realm.BeginWrite();
Transaction? transaction = null;
if (!realm.IsInTransaction)
transaction = realm.BeginWrite();
var result = function(realm);
transaction.Commit();
transaction?.Commit();
return result;
}

View File

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

View File

@ -1,10 +1,7 @@
// 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.IO;
using System.Text;
@ -17,34 +14,31 @@ namespace osu.Game.IO
public class LineBufferedReader : IDisposable
{
private readonly StreamReader streamReader;
private readonly Queue<string> lineBuffer;
private string? peekedLine;
public LineBufferedReader(Stream stream, bool leaveOpen = false)
{
streamReader = new StreamReader(stream, Encoding.UTF8, true, 1024, leaveOpen);
lineBuffer = new Queue<string>();
streamReader = new StreamReader(stream, Encoding.UTF8, true, -1, leaveOpen);
}
/// <summary>
/// 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.
/// </summary>
public string PeekLine()
{
if (lineBuffer.Count > 0)
return lineBuffer.Peek();
string line = streamReader.ReadLine();
if (line != null)
lineBuffer.Enqueue(line);
return line;
}
public string? PeekLine() => peekedLine ??= streamReader.ReadLine();
/// <summary>
/// Reads the next line from the stream and consumes it.
/// If a line was peeked, that same line will then be consumed and returned.
/// </summary>
public string ReadLine() => lineBuffer.Count > 0 ? lineBuffer.Dequeue() : streamReader.ReadLine();
public string? ReadLine()
{
string? line = peekedLine ?? streamReader.ReadLine();
peekedLine = null;
return line;
}
/// <summary>
/// Reads the stream to its end and returns the text read.
@ -53,14 +47,13 @@ namespace osu.Game.IO
public string ReadToEnd()
{
string remainingText = streamReader.ReadToEnd();
if (lineBuffer.Count == 0)
if (peekedLine == null)
return remainingText;
var builder = new StringBuilder();
// this might not be completely correct due to varying platform line endings
while (lineBuffer.Count > 0)
builder.AppendLine(lineBuffer.Dequeue());
builder.AppendLine(peekedLine);
builder.Append(remainingText);
return builder.ToString();
@ -68,7 +61,7 @@ namespace osu.Game.IO
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));
if (Score.Files.Count > 0)
{
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))));
}
return items.ToArray();
}

View File

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

View File

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