diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 68f8ef51ef..3c52802cf6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -77,10 +77,6 @@ jobs:
run: msbuild osu.Android/osu.Android.csproj /restore /p:Configuration=Debug
build-only-ios:
- # While this workflow technically *can* run, it fails as iOS builds are blocked by multiple issues.
- # See https://github.com/ppy/osu-framework/issues/4677 for the details.
- # The job can be unblocked once those issues are resolved and game deployments can happen again.
- if: false
name: Build only (iOS)
runs-on: macos-latest
timeout-minutes: 60
diff --git a/osu.Game.Tests/Database/GeneralUsageTests.cs b/osu.Game.Tests/Database/GeneralUsageTests.cs
index 3e8b6091fd..841bf2de43 100644
--- a/osu.Game.Tests/Database/GeneralUsageTests.cs
+++ b/osu.Game.Tests/Database/GeneralUsageTests.cs
@@ -5,6 +5,8 @@ using System;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
+using osu.Game.Models;
+using Realms;
#nullable enable
@@ -33,6 +35,39 @@ namespace osu.Game.Tests.Database
});
}
+ ///
+ /// Test to ensure that a `CreateContext` call nested inside a subscription doesn't cause any deadlocks
+ /// due to context fetching semaphores.
+ ///
+ [Test]
+ public void TestNestedContextCreationWithSubscription()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ bool callbackRan = false;
+
+ using (var context = realmFactory.CreateContext())
+ {
+ var subscription = context.All().SubscribeForNotifications((sender, changes, error) =>
+ {
+ using (realmFactory.CreateContext())
+ {
+ callbackRan = true;
+ }
+ });
+
+ // Force the callback above to run.
+ using (realmFactory.CreateContext())
+ {
+ }
+
+ subscription.Dispose();
+ }
+
+ Assert.IsTrue(callbackRan);
+ });
+ }
+
[Test]
public void TestBlockOperationsWithContention()
{
diff --git a/osu.Game.Tests/Database/RealmLiveTests.cs b/osu.Game.Tests/Database/RealmLiveTests.cs
index 16e2c0fc6a..8ab19c8329 100644
--- a/osu.Game.Tests/Database/RealmLiveTests.cs
+++ b/osu.Game.Tests/Database/RealmLiveTests.cs
@@ -62,43 +62,6 @@ namespace osu.Game.Tests.Database
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
}
- [Test]
- public void TestValueAccessWithOpenContext()
- {
- RunTestWithRealm((realmFactory, _) =>
- {
- ILive? 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()
{
@@ -154,6 +117,60 @@ namespace osu.Game.Tests.Database
});
}
+ [Test]
+ public void TestValueAccessNonManaged()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ var beatmap = new RealmBeatmap(CreateRuleset(), new RealmBeatmapDifficulty(), new RealmBeatmapMetadata());
+ var liveBeatmap = beatmap.ToLive();
+
+ Assert.DoesNotThrow(() =>
+ {
+ var __ = liveBeatmap.Value;
+ });
+ });
+ }
+
+ [Test]
+ public void TestValueAccessWithOpenContextFails()
+ {
+ RunTestWithRealm((realmFactory, _) =>
+ {
+ ILive? 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(() =>
+ {
+ // Can't be used, without a valid context.
+ Assert.Throws(() =>
+ {
+ var __ = liveBeatmap.Value;
+ });
+
+ // Can't be used, even from within a valid context.
+ using (realmFactory.CreateContext())
+ {
+ Assert.Throws(() =>
+ {
+ var __ = liveBeatmap.Value;
+ });
+ }
+ }, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
+ });
+ }
+
[Test]
public void TestValueAccessWithoutOpenContextFails()
{
@@ -215,23 +232,22 @@ namespace osu.Game.Tests.Database
Assert.AreEqual(0, updateThreadContext.All().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().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 =>
+ liveBeatmap.PerformRead(resolved =>
{
- // can use with the main context.
- r.Remove(resolved);
+ // retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
+ // ReSharper disable once AccessToDisposedClosure
+ Assert.AreEqual(2, updateThreadContext.All().Count());
+ Assert.AreEqual(1, changesTriggered);
+
+ // can access properties without a crash.
+ Assert.IsFalse(resolved.Hidden);
+
+ // ReSharper disable once AccessToDisposedClosure
+ updateThreadContext.Write(r =>
+ {
+ // can use with the main context.
+ r.Remove(resolved);
+ });
});
}
diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
index e458e66ab7..ae8eec2629 100644
--- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
+++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs
@@ -3,12 +3,14 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
+using osu.Game.Utils;
namespace osu.Game.Tests.NonVisual
{
@@ -20,8 +22,10 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator().CreateDifficultyAdjustmentModCombinations();
- Assert.AreEqual(1, combinations.Length);
- Assert.IsTrue(combinations[0] is ModNoMod);
+ assertCombinations(new[]
+ {
+ new[] { typeof(ModNoMod) }
+ }, combinations);
}
[Test]
@@ -29,9 +33,11 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModA()).CreateDifficultyAdjustmentModCombinations();
- Assert.AreEqual(2, combinations.Length);
- Assert.IsTrue(combinations[0] is ModNoMod);
- Assert.IsTrue(combinations[1] is ModA);
+ assertCombinations(new[]
+ {
+ new[] { typeof(ModNoMod) },
+ new[] { typeof(ModA) }
+ }, combinations);
}
[Test]
@@ -39,14 +45,13 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModB()).CreateDifficultyAdjustmentModCombinations();
- Assert.AreEqual(4, combinations.Length);
- Assert.IsTrue(combinations[0] is ModNoMod);
- Assert.IsTrue(combinations[1] is ModA);
- Assert.IsTrue(combinations[2] is MultiMod);
- Assert.IsTrue(combinations[3] is ModB);
-
- Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
- Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
+ assertCombinations(new[]
+ {
+ new[] { typeof(ModNoMod) },
+ new[] { typeof(ModA) },
+ new[] { typeof(ModA), typeof(ModB) },
+ new[] { typeof(ModB) }
+ }, combinations);
}
[Test]
@@ -54,10 +59,12 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModIncompatibleWithA()).CreateDifficultyAdjustmentModCombinations();
- Assert.AreEqual(3, combinations.Length);
- Assert.IsTrue(combinations[0] is ModNoMod);
- Assert.IsTrue(combinations[1] is ModA);
- Assert.IsTrue(combinations[2] is ModIncompatibleWithA);
+ assertCombinations(new[]
+ {
+ new[] { typeof(ModNoMod) },
+ new[] { typeof(ModA) },
+ new[] { typeof(ModIncompatibleWithA) }
+ }, combinations);
}
[Test]
@@ -65,22 +72,17 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModB(), new ModIncompatibleWithA(), new ModIncompatibleWithAAndB()).CreateDifficultyAdjustmentModCombinations();
- Assert.AreEqual(8, combinations.Length);
- Assert.IsTrue(combinations[0] is ModNoMod);
- Assert.IsTrue(combinations[1] is ModA);
- Assert.IsTrue(combinations[2] is MultiMod);
- Assert.IsTrue(combinations[3] is ModB);
- Assert.IsTrue(combinations[4] is MultiMod);
- Assert.IsTrue(combinations[5] is ModIncompatibleWithA);
- Assert.IsTrue(combinations[6] is MultiMod);
- Assert.IsTrue(combinations[7] is ModIncompatibleWithAAndB);
-
- Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
- Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
- Assert.IsTrue(((MultiMod)combinations[4]).Mods[0] is ModB);
- Assert.IsTrue(((MultiMod)combinations[4]).Mods[1] is ModIncompatibleWithA);
- Assert.IsTrue(((MultiMod)combinations[6]).Mods[0] is ModIncompatibleWithA);
- Assert.IsTrue(((MultiMod)combinations[6]).Mods[1] is ModIncompatibleWithAAndB);
+ assertCombinations(new[]
+ {
+ new[] { typeof(ModNoMod) },
+ new[] { typeof(ModA) },
+ new[] { typeof(ModA), typeof(ModB) },
+ new[] { typeof(ModB) },
+ new[] { typeof(ModB), typeof(ModIncompatibleWithA) },
+ new[] { typeof(ModIncompatibleWithA) },
+ new[] { typeof(ModIncompatibleWithA), typeof(ModIncompatibleWithAAndB) },
+ new[] { typeof(ModIncompatibleWithAAndB) },
+ }, combinations);
}
[Test]
@@ -88,10 +90,12 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModAofA(), new ModIncompatibleWithAofA()).CreateDifficultyAdjustmentModCombinations();
- Assert.AreEqual(3, combinations.Length);
- Assert.IsTrue(combinations[0] is ModNoMod);
- Assert.IsTrue(combinations[1] is ModAofA);
- Assert.IsTrue(combinations[2] is ModIncompatibleWithAofA);
+ assertCombinations(new[]
+ {
+ new[] { typeof(ModNoMod) },
+ new[] { typeof(ModAofA) },
+ new[] { typeof(ModIncompatibleWithAofA) }
+ }, combinations);
}
[Test]
@@ -99,17 +103,13 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModC())).CreateDifficultyAdjustmentModCombinations();
- Assert.AreEqual(4, combinations.Length);
- Assert.IsTrue(combinations[0] is ModNoMod);
- Assert.IsTrue(combinations[1] is ModA);
- Assert.IsTrue(combinations[2] is MultiMod);
- Assert.IsTrue(combinations[3] is MultiMod);
-
- Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
- Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
- Assert.IsTrue(((MultiMod)combinations[2]).Mods[2] is ModC);
- Assert.IsTrue(((MultiMod)combinations[3]).Mods[0] is ModB);
- Assert.IsTrue(((MultiMod)combinations[3]).Mods[1] is ModC);
+ assertCombinations(new[]
+ {
+ new[] { typeof(ModNoMod) },
+ new[] { typeof(ModA) },
+ new[] { typeof(ModA), typeof(ModB), typeof(ModC) },
+ new[] { typeof(ModB), typeof(ModC) }
+ }, combinations);
}
[Test]
@@ -117,13 +117,12 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModIncompatibleWithA())).CreateDifficultyAdjustmentModCombinations();
- Assert.AreEqual(3, combinations.Length);
- Assert.IsTrue(combinations[0] is ModNoMod);
- Assert.IsTrue(combinations[1] is ModA);
- Assert.IsTrue(combinations[2] is MultiMod);
-
- Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModB);
- Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModIncompatibleWithA);
+ assertCombinations(new[]
+ {
+ new[] { typeof(ModNoMod) },
+ new[] { typeof(ModA) },
+ new[] { typeof(ModB), typeof(ModIncompatibleWithA) }
+ }, combinations);
}
[Test]
@@ -131,13 +130,28 @@ namespace osu.Game.Tests.NonVisual
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModA(), new ModB())).CreateDifficultyAdjustmentModCombinations();
- Assert.AreEqual(3, combinations.Length);
- Assert.IsTrue(combinations[0] is ModNoMod);
- Assert.IsTrue(combinations[1] is ModA);
- Assert.IsTrue(combinations[2] is MultiMod);
+ assertCombinations(new[]
+ {
+ new[] { typeof(ModNoMod) },
+ new[] { typeof(ModA) },
+ new[] { typeof(ModA), typeof(ModB) }
+ }, combinations);
+ }
- Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
- Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
+ private void assertCombinations(Type[][] expectedCombinations, Mod[] actualCombinations)
+ {
+ Assert.AreEqual(expectedCombinations.Length, actualCombinations.Length);
+
+ Assert.Multiple(() =>
+ {
+ for (int i = 0; i < expectedCombinations.Length; ++i)
+ {
+ Type[] expectedTypes = expectedCombinations[i];
+ Type[] actualTypes = ModUtils.FlattenMod(actualCombinations[i]).Select(m => m.GetType()).ToArray();
+
+ Assert.That(expectedTypes, Is.EquivalentTo(actualTypes));
+ }
+ });
}
private class ModA : Mod
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
index 84b24ba3a1..a5229702a8 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
@@ -144,7 +144,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("set mods", () => SelectedMods.Value = new[] { new TaikoModDoubleTime() });
AddStep("confirm selection", () => songSelect.FinaliseSelection());
- AddStep("exit song select", () => songSelect.Exit());
+
+ AddUntilStep("song select exited", () => !songSelect.IsCurrentScreen());
AddAssert("beatmap not changed", () => Beatmap.Value.BeatmapInfo.Equals(selectedBeatmap));
AddAssert("ruleset not changed", () => Ruleset.Value.Equals(new TaikoRuleset().RulesetInfo));
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs
index c70906927e..981989c28a 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTeamVersus.cs
@@ -11,6 +11,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API.Requests.Responses;
+using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
@@ -118,6 +119,33 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("user still on team 0", () => (client.Room?.Users.FirstOrDefault()?.MatchState as TeamVersusUserState)?.TeamID == 0);
}
+ [Test]
+ public void TestSettingsUpdatedWhenChangingMatchType()
+ {
+ createRoom(() => new Room
+ {
+ Name = { Value = "Test Room" },
+ Type = { Value = MatchType.HeadToHead },
+ Playlist =
+ {
+ new PlaylistItem
+ {
+ Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
+ Ruleset = { Value = new OsuRuleset().RulesetInfo },
+ }
+ }
+ });
+
+ AddUntilStep("match type head to head", () => client.APIRoom?.Type.Value == MatchType.HeadToHead);
+
+ AddStep("change match type", () => client.ChangeSettings(new MultiplayerRoomSettings
+ {
+ MatchType = MatchType.TeamVersus
+ }));
+
+ AddUntilStep("api room updated to team versus", () => client.APIRoom?.Type.Value == MatchType.TeamVersus);
+ }
+
[Test]
public void TestChangeTypeViaMatchSettings()
{
@@ -152,6 +180,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for room open", () => this.ChildrenOfType().FirstOrDefault()?.IsLoaded == true);
AddWaitStep("wait for transition", 2);
+ AddUntilStep("create room button enabled", () => this.ChildrenOfType().Single().Enabled.Value);
AddStep("create room", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType().Single());
diff --git a/osu.Game/Database/BeatmapLookupCache.cs b/osu.Game/Database/BeatmapLookupCache.cs
index c4e20d59b6..c6f8244494 100644
--- a/osu.Game/Database/BeatmapLookupCache.cs
+++ b/osu.Game/Database/BeatmapLookupCache.cs
@@ -85,7 +85,7 @@ namespace osu.Game.Database
// Grab at most 50 unique beatmap IDs from the queue.
lock (taskAssignmentLock)
{
- while (pendingBeatmapTasks.Count > 0 && beatmapTasks.Count < 1)
+ while (pendingBeatmapTasks.Count > 0 && beatmapTasks.Count < 50)
{
(int id, TaskCompletionSource task) next = pendingBeatmapTasks.Dequeue();
@@ -102,8 +102,11 @@ namespace osu.Game.Database
}
}
+ if (beatmapTasks.Count == 0)
+ return;
+
// Query the beatmaps.
- var request = new GetBeatmapRequest(new APIBeatmap { OnlineID = beatmapTasks.Keys.First() });
+ var request = new GetBeatmapsRequest(beatmapTasks.Keys.ToArray());
// rather than queueing, we maintain our own single-threaded request stream.
// todo: we probably want retry logic here.
@@ -117,16 +120,19 @@ namespace osu.Game.Database
createNewTask();
}
- List foundBeatmaps = new List { request.Response };
+ List foundBeatmaps = request.Response?.Beatmaps;
- foreach (var beatmap in foundBeatmaps)
+ if (foundBeatmaps != null)
{
- if (beatmapTasks.TryGetValue(beatmap.OnlineID, out var tasks))
+ foreach (var beatmap in foundBeatmaps)
{
- foreach (var task in tasks)
- task.SetResult(beatmap);
+ if (beatmapTasks.TryGetValue(beatmap.OnlineID, out var tasks))
+ {
+ foreach (var task in tasks)
+ task.SetResult(beatmap);
- beatmapTasks.Remove(beatmap.OnlineID);
+ beatmapTasks.Remove(beatmap.OnlineID);
+ }
}
}
diff --git a/osu.Game/Database/ILive.cs b/osu.Game/Database/ILive.cs
index a863339f11..3011754bc1 100644
--- a/osu.Game/Database/ILive.cs
+++ b/osu.Game/Database/ILive.cs
@@ -38,10 +38,10 @@ namespace osu.Game.Database
bool IsManaged { get; }
///
- /// Resolve the value of this instance on the current thread's context.
+ /// Resolve the value of this instance on the update thread.
///
///
- /// After resolving the data should not be passed between threads.
+ /// After resolving, the data should not be passed between threads.
///
T Value { get; }
}
diff --git a/osu.Game/Database/RealmContextFactory.cs b/osu.Game/Database/RealmContextFactory.cs
index 2bc77934a8..a20139e830 100644
--- a/osu.Game/Database/RealmContextFactory.cs
+++ b/osu.Game/Database/RealmContextFactory.cs
@@ -52,6 +52,8 @@ namespace osu.Game.Database
///
private readonly SemaphoreSlim contextCreationLock = new SemaphoreSlim(1);
+ private readonly ThreadLocal currentThreadCanCreateContexts = new ThreadLocal();
+
private static readonly GlobalStatistic refreshes = GlobalStatistics.Get(@"Realm", @"Dirty Refreshes");
private static readonly GlobalStatistic contexts_created = GlobalStatistics.Get(@"Realm", @"Contexts (Created)");
@@ -151,9 +153,22 @@ namespace osu.Game.Database
if (isDisposed)
throw new ObjectDisposedException(nameof(RealmContextFactory));
+ bool tookSemaphoreLock = false;
+
try
{
- contextCreationLock.Wait();
+ if (!currentThreadCanCreateContexts.Value)
+ {
+ contextCreationLock.Wait();
+ currentThreadCanCreateContexts.Value = true;
+ tookSemaphoreLock = true;
+ }
+ else
+ {
+ // the semaphore is used to handle blocking of all context creation during certain periods.
+ // once the semaphore has been taken by this code section, it is safe to create further contexts on the same thread.
+ // this can happen if a realm subscription is active and triggers a callback which has user code that calls `CreateContext`.
+ }
contexts_created.Value++;
@@ -161,7 +176,11 @@ namespace osu.Game.Database
}
finally
{
- contextCreationLock.Release();
+ if (tookSemaphoreLock)
+ {
+ contextCreationLock.Release();
+ currentThreadCanCreateContexts.Value = false;
+ }
}
}
diff --git a/osu.Game/Database/RealmLive.cs b/osu.Game/Database/RealmLive.cs
index 5ee40f5b4d..2accea305a 100644
--- a/osu.Game/Database/RealmLive.cs
+++ b/osu.Game/Database/RealmLive.cs
@@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Threading;
+using osu.Framework.Development;
using Realms;
#nullable enable
@@ -19,9 +19,6 @@ namespace osu.Game.Database
public bool IsManaged => data.IsManaged;
- private readonly SynchronizationContext? fetchedContext;
- private readonly int fetchedThreadId;
-
///
/// The original live data used to create this instance.
///
@@ -35,12 +32,6 @@ namespace osu.Game.Database
{
this.data = data;
- if (data.IsManaged)
- {
- fetchedContext = SynchronizationContext.Current;
- fetchedThreadId = Thread.CurrentThread.ManagedThreadId;
- }
-
ID = data.ID;
}
@@ -50,7 +41,7 @@ namespace osu.Game.Database
/// The action to perform.
public void PerformRead(Action perform)
{
- if (originalDataValid)
+ if (!IsManaged)
{
perform(data);
return;
@@ -69,7 +60,7 @@ namespace osu.Game.Database
if (typeof(RealmObjectBase).IsAssignableFrom(typeof(TReturn)))
throw new InvalidOperationException($"Realm live objects should not exit the scope of {nameof(PerformRead)}.");
- if (originalDataValid)
+ if (!IsManaged)
return perform(data);
using (var realm = Realm.GetInstance(data.Realm.Config))
@@ -97,27 +88,20 @@ namespace osu.Game.Database
{
get
{
- if (originalDataValid)
+ if (!IsManaged)
return data;
- T retrieved;
+ if (!ThreadSafety.IsUpdateThread)
+ throw new InvalidOperationException($"Can't use {nameof(Value)} on managed objects from non-update threads");
- using (var realm = Realm.GetInstance(data.Realm.Config))
- retrieved = realm.Find(ID);
+ // When using Value, we rely on garbage collection for the realm instance used to retrieve the instance.
+ // As we are sure that this is on the update thread, there should always be an open and constantly refreshing realm instance to ensure file size growth is a non-issue.
+ var realm = Realm.GetInstance(data.Realm.Config);
- if (!retrieved.IsValid)
- throw new InvalidOperationException("Attempted to access value without an open context");
-
- return retrieved;
+ return realm.Find(ID);
}
}
- private bool originalDataValid => !IsManaged || (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;
-
public bool Equals(ILive? other) => ID == other?.ID;
}
}
diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs
index dae2d2549c..26f4e9fb3b 100644
--- a/osu.Game/Database/UserLookupCache.cs
+++ b/osu.Game/Database/UserLookupCache.cs
@@ -100,6 +100,9 @@ namespace osu.Game.Database
}
}
+ if (userTasks.Count == 0)
+ return;
+
// Query the users.
var request = new GetUsersRequest(userTasks.Keys.ToArray());
diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs
index 43195811dc..efb0b102d0 100644
--- a/osu.Game/Online/API/APIRequest.cs
+++ b/osu.Game/Online/API/APIRequest.cs
@@ -38,7 +38,12 @@ namespace osu.Game.Online.API
protected override void PostProcess()
{
base.PostProcess();
- Response = ((OsuJsonWebRequest)WebRequest)?.ResponseObject;
+
+ if (WebRequest != null)
+ {
+ Response = ((OsuJsonWebRequest)WebRequest).ResponseObject;
+ Logger.Log($"{GetType()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes");
+ }
}
internal void TriggerSuccess(T result)
diff --git a/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs
new file mode 100644
index 0000000000..1d71e22b77
--- /dev/null
+++ b/osu.Game/Online/API/Requests/GetBeatmapsRequest.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+
+namespace osu.Game.Online.API.Requests
+{
+ public class GetBeatmapsRequest : APIRequest
+ {
+ private readonly int[] beatmapIds;
+
+ private const int max_ids_per_request = 50;
+
+ public GetBeatmapsRequest(int[] beatmapIds)
+ {
+ if (beatmapIds.Length > max_ids_per_request)
+ throw new ArgumentException($"{nameof(GetBeatmapsRequest)} calls only support up to {max_ids_per_request} IDs at once");
+
+ this.beatmapIds = beatmapIds;
+ }
+
+ protected override string Target => "beatmaps/?ids[]=" + string.Join("&ids[]=", beatmapIds);
+ }
+}
diff --git a/osu.Game/Online/API/Requests/GetBeatmapsResponse.cs b/osu.Game/Online/API/Requests/GetBeatmapsResponse.cs
new file mode 100644
index 0000000000..c450c3269c
--- /dev/null
+++ b/osu.Game/Online/API/Requests/GetBeatmapsResponse.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using Newtonsoft.Json;
+using osu.Game.Online.API.Requests.Responses;
+
+namespace osu.Game.Online.API.Requests
+{
+ public class GetBeatmapsResponse : ResponseWithCursor
+ {
+ [JsonProperty("beatmaps")]
+ public List Beatmaps;
+ }
+}
diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
index 78bf2c4db3..60a7dda961 100644
--- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs
+++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs
@@ -702,6 +702,7 @@ namespace osu.Game.Online.Multiplayer
Room.Settings = settings;
APIRoom.Name.Value = Room.Settings.Name;
APIRoom.Password.Value = Room.Settings.Password;
+ APIRoom.Type.Value = Room.Settings.MatchType;
APIRoom.QueueMode.Value = Room.Settings.QueueMode;
RoomUpdated?.Invoke();
diff --git a/osu.Game/Overlays/OnScreenDisplay.cs b/osu.Game/Overlays/OnScreenDisplay.cs
index af6d24fc65..be9d3cd794 100644
--- a/osu.Game/Overlays/OnScreenDisplay.cs
+++ b/osu.Game/Overlays/OnScreenDisplay.cs
@@ -95,13 +95,13 @@ namespace osu.Game.Overlays
/// Displays the provided temporarily.
///
///
- public void Display(Toast toast)
+ public void Display(Toast toast) => Schedule(() =>
{
box.Child = toast;
DisplayTemporarily(box);
- }
+ });
- private void displayTrackedSettingChange(SettingDescription description) => Schedule(() => Display(new TrackedSettingToast(description)));
+ private void displayTrackedSettingChange(SettingDescription description) => Display(new TrackedSettingToast(description));
private TransformSequence fadeIn;
private ScheduledDelegate fadeOut;