mirror of
https://github.com/ppy/osu.git
synced 2025-01-06 21:52:54 +08:00
Merge branch 'master' into multiplayer-delayed-playlist-load-broken
This commit is contained in:
commit
42ad726154
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -77,10 +77,6 @@ jobs:
|
|||||||
run: msbuild osu.Android/osu.Android.csproj /restore /p:Configuration=Debug
|
run: msbuild osu.Android/osu.Android.csproj /restore /p:Configuration=Debug
|
||||||
|
|
||||||
build-only-ios:
|
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)
|
name: Build only (iOS)
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
|
@ -5,6 +5,8 @@ using System;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Models;
|
||||||
|
using Realms;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
@ -33,6 +35,39 @@ namespace osu.Game.Tests.Database
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Test to ensure that a `CreateContext` call nested inside a subscription doesn't cause any deadlocks
|
||||||
|
/// due to context fetching semaphores.
|
||||||
|
/// </summary>
|
||||||
|
[Test]
|
||||||
|
public void TestNestedContextCreationWithSubscription()
|
||||||
|
{
|
||||||
|
RunTestWithRealm((realmFactory, _) =>
|
||||||
|
{
|
||||||
|
bool callbackRan = false;
|
||||||
|
|
||||||
|
using (var context = realmFactory.CreateContext())
|
||||||
|
{
|
||||||
|
var subscription = context.All<RealmBeatmap>().SubscribeForNotifications((sender, changes, error) =>
|
||||||
|
{
|
||||||
|
using (realmFactory.CreateContext())
|
||||||
|
{
|
||||||
|
callbackRan = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Force the callback above to run.
|
||||||
|
using (realmFactory.CreateContext())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
subscription.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.IsTrue(callbackRan);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestBlockOperationsWithContention()
|
public void TestBlockOperationsWithContention()
|
||||||
{
|
{
|
||||||
|
@ -62,43 +62,6 @@ namespace osu.Game.Tests.Database
|
|||||||
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
|
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public void TestValueAccessWithOpenContext()
|
|
||||||
{
|
|
||||||
RunTestWithRealm((realmFactory, _) =>
|
|
||||||
{
|
|
||||||
ILive<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]
|
[Test]
|
||||||
public void TestScopedReadWithoutContext()
|
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<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(() =>
|
||||||
|
{
|
||||||
|
// Can't be used, without a valid context.
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
{
|
||||||
|
var __ = liveBeatmap.Value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Can't be used, even from within a valid context.
|
||||||
|
using (realmFactory.CreateContext())
|
||||||
|
{
|
||||||
|
Assert.Throws<InvalidOperationException>(() =>
|
||||||
|
{
|
||||||
|
var __ = liveBeatmap.Value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler).Wait();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestValueAccessWithoutOpenContextFails()
|
public void TestValueAccessWithoutOpenContextFails()
|
||||||
{
|
{
|
||||||
@ -215,23 +232,22 @@ namespace osu.Game.Tests.Database
|
|||||||
Assert.AreEqual(0, updateThreadContext.All<RealmBeatmap>().Count());
|
Assert.AreEqual(0, updateThreadContext.All<RealmBeatmap>().Count());
|
||||||
Assert.AreEqual(0, changesTriggered);
|
Assert.AreEqual(0, changesTriggered);
|
||||||
|
|
||||||
var resolved = liveBeatmap.Value;
|
liveBeatmap.PerformRead(resolved =>
|
||||||
|
|
||||||
// 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.
|
// retrieval causes an implicit refresh. even changes that aren't related to the retrieval are fired at this point.
|
||||||
r.Remove(resolved);
|
// ReSharper disable once AccessToDisposedClosure
|
||||||
|
Assert.AreEqual(2, updateThreadContext.All<RealmBeatmap>().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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,12 +3,14 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Difficulty;
|
using osu.Game.Rulesets.Difficulty;
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Difficulty.Skills;
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Utils;
|
||||||
|
|
||||||
namespace osu.Game.Tests.NonVisual
|
namespace osu.Game.Tests.NonVisual
|
||||||
{
|
{
|
||||||
@ -20,8 +22,10 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
{
|
{
|
||||||
var combinations = new TestLegacyDifficultyCalculator().CreateDifficultyAdjustmentModCombinations();
|
var combinations = new TestLegacyDifficultyCalculator().CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
Assert.AreEqual(1, combinations.Length);
|
assertCombinations(new[]
|
||||||
Assert.IsTrue(combinations[0] is ModNoMod);
|
{
|
||||||
|
new[] { typeof(ModNoMod) }
|
||||||
|
}, combinations);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -29,9 +33,11 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
{
|
{
|
||||||
var combinations = new TestLegacyDifficultyCalculator(new ModA()).CreateDifficultyAdjustmentModCombinations();
|
var combinations = new TestLegacyDifficultyCalculator(new ModA()).CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
Assert.AreEqual(2, combinations.Length);
|
assertCombinations(new[]
|
||||||
Assert.IsTrue(combinations[0] is ModNoMod);
|
{
|
||||||
Assert.IsTrue(combinations[1] is ModA);
|
new[] { typeof(ModNoMod) },
|
||||||
|
new[] { typeof(ModA) }
|
||||||
|
}, combinations);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -39,14 +45,13 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
{
|
{
|
||||||
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModB()).CreateDifficultyAdjustmentModCombinations();
|
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModB()).CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
Assert.AreEqual(4, combinations.Length);
|
assertCombinations(new[]
|
||||||
Assert.IsTrue(combinations[0] is ModNoMod);
|
{
|
||||||
Assert.IsTrue(combinations[1] is ModA);
|
new[] { typeof(ModNoMod) },
|
||||||
Assert.IsTrue(combinations[2] is MultiMod);
|
new[] { typeof(ModA) },
|
||||||
Assert.IsTrue(combinations[3] is ModB);
|
new[] { typeof(ModA), typeof(ModB) },
|
||||||
|
new[] { typeof(ModB) }
|
||||||
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
|
}, combinations);
|
||||||
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -54,10 +59,12 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
{
|
{
|
||||||
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModIncompatibleWithA()).CreateDifficultyAdjustmentModCombinations();
|
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModIncompatibleWithA()).CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
Assert.AreEqual(3, combinations.Length);
|
assertCombinations(new[]
|
||||||
Assert.IsTrue(combinations[0] is ModNoMod);
|
{
|
||||||
Assert.IsTrue(combinations[1] is ModA);
|
new[] { typeof(ModNoMod) },
|
||||||
Assert.IsTrue(combinations[2] is ModIncompatibleWithA);
|
new[] { typeof(ModA) },
|
||||||
|
new[] { typeof(ModIncompatibleWithA) }
|
||||||
|
}, combinations);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -65,22 +72,17 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
{
|
{
|
||||||
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModB(), new ModIncompatibleWithA(), new ModIncompatibleWithAAndB()).CreateDifficultyAdjustmentModCombinations();
|
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new ModB(), new ModIncompatibleWithA(), new ModIncompatibleWithAAndB()).CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
Assert.AreEqual(8, combinations.Length);
|
assertCombinations(new[]
|
||||||
Assert.IsTrue(combinations[0] is ModNoMod);
|
{
|
||||||
Assert.IsTrue(combinations[1] is ModA);
|
new[] { typeof(ModNoMod) },
|
||||||
Assert.IsTrue(combinations[2] is MultiMod);
|
new[] { typeof(ModA) },
|
||||||
Assert.IsTrue(combinations[3] is ModB);
|
new[] { typeof(ModA), typeof(ModB) },
|
||||||
Assert.IsTrue(combinations[4] is MultiMod);
|
new[] { typeof(ModB) },
|
||||||
Assert.IsTrue(combinations[5] is ModIncompatibleWithA);
|
new[] { typeof(ModB), typeof(ModIncompatibleWithA) },
|
||||||
Assert.IsTrue(combinations[6] is MultiMod);
|
new[] { typeof(ModIncompatibleWithA) },
|
||||||
Assert.IsTrue(combinations[7] is ModIncompatibleWithAAndB);
|
new[] { typeof(ModIncompatibleWithA), typeof(ModIncompatibleWithAAndB) },
|
||||||
|
new[] { typeof(ModIncompatibleWithAAndB) },
|
||||||
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
|
}, combinations);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -88,10 +90,12 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
{
|
{
|
||||||
var combinations = new TestLegacyDifficultyCalculator(new ModAofA(), new ModIncompatibleWithAofA()).CreateDifficultyAdjustmentModCombinations();
|
var combinations = new TestLegacyDifficultyCalculator(new ModAofA(), new ModIncompatibleWithAofA()).CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
Assert.AreEqual(3, combinations.Length);
|
assertCombinations(new[]
|
||||||
Assert.IsTrue(combinations[0] is ModNoMod);
|
{
|
||||||
Assert.IsTrue(combinations[1] is ModAofA);
|
new[] { typeof(ModNoMod) },
|
||||||
Assert.IsTrue(combinations[2] is ModIncompatibleWithAofA);
|
new[] { typeof(ModAofA) },
|
||||||
|
new[] { typeof(ModIncompatibleWithAofA) }
|
||||||
|
}, combinations);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -99,17 +103,13 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
{
|
{
|
||||||
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModC())).CreateDifficultyAdjustmentModCombinations();
|
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModC())).CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
Assert.AreEqual(4, combinations.Length);
|
assertCombinations(new[]
|
||||||
Assert.IsTrue(combinations[0] is ModNoMod);
|
{
|
||||||
Assert.IsTrue(combinations[1] is ModA);
|
new[] { typeof(ModNoMod) },
|
||||||
Assert.IsTrue(combinations[2] is MultiMod);
|
new[] { typeof(ModA) },
|
||||||
Assert.IsTrue(combinations[3] is MultiMod);
|
new[] { typeof(ModA), typeof(ModB), typeof(ModC) },
|
||||||
|
new[] { typeof(ModB), typeof(ModC) }
|
||||||
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
|
}, combinations);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -117,13 +117,12 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
{
|
{
|
||||||
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModIncompatibleWithA())).CreateDifficultyAdjustmentModCombinations();
|
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModIncompatibleWithA())).CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
Assert.AreEqual(3, combinations.Length);
|
assertCombinations(new[]
|
||||||
Assert.IsTrue(combinations[0] is ModNoMod);
|
{
|
||||||
Assert.IsTrue(combinations[1] is ModA);
|
new[] { typeof(ModNoMod) },
|
||||||
Assert.IsTrue(combinations[2] is MultiMod);
|
new[] { typeof(ModA) },
|
||||||
|
new[] { typeof(ModB), typeof(ModIncompatibleWithA) }
|
||||||
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModB);
|
}, combinations);
|
||||||
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModIncompatibleWithA);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -131,13 +130,28 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
{
|
{
|
||||||
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModA(), new ModB())).CreateDifficultyAdjustmentModCombinations();
|
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModA(), new ModB())).CreateDifficultyAdjustmentModCombinations();
|
||||||
|
|
||||||
Assert.AreEqual(3, combinations.Length);
|
assertCombinations(new[]
|
||||||
Assert.IsTrue(combinations[0] is ModNoMod);
|
{
|
||||||
Assert.IsTrue(combinations[1] is ModA);
|
new[] { typeof(ModNoMod) },
|
||||||
Assert.IsTrue(combinations[2] is MultiMod);
|
new[] { typeof(ModA) },
|
||||||
|
new[] { typeof(ModA), typeof(ModB) }
|
||||||
|
}, combinations);
|
||||||
|
}
|
||||||
|
|
||||||
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
|
private void assertCombinations(Type[][] expectedCombinations, Mod[] actualCombinations)
|
||||||
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
|
{
|
||||||
|
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
|
private class ModA : Mod
|
||||||
|
@ -144,7 +144,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
AddStep("set mods", () => SelectedMods.Value = new[] { new TaikoModDoubleTime() });
|
AddStep("set mods", () => SelectedMods.Value = new[] { new TaikoModDoubleTime() });
|
||||||
|
|
||||||
AddStep("confirm selection", () => songSelect.FinaliseSelection());
|
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("beatmap not changed", () => Beatmap.Value.BeatmapInfo.Equals(selectedBeatmap));
|
||||||
AddAssert("ruleset not changed", () => Ruleset.Value.Equals(new TaikoRuleset().RulesetInfo));
|
AddAssert("ruleset not changed", () => Ruleset.Value.Equals(new TaikoRuleset().RulesetInfo));
|
||||||
|
@ -11,6 +11,7 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Online.Multiplayer;
|
||||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
||||||
using osu.Game.Online.Rooms;
|
using osu.Game.Online.Rooms;
|
||||||
using osu.Game.Rulesets;
|
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);
|
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]
|
[Test]
|
||||||
public void TestChangeTypeViaMatchSettings()
|
public void TestChangeTypeViaMatchSettings()
|
||||||
{
|
{
|
||||||
@ -152,6 +180,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
|
AddUntilStep("wait for room open", () => this.ChildrenOfType<MultiplayerMatchSubScreen>().FirstOrDefault()?.IsLoaded == true);
|
||||||
AddWaitStep("wait for transition", 2);
|
AddWaitStep("wait for transition", 2);
|
||||||
|
|
||||||
|
AddUntilStep("create room button enabled", () => this.ChildrenOfType<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>().Single().Enabled.Value);
|
||||||
AddStep("create room", () =>
|
AddStep("create room", () =>
|
||||||
{
|
{
|
||||||
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>().Single());
|
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>().Single());
|
||||||
|
@ -85,7 +85,7 @@ namespace osu.Game.Database
|
|||||||
// Grab at most 50 unique beatmap IDs from the queue.
|
// Grab at most 50 unique beatmap IDs from the queue.
|
||||||
lock (taskAssignmentLock)
|
lock (taskAssignmentLock)
|
||||||
{
|
{
|
||||||
while (pendingBeatmapTasks.Count > 0 && beatmapTasks.Count < 1)
|
while (pendingBeatmapTasks.Count > 0 && beatmapTasks.Count < 50)
|
||||||
{
|
{
|
||||||
(int id, TaskCompletionSource<APIBeatmap> task) next = pendingBeatmapTasks.Dequeue();
|
(int id, TaskCompletionSource<APIBeatmap> task) next = pendingBeatmapTasks.Dequeue();
|
||||||
|
|
||||||
@ -102,8 +102,11 @@ namespace osu.Game.Database
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (beatmapTasks.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
// Query the beatmaps.
|
// 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.
|
// rather than queueing, we maintain our own single-threaded request stream.
|
||||||
// todo: we probably want retry logic here.
|
// todo: we probably want retry logic here.
|
||||||
@ -117,16 +120,19 @@ namespace osu.Game.Database
|
|||||||
createNewTask();
|
createNewTask();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<APIBeatmap> foundBeatmaps = new List<APIBeatmap> { request.Response };
|
List<APIBeatmap> 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)
|
if (beatmapTasks.TryGetValue(beatmap.OnlineID, out var tasks))
|
||||||
task.SetResult(beatmap);
|
{
|
||||||
|
foreach (var task in tasks)
|
||||||
|
task.SetResult(beatmap);
|
||||||
|
|
||||||
beatmapTasks.Remove(beatmap.OnlineID);
|
beatmapTasks.Remove(beatmap.OnlineID);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -38,10 +38,10 @@ namespace osu.Game.Database
|
|||||||
bool IsManaged { get; }
|
bool IsManaged { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolve the value of this instance on the current thread's context.
|
/// Resolve the value of this instance on the update thread.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// After resolving the data should not be passed between threads.
|
/// After resolving, the data should not be passed between threads.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
T Value { get; }
|
T Value { get; }
|
||||||
}
|
}
|
||||||
|
@ -52,6 +52,8 @@ namespace osu.Game.Database
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly SemaphoreSlim contextCreationLock = new SemaphoreSlim(1);
|
private readonly SemaphoreSlim contextCreationLock = new SemaphoreSlim(1);
|
||||||
|
|
||||||
|
private readonly ThreadLocal<bool> currentThreadCanCreateContexts = new ThreadLocal<bool>();
|
||||||
|
|
||||||
private static readonly GlobalStatistic<int> refreshes = GlobalStatistics.Get<int>(@"Realm", @"Dirty Refreshes");
|
private static readonly GlobalStatistic<int> refreshes = GlobalStatistics.Get<int>(@"Realm", @"Dirty Refreshes");
|
||||||
private static readonly GlobalStatistic<int> contexts_created = GlobalStatistics.Get<int>(@"Realm", @"Contexts (Created)");
|
private static readonly GlobalStatistic<int> contexts_created = GlobalStatistics.Get<int>(@"Realm", @"Contexts (Created)");
|
||||||
|
|
||||||
@ -151,9 +153,22 @@ namespace osu.Game.Database
|
|||||||
if (isDisposed)
|
if (isDisposed)
|
||||||
throw new ObjectDisposedException(nameof(RealmContextFactory));
|
throw new ObjectDisposedException(nameof(RealmContextFactory));
|
||||||
|
|
||||||
|
bool tookSemaphoreLock = false;
|
||||||
|
|
||||||
try
|
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++;
|
contexts_created.Value++;
|
||||||
|
|
||||||
@ -161,7 +176,11 @@ namespace osu.Game.Database
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
contextCreationLock.Release();
|
if (tookSemaphoreLock)
|
||||||
|
{
|
||||||
|
contextCreationLock.Release();
|
||||||
|
currentThreadCanCreateContexts.Value = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +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;
|
using System;
|
||||||
using System.Threading;
|
using osu.Framework.Development;
|
||||||
using Realms;
|
using Realms;
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
@ -19,9 +19,6 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
public bool IsManaged => data.IsManaged;
|
public bool IsManaged => data.IsManaged;
|
||||||
|
|
||||||
private readonly SynchronizationContext? fetchedContext;
|
|
||||||
private readonly int fetchedThreadId;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The original live data used to create this instance.
|
/// The original live data used to create this instance.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -35,12 +32,6 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
this.data = data;
|
this.data = data;
|
||||||
|
|
||||||
if (data.IsManaged)
|
|
||||||
{
|
|
||||||
fetchedContext = SynchronizationContext.Current;
|
|
||||||
fetchedThreadId = Thread.CurrentThread.ManagedThreadId;
|
|
||||||
}
|
|
||||||
|
|
||||||
ID = data.ID;
|
ID = data.ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +41,7 @@ namespace osu.Game.Database
|
|||||||
/// <param name="perform">The action to perform.</param>
|
/// <param name="perform">The action to perform.</param>
|
||||||
public void PerformRead(Action<T> perform)
|
public void PerformRead(Action<T> perform)
|
||||||
{
|
{
|
||||||
if (originalDataValid)
|
if (!IsManaged)
|
||||||
{
|
{
|
||||||
perform(data);
|
perform(data);
|
||||||
return;
|
return;
|
||||||
@ -69,7 +60,7 @@ namespace osu.Game.Database
|
|||||||
if (typeof(RealmObjectBase).IsAssignableFrom(typeof(TReturn)))
|
if (typeof(RealmObjectBase).IsAssignableFrom(typeof(TReturn)))
|
||||||
throw new InvalidOperationException($"Realm live objects should not exit the scope of {nameof(PerformRead)}.");
|
throw new InvalidOperationException($"Realm live objects should not exit the scope of {nameof(PerformRead)}.");
|
||||||
|
|
||||||
if (originalDataValid)
|
if (!IsManaged)
|
||||||
return perform(data);
|
return perform(data);
|
||||||
|
|
||||||
using (var realm = Realm.GetInstance(data.Realm.Config))
|
using (var realm = Realm.GetInstance(data.Realm.Config))
|
||||||
@ -97,27 +88,20 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
if (originalDataValid)
|
if (!IsManaged)
|
||||||
return data;
|
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))
|
// When using Value, we rely on garbage collection for the realm instance used to retrieve the instance.
|
||||||
retrieved = realm.Find<T>(ID);
|
// 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)
|
return realm.Find<T>(ID);
|
||||||
throw new InvalidOperationException("Attempted to access value without an open context");
|
|
||||||
|
|
||||||
return retrieved;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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<T>? other) => ID == other?.ID;
|
public bool Equals(ILive<T>? other) => ID == other?.ID;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -100,6 +100,9 @@ namespace osu.Game.Database
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userTasks.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
// Query the users.
|
// Query the users.
|
||||||
var request = new GetUsersRequest(userTasks.Keys.ToArray());
|
var request = new GetUsersRequest(userTasks.Keys.ToArray());
|
||||||
|
|
||||||
|
@ -38,7 +38,12 @@ namespace osu.Game.Online.API
|
|||||||
protected override void PostProcess()
|
protected override void PostProcess()
|
||||||
{
|
{
|
||||||
base.PostProcess();
|
base.PostProcess();
|
||||||
Response = ((OsuJsonWebRequest<T>)WebRequest)?.ResponseObject;
|
|
||||||
|
if (WebRequest != null)
|
||||||
|
{
|
||||||
|
Response = ((OsuJsonWebRequest<T>)WebRequest).ResponseObject;
|
||||||
|
Logger.Log($"{GetType()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void TriggerSuccess(T result)
|
internal void TriggerSuccess(T result)
|
||||||
|
24
osu.Game/Online/API/Requests/GetBeatmapsRequest.cs
Normal file
24
osu.Game/Online/API/Requests/GetBeatmapsRequest.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// 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;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.API.Requests
|
||||||
|
{
|
||||||
|
public class GetBeatmapsRequest : APIRequest<GetBeatmapsResponse>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
15
osu.Game/Online/API/Requests/GetBeatmapsResponse.cs
Normal file
15
osu.Game/Online/API/Requests/GetBeatmapsResponse.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
|
||||||
|
namespace osu.Game.Online.API.Requests
|
||||||
|
{
|
||||||
|
public class GetBeatmapsResponse : ResponseWithCursor
|
||||||
|
{
|
||||||
|
[JsonProperty("beatmaps")]
|
||||||
|
public List<APIBeatmap> Beatmaps;
|
||||||
|
}
|
||||||
|
}
|
@ -702,6 +702,7 @@ namespace osu.Game.Online.Multiplayer
|
|||||||
Room.Settings = settings;
|
Room.Settings = settings;
|
||||||
APIRoom.Name.Value = Room.Settings.Name;
|
APIRoom.Name.Value = Room.Settings.Name;
|
||||||
APIRoom.Password.Value = Room.Settings.Password;
|
APIRoom.Password.Value = Room.Settings.Password;
|
||||||
|
APIRoom.Type.Value = Room.Settings.MatchType;
|
||||||
APIRoom.QueueMode.Value = Room.Settings.QueueMode;
|
APIRoom.QueueMode.Value = Room.Settings.QueueMode;
|
||||||
RoomUpdated?.Invoke();
|
RoomUpdated?.Invoke();
|
||||||
|
|
||||||
|
@ -95,13 +95,13 @@ namespace osu.Game.Overlays
|
|||||||
/// Displays the provided <see cref="Toast"/> temporarily.
|
/// Displays the provided <see cref="Toast"/> temporarily.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="toast"></param>
|
/// <param name="toast"></param>
|
||||||
public void Display(Toast toast)
|
public void Display(Toast toast) => Schedule(() =>
|
||||||
{
|
{
|
||||||
box.Child = toast;
|
box.Child = toast;
|
||||||
DisplayTemporarily(box);
|
DisplayTemporarily(box);
|
||||||
}
|
});
|
||||||
|
|
||||||
private void displayTrackedSettingChange(SettingDescription description) => Schedule(() => Display(new TrackedSettingToast(description)));
|
private void displayTrackedSettingChange(SettingDescription description) => Display(new TrackedSettingToast(description));
|
||||||
|
|
||||||
private TransformSequence<Drawable> fadeIn;
|
private TransformSequence<Drawable> fadeIn;
|
||||||
private ScheduledDelegate fadeOut;
|
private ScheduledDelegate fadeOut;
|
||||||
|
Loading…
Reference in New Issue
Block a user