Merge branch 'master' into fix-spinner-tests

This commit is contained in:
Dan Balasescu 2021-10-13 17:02:15 +09:00 committed by GitHub
commit 1d43e472c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 525 additions and 127 deletions

View File

@ -1,3 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;

View File

@ -0,0 +1,213 @@
// 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.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Models;
using Realms;
#nullable enable
namespace osu.Game.Tests.Database
{
public class RealmLiveTests : RealmTest
{
[Test]
public void TestLiveCastability()
{
RunTestWithRealm((realmFactory, _) =>
{
RealmLive<RealmBeatmap> beatmap = realmFactory.CreateContext().Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata()))).ToLive();
ILive<IBeatmapInfo> iBeatmap = beatmap;
Assert.AreEqual(0, iBeatmap.Value.Length);
});
}
[Test]
public void TestValueAccessWithOpenContext()
{
RunTestWithRealm((realmFactory, _) =>
{
RealmLive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
{
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
liveBeatmap = beatmap.ToLive();
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
Debug.Assert(liveBeatmap != null);
Task.Factory.StartNew(() =>
{
Assert.DoesNotThrow(() =>
{
using (realmFactory.CreateContext())
{
var resolved = liveBeatmap.Value;
Assert.IsTrue(resolved.Realm.IsClosed);
Assert.IsTrue(resolved.IsValid);
// can access properties without a crash.
Assert.IsFalse(resolved.Hidden);
}
});
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
});
}
[Test]
public void TestScopedReadWithoutContext()
{
RunTestWithRealm((realmFactory, _) =>
{
RealmLive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
{
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
liveBeatmap = beatmap.ToLive();
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
Debug.Assert(liveBeatmap != null);
Task.Factory.StartNew(() =>
{
liveBeatmap.PerformRead(beatmap =>
{
Assert.IsTrue(beatmap.IsValid);
Assert.IsFalse(beatmap.Hidden);
});
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
});
}
[Test]
public void TestScopedWriteWithoutContext()
{
RunTestWithRealm((realmFactory, _) =>
{
RealmLive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
{
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
liveBeatmap = beatmap.ToLive();
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
Debug.Assert(liveBeatmap != null);
Task.Factory.StartNew(() =>
{
liveBeatmap.PerformWrite(beatmap => { beatmap.Hidden = true; });
liveBeatmap.PerformRead(beatmap => { Assert.IsTrue(beatmap.Hidden); });
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
});
}
[Test]
public void TestValueAccessWithoutOpenContextFails()
{
RunTestWithRealm((realmFactory, _) =>
{
RealmLive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
{
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
liveBeatmap = beatmap.ToLive();
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
Debug.Assert(liveBeatmap != null);
Task.Factory.StartNew(() =>
{
Assert.Throws<InvalidOperationException>(() =>
{
var unused = liveBeatmap.Value;
});
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
});
}
[Test]
public void TestLiveAssumptions()
{
RunTestWithRealm((realmFactory, _) =>
{
int changesTriggered = 0;
using (var updateThreadContext = realmFactory.CreateContext())
{
updateThreadContext.All<RealmBeatmap>().SubscribeForNotifications(gotChange);
RealmLive<RealmBeatmap>? liveBeatmap = null;
Task.Factory.StartNew(() =>
{
using (var threadContext = realmFactory.CreateContext())
{
var ruleset = CreateRuleset();
var beatmap = threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
// add a second beatmap to ensure that a full refresh occurs below.
// not just a refresh from the resolved Live.
threadContext.Write(r => r.Add(new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), new RealmBeatmapMetadata())));
liveBeatmap = beatmap.ToLive();
}
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
Debug.Assert(liveBeatmap != null);
// not yet seen by main context
Assert.AreEqual(0, updateThreadContext.All<RealmBeatmap>().Count());
Assert.AreEqual(0, changesTriggered);
var resolved = liveBeatmap.Value;
// retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
Assert.AreEqual(2, updateThreadContext.All<RealmBeatmap>().Count());
Assert.AreEqual(1, changesTriggered);
// even though the realm that this instance was resolved for was closed, it's still valid.
Assert.IsTrue(resolved.Realm.IsClosed);
Assert.IsTrue(resolved.IsValid);
// can access properties without a crash.
Assert.IsFalse(resolved.Hidden);
updateThreadContext.Write(r =>
{
// can use with the main context.
r.Remove(resolved);
});
}
void gotChange(IRealmCollection<RealmBeatmap> sender, ChangeSet changes, Exception error)
{
changesTriggered++;
}
});
}
}
}

View File

@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
@ -18,16 +19,19 @@ namespace osu.Game.Tests.Visual.Audio
{ {
public class TestSceneAudioFilter : OsuTestScene public class TestSceneAudioFilter : OsuTestScene
{ {
private OsuSpriteText lowpassText; private OsuSpriteText lowPassText;
private AudioFilter lowpassFilter; private AudioFilter lowPassFilter;
private OsuSpriteText highpassText; private OsuSpriteText highPassText;
private AudioFilter highpassFilter; private AudioFilter highPassFilter;
private Track track; private Track track;
private WaveformTestBeatmap beatmap; private WaveformTestBeatmap beatmap;
private OsuSliderBar<int> lowPassSlider;
private OsuSliderBar<int> highPassSlider;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(AudioManager audio) private void load(AudioManager audio)
{ {
@ -38,53 +42,89 @@ namespace osu.Game.Tests.Visual.Audio
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
lowpassFilter = new AudioFilter(audio.TrackMixer), lowPassFilter = new AudioFilter(audio.TrackMixer),
highpassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass), highPassFilter = new AudioFilter(audio.TrackMixer, BQFType.HighPass),
lowpassText = new OsuSpriteText lowPassText = new OsuSpriteText
{ {
Padding = new MarginPadding(20), Padding = new MarginPadding(20),
Text = $"Low Pass: {lowpassFilter.Cutoff.Value}hz", Text = $"Low Pass: {lowPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40) Font = new FontUsage(size: 40)
}, },
new OsuSliderBar<int> lowPassSlider = new OsuSliderBar<int>
{ {
Width = 500, Width = 500,
Height = 50, Height = 50,
Padding = new MarginPadding(20), Padding = new MarginPadding(20),
Current = { BindTarget = lowpassFilter.Cutoff } Current = new BindableInt
{
MinValue = 0,
MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF,
}
}, },
highpassText = new OsuSpriteText highPassText = new OsuSpriteText
{ {
Padding = new MarginPadding(20), Padding = new MarginPadding(20),
Text = $"High Pass: {highpassFilter.Cutoff.Value}hz", Text = $"High Pass: {highPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40) Font = new FontUsage(size: 40)
}, },
new OsuSliderBar<int> highPassSlider = new OsuSliderBar<int>
{ {
Width = 500, Width = 500,
Height = 50, Height = 50,
Padding = new MarginPadding(20), Padding = new MarginPadding(20),
Current = { BindTarget = highpassFilter.Cutoff } Current = new BindableInt
{
MinValue = 0,
MaxValue = AudioFilter.MAX_LOWPASS_CUTOFF,
}
} }
} }
}); });
lowpassFilter.Cutoff.ValueChanged += e => lowpassText.Text = $"Low Pass: {e.NewValue}hz";
highpassFilter.Cutoff.ValueChanged += e => highpassText.Text = $"High Pass: {e.NewValue}hz"; lowPassSlider.Current.ValueChanged += e =>
{
lowPassText.Text = $"Low Pass: {e.NewValue}hz";
lowPassFilter.Cutoff = e.NewValue;
};
highPassSlider.Current.ValueChanged += e =>
{
highPassText.Text = $"High Pass: {e.NewValue}hz";
highPassFilter.Cutoff = e.NewValue;
};
} }
#region Overrides of Drawable
protected override void Update()
{
base.Update();
highPassSlider.Current.Value = highPassFilter.Cutoff;
lowPassSlider.Current.Value = lowPassFilter.Cutoff;
}
#endregion
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {
AddStep("Play Track", () => track.Start()); AddStep("Play Track", () => track.Start());
AddStep("Reset filters", () =>
{
lowPassFilter.Cutoff = AudioFilter.MAX_LOWPASS_CUTOFF;
highPassFilter.Cutoff = 0;
});
waitTrackPlay(); waitTrackPlay();
} }
[Test] [Test]
public void TestLowPass() public void TestLowPassSweep()
{ {
AddStep("Filter Sweep", () => AddStep("Filter Sweep", () =>
{ {
lowpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then() lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
.CutoffTo(0, 2000, Easing.OutCubic); .CutoffTo(0, 2000, Easing.OutCubic);
}); });
@ -92,7 +132,7 @@ namespace osu.Game.Tests.Visual.Audio
AddStep("Filter Sweep (reverse)", () => AddStep("Filter Sweep (reverse)", () =>
{ {
lowpassFilter.CutoffTo(0).Then() lowPassFilter.CutoffTo(0).Then()
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic); .CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
}); });
@ -101,11 +141,11 @@ namespace osu.Game.Tests.Visual.Audio
} }
[Test] [Test]
public void TestHighPass() public void TestHighPassSweep()
{ {
AddStep("Filter Sweep", () => AddStep("Filter Sweep", () =>
{ {
highpassFilter.CutoffTo(0).Then() highPassFilter.CutoffTo(0).Then()
.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic); .CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 2000, Easing.InCubic);
}); });
@ -113,7 +153,7 @@ namespace osu.Game.Tests.Visual.Audio
AddStep("Filter Sweep (reverse)", () => AddStep("Filter Sweep (reverse)", () =>
{ {
highpassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then() highPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF).Then()
.CutoffTo(0, 2000, Easing.OutCubic); .CutoffTo(0, 2000, Easing.OutCubic);
}); });

View File

@ -4,7 +4,6 @@
using System.Diagnostics; using System.Diagnostics;
using ManagedBass.Fx; using ManagedBass.Fx;
using osu.Framework.Audio.Mixing; using osu.Framework.Audio.Mixing;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
namespace osu.Game.Audio.Effects namespace osu.Game.Audio.Effects
@ -21,10 +20,25 @@ namespace osu.Game.Audio.Effects
private readonly BQFParameters filter; private readonly BQFParameters filter;
private readonly BQFType type; private readonly BQFType type;
private bool isAttached;
private int cutoff;
/// <summary> /// <summary>
/// The current cutoff of this filter. /// The cutoff frequency of this filter.
/// </summary> /// </summary>
public BindableNumber<int> Cutoff { get; } public int Cutoff
{
get => cutoff;
set
{
if (value == cutoff)
return;
cutoff = value;
updateFilter(cutoff);
}
}
/// <summary> /// <summary>
/// A Component that implements a BASS FX BiQuad Filter Effect. /// A Component that implements a BASS FX BiQuad Filter Effect.
@ -36,102 +50,96 @@ namespace osu.Game.Audio.Effects
this.mixer = mixer; this.mixer = mixer;
this.type = type; this.type = type;
int initialCutoff;
switch (type)
{
case BQFType.HighPass:
initialCutoff = 1;
break;
case BQFType.LowPass:
initialCutoff = MAX_LOWPASS_CUTOFF;
break;
default:
initialCutoff = 500; // A default that should ensure audio remains audible for other filters.
break;
}
Cutoff = new BindableNumber<int>(initialCutoff)
{
MinValue = 1,
MaxValue = MAX_LOWPASS_CUTOFF
};
filter = new BQFParameters filter = new BQFParameters
{ {
lFilter = type, lFilter = type,
fCenter = initialCutoff,
fBandwidth = 0, fBandwidth = 0,
fQ = 0.7f // This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0) // This allows fCenter to go up to 22049hz (nyquist - 1hz) without overflowing and causing weird filter behaviour (see: https://www.un4seen.com/forum/?topic=19542.0)
fQ = 0.7f
}; };
// Don't start attached if this is low-pass or high-pass filter (as they have special auto-attach/detach logic) Cutoff = getInitialCutoff(type);
if (type != BQFType.LowPass && type != BQFType.HighPass)
attachFilter();
Cutoff.ValueChanged += updateFilter;
} }
private void attachFilter() private int getInitialCutoff(BQFType type)
{ {
Debug.Assert(!mixer.Effects.Contains(filter)); switch (type)
mixer.Effects.Add(filter);
}
private void detachFilter()
{
Debug.Assert(mixer.Effects.Contains(filter));
mixer.Effects.Remove(filter);
}
private void updateFilter(ValueChangedEvent<int> cutoff)
{
// Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz.
if (type == BQFType.LowPass)
{ {
if (cutoff.NewValue >= MAX_LOWPASS_CUTOFF) case BQFType.HighPass:
{ return 1;
detachFilter();
return;
}
if (cutoff.OldValue >= MAX_LOWPASS_CUTOFF && cutoff.NewValue < MAX_LOWPASS_CUTOFF) case BQFType.LowPass:
attachFilter(); return MAX_LOWPASS_CUTOFF;
default:
return 500; // A default that should ensure audio remains audible for other filters.
}
}
private void updateFilter(int newValue)
{
switch (type)
{
case BQFType.LowPass:
// Workaround for weird behaviour when rapidly setting fCenter of a low-pass filter to nyquist - 1hz.
if (newValue >= MAX_LOWPASS_CUTOFF)
{
ensureDetached();
return;
}
break;
// Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz.
case BQFType.HighPass:
if (newValue <= 1)
{
ensureDetached();
return;
}
break;
} }
// Workaround for weird behaviour when rapidly setting fCenter of a high-pass filter to 1hz. ensureAttached();
if (type == BQFType.HighPass)
{
if (cutoff.NewValue <= 1)
{
detachFilter();
return;
}
if (cutoff.OldValue <= 1 && cutoff.NewValue > 1)
attachFilter();
}
var filterIndex = mixer.Effects.IndexOf(filter); var filterIndex = mixer.Effects.IndexOf(filter);
if (filterIndex < 0) return; if (filterIndex < 0) return;
if (mixer.Effects[filterIndex] is BQFParameters existingFilter) if (mixer.Effects[filterIndex] is BQFParameters existingFilter)
{ {
existingFilter.fCenter = cutoff.NewValue; existingFilter.fCenter = newValue;
// required to update effect with new parameters. // required to update effect with new parameters.
mixer.Effects[filterIndex] = existingFilter; mixer.Effects[filterIndex] = existingFilter;
} }
} }
private void ensureAttached()
{
if (isAttached)
return;
Debug.Assert(!mixer.Effects.Contains(filter));
mixer.Effects.Add(filter);
isAttached = true;
}
private void ensureDetached()
{
if (!isAttached)
return;
Debug.Assert(mixer.Effects.Contains(filter));
mixer.Effects.Remove(filter);
isAttached = false;
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
ensureDetached();
if (mixer.Effects.Contains(filter))
detachFilter();
} }
} }
} }

View File

@ -1,7 +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.
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms; using osu.Framework.Graphics.Transforms;
@ -12,7 +11,7 @@ namespace osu.Game.Audio.Effects
/// <summary> /// <summary>
/// The filter cutoff. /// The filter cutoff.
/// </summary> /// </summary>
BindableNumber<int> Cutoff { get; } int Cutoff { get; set; }
} }
public static class FilterableAudioComponentExtensions public static class FilterableAudioComponentExtensions
@ -40,7 +39,7 @@ namespace osu.Game.Audio.Effects
public static TransformSequence<T> CutoffTo<T, TEasing>(this T component, int newCutoff, double duration, TEasing easing) public static TransformSequence<T> CutoffTo<T, TEasing>(this T component, int newCutoff, double duration, TEasing easing)
where T : class, ITransformableFilter, IDrawable where T : class, ITransformableFilter, IDrawable
where TEasing : IEasingFunction where TEasing : IEasingFunction
=> component.TransformBindableTo(component.Cutoff, newCutoff, duration, easing); => component.TransformTo(nameof(component.Cutoff), newCutoff, duration, easing);
/// <summary> /// <summary>
/// Smoothly adjusts filter cutoff over time. /// Smoothly adjusts filter cutoff over time.
@ -49,6 +48,6 @@ namespace osu.Game.Audio.Effects
public static TransformSequence<T> CutoffTo<T, TEasing>(this TransformSequence<T> sequence, int newCutoff, double duration, TEasing easing) public static TransformSequence<T> CutoffTo<T, TEasing>(this TransformSequence<T> sequence, int newCutoff, double duration, TEasing easing)
where T : class, ITransformableFilter, IDrawable where T : class, ITransformableFilter, IDrawable
where TEasing : IEasingFunction where TEasing : IEasingFunction
=> sequence.Append(o => o.TransformBindableTo(o.Cutoff, newCutoff, duration, easing)); => sequence.Append(o => o.TransformTo(nameof(o.Cutoff), newCutoff, duration, easing));
} }
} }

View File

@ -0,0 +1,111 @@
// 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.Threading;
using Realms;
#nullable enable
namespace osu.Game.Database
{
/// <summary>
/// Provides a method of working with realm objects over longer application lifetimes.
/// </summary>
/// <typeparam name="T">The underlying object type.</typeparam>
public class RealmLive<T> : ILive<T> where T : RealmObject, IHasGuidPrimaryKey
{
public Guid ID { get; }
private readonly SynchronizationContext? fetchedContext;
private readonly int fetchedThreadId;
/// <summary>
/// The original live data used to create this instance.
/// </summary>
private readonly T data;
/// <summary>
/// Construct a new instance of live realm data.
/// </summary>
/// <param name="data">The realm data.</param>
public RealmLive(T data)
{
this.data = data;
fetchedContext = SynchronizationContext.Current;
fetchedThreadId = Thread.CurrentThread.ManagedThreadId;
ID = data.ID;
}
/// <summary>
/// Perform a read operation on this live object.
/// </summary>
/// <param name="perform">The action to perform.</param>
public void PerformRead(Action<T> perform)
{
if (originalDataValid)
{
perform(data);
return;
}
using (var realm = Realm.GetInstance(data.Realm.Config))
perform(realm.Find<T>(ID));
}
/// <summary>
/// Perform a read operation on this live object.
/// </summary>
/// <param name="perform">The action to perform.</param>
public TReturn PerformRead<TReturn>(Func<T, TReturn> perform)
{
if (typeof(RealmObjectBase).IsAssignableFrom(typeof(TReturn)))
throw new InvalidOperationException($"Realm live objects should not exit the scope of {nameof(PerformRead)}.");
if (originalDataValid)
return perform(data);
using (var realm = Realm.GetInstance(data.Realm.Config))
return perform(realm.Find<T>(ID));
}
/// <summary>
/// Perform a write operation on this live object.
/// </summary>
/// <param name="perform">The action to perform.</param>
public void PerformWrite(Action<T> perform) =>
PerformRead(t =>
{
var transaction = t.Realm.BeginWrite();
perform(t);
transaction.Commit();
});
public T Value
{
get
{
if (originalDataValid)
return data;
T retrieved;
using (var realm = Realm.GetInstance(data.Realm.Config))
retrieved = realm.Find<T>(ID);
if (!retrieved.IsValid)
throw new InvalidOperationException("Attempted to access value without an open context");
return retrieved;
}
}
private bool originalDataValid => isCorrectThread && data.IsValid;
// this matches realm's internal thread validation (see https://github.com/realm/realm-dotnet/blob/903b4d0b304f887e37e2d905384fb572a6496e70/Realm/Realm/Native/SynchronizationContextScheduler.cs#L72)
private bool isCorrectThread
=> (fetchedContext != null && SynchronizationContext.Current == fetchedContext) || fetchedThreadId == Thread.CurrentThread.ManagedThreadId;
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using AutoMapper; using AutoMapper;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using Realms; using Realms;
@ -47,5 +48,17 @@ namespace osu.Game.Database
return mapper.Map<T>(item); return mapper.Map<T>(item);
} }
public static List<RealmLive<T>> ToLive<T>(this IEnumerable<T> realmList)
where T : RealmObject, IHasGuidPrimaryKey
{
return realmList.Select(l => new RealmLive<T>(l)).ToList();
}
public static RealmLive<T> ToLive<T>(this T realmObject)
where T : RealmObject, IHasGuidPrimaryKey
{
return new RealmLive<T>(realmObject);
}
} }
} }

View File

@ -35,6 +35,8 @@ namespace osu.Game.Screens.Play
{ {
protected const float BACKGROUND_BLUR = 15; protected const float BACKGROUND_BLUR = 15;
private const double content_out_duration = 300;
public override bool HideOverlaysOnEnter => hideOverlays; public override bool HideOverlaysOnEnter => hideOverlays;
public override bool DisallowExternalBeatmapRulesetChanges => true; public override bool DisallowExternalBeatmapRulesetChanges => true;
@ -135,36 +137,39 @@ namespace osu.Game.Screens.Play
muteWarningShownOnce = sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce); muteWarningShownOnce = sessionStatics.GetBindable<bool>(Static.MutedAudioNotificationShownOnce);
batteryWarningShownOnce = sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce); batteryWarningShownOnce = sessionStatics.GetBindable<bool>(Static.LowBatteryNotificationShownOnce);
InternalChild = (content = new LogoTrackingContainer InternalChildren = new Drawable[]
{ {
Anchor = Anchor.Centre, (content = new LogoTrackingContainer
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
}).WithChildren(new Drawable[]
{
MetadataInfo = new BeatmapMetadataDisplay(Beatmap.Value, Mods, content.LogoFacade)
{ {
Alpha = 0,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}, RelativeSizeAxes = Axes.Both,
PlayerSettings = new FillFlowContainer<PlayerSettingsGroup> }).WithChildren(new Drawable[]
{ {
Anchor = Anchor.TopRight, MetadataInfo = new BeatmapMetadataDisplay(Beatmap.Value, Mods, content.LogoFacade)
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 20),
Margin = new MarginPadding(25),
Children = new PlayerSettingsGroup[]
{ {
VisualSettings = new VisualSettings(), Alpha = 0,
new InputSettings() Anchor = Anchor.Centre,
} Origin = Anchor.Centre,
}, },
idleTracker = new IdleTracker(750), PlayerSettings = new FillFlowContainer<PlayerSettingsGroup>
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 20),
Margin = new MarginPadding(25),
Children = new PlayerSettingsGroup[]
{
VisualSettings = new VisualSettings(),
new InputSettings()
}
},
idleTracker = new IdleTracker(750),
}),
lowPassFilter = new AudioFilter(audio.TrackMixer) lowPassFilter = new AudioFilter(audio.TrackMixer)
}); };
if (Beatmap.Value.BeatmapInfo.EpilepsyWarning) if (Beatmap.Value.BeatmapInfo.EpilepsyWarning)
{ {
@ -195,7 +200,6 @@ namespace osu.Game.Screens.Play
epilepsyWarning.DimmableBackground = b; epilepsyWarning.DimmableBackground = b;
}); });
lowPassFilter.CutoffTo(500, 100, Easing.OutCubic);
Beatmap.Value.Track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment); Beatmap.Value.Track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment);
content.ScaleTo(0.7f); content.ScaleTo(0.7f);
@ -240,15 +244,15 @@ namespace osu.Game.Screens.Play
public override bool OnExiting(IScreen next) public override bool OnExiting(IScreen next)
{ {
cancelLoad(); cancelLoad();
contentOut();
content.ScaleTo(0.7f, 150, Easing.InQuint); // Ensure the screen doesn't expire until all the outwards fade operations have completed.
this.FadeOut(150); this.Delay(content_out_duration).FadeOut();
ApplyToBackground(b => b.IgnoreUserSettings.Value = true); ApplyToBackground(b => b.IgnoreUserSettings.Value = true);
BackgroundBrightnessReduction = false; BackgroundBrightnessReduction = false;
Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment); Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment);
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic);
return base.OnExiting(next); return base.OnExiting(next);
} }
@ -344,6 +348,7 @@ namespace osu.Game.Screens.Play
content.FadeInFromZero(400); content.FadeInFromZero(400);
content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer); content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer);
lowPassFilter.CutoffTo(1000, 650, Easing.OutQuint);
ApplyToBackground(b => b?.FadeColour(Color4.White, 800, Easing.OutQuint)); ApplyToBackground(b => b?.FadeColour(Color4.White, 800, Easing.OutQuint));
} }
@ -353,8 +358,9 @@ namespace osu.Game.Screens.Play
// Ensure the logo is no longer tracking before we scale the content // Ensure the logo is no longer tracking before we scale the content
content.StopTracking(); content.StopTracking();
content.ScaleTo(0.7f, 300, Easing.InQuint); content.ScaleTo(0.7f, content_out_duration * 2, Easing.OutQuint);
content.FadeOut(250); content.FadeOut(content_out_duration, Easing.OutQuint);
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, content_out_duration);
} }
private void pushWhenLoaded() private void pushWhenLoaded()
@ -381,7 +387,7 @@ namespace osu.Game.Screens.Play
contentOut(); contentOut();
TransformSequence<PlayerLoader> pushSequence = this.Delay(250); TransformSequence<PlayerLoader> pushSequence = this.Delay(content_out_duration);
// only show if the warning was created (i.e. the beatmap needs it) // only show if the warning was created (i.e. the beatmap needs it)
// and this is not a restart of the map (the warning expires after first load). // and this is not a restart of the map (the warning expires after first load).
@ -400,6 +406,11 @@ namespace osu.Game.Screens.Play
}) })
.Delay(EpilepsyWarning.FADE_DURATION); .Delay(EpilepsyWarning.FADE_DURATION);
} }
else
{
// This goes hand-in-hand with the restoration of low pass filter in contentOut().
this.TransformBindableTo(volumeAdjustment, 0, content_out_duration, Easing.OutCubic);
}
pushSequence.Schedule(() => pushSequence.Schedule(() =>
{ {