1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 18:47:27 +08:00

Merge branch 'master' into fix-gameplay-samepl-trigger-source-rewind

This commit is contained in:
Bartłomiej Dach 2023-07-08 13:33:04 +02:00
commit 832c1c0009
No known key found for this signature in database
77 changed files with 290 additions and 189 deletions

View File

@ -11,7 +11,7 @@
<AndroidManifestMerger>manifestmerger.jar</AndroidManifestMerger>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.625.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.707.0" />
</ItemGroup>
<ItemGroup>
<AndroidManifestOverlay Include="$(MSBuildThisFileDirectory)osu.Android\Properties\AndroidManifestOverlay.xml" />

View File

@ -187,7 +187,7 @@ namespace osu.Desktop
return edit.BeatmapInfo.ToString() ?? string.Empty;
case UserActivity.WatchingReplay watching:
return watching.BeatmapInfo.ToString();
return watching.BeatmapInfo?.ToString() ?? string.Empty;
case UserActivity.InLobby lobby:
return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value;

View File

@ -147,14 +147,12 @@ namespace osu.Desktop
{
base.SetHost(host);
var desktopWindow = (SDL2DesktopWindow)host.Window;
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
if (iconStream != null)
desktopWindow.SetIconFromStream(iconStream);
host.Window.SetIconFromStream(iconStream);
desktopWindow.CursorState |= CursorState.Hidden;
desktopWindow.Title = Name;
host.Window.CursorState |= CursorState.Hidden;
host.Window.Title = Name;
}
protected override BatteryInfo CreateBatteryInfo() => new SDL2BatteryInfo();

View File

@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
effectiveMissCount = Math.Max(1.0, 1000.0 / totalSuccessfulHits) * countMiss;
// TODO: The detection of rulesets is temporary until the leftover old skills have been reworked.
bool isConvert = score.BeatmapInfo.Ruleset.OnlineID != 1;
bool isConvert = score.BeatmapInfo!.Ruleset.OnlineID != 1;
double multiplier = 1.13;

View File

@ -1,6 +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.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -8,6 +9,7 @@ using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
@ -26,11 +28,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
private Bindable<int> currentCombo { get; } = new BindableInt();
private int animationFrame;
private double beatLength;
// required for editor blueprints (not sure why these circle pieces are zero size).
public override Quad ScreenSpaceDrawQuad => backgroundLayer.ScreenSpaceDrawQuad;
private TimingControlPoint timingPoint = TimingControlPoint.DEFAULT;
public LegacyCirclePiece()
{
RelativeSizeAxes = Axes.Both;
@ -39,11 +42,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
[Resolved(canBeNull: true)]
private GameplayState? gameplayState { get; set; }
[Resolved(canBeNull: true)]
private IBeatSyncProvider? beatSyncProvider { get; set; }
[BackgroundDependencyLoader]
private void load(ISkinSource skin, DrawableHitObject drawableHitObject)
private void load(ISkinSource skin, DrawableHitObject drawableHitObject, IBeatSyncProvider? beatSyncProvider)
{
Drawable? getDrawableFor(string lookup)
{
@ -64,6 +64,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
if (foregroundLayer != null)
AddInternal(foregroundLayer);
drawableHitObject.StartTimeBindable.BindValueChanged(startTime =>
{
timingPoint = beatSyncProvider?.ControlPoints?.TimingPointAt(startTime.NewValue) ?? TimingControlPoint.DEFAULT;
}, true);
// Animations in taiko skins are used in a custom way (>150 combo and animating in time with beat).
// For now just stop at first frame for sanity.
foreach (var c in InternalChildren)
@ -115,14 +120,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
return;
}
if (beatSyncProvider?.ControlPoints != null)
{
beatLength = beatSyncProvider.ControlPoints.TimingPointAt(Time.Current).BeatLength;
animationFrame = Time.Current % ((beatLength * 2) / multiplier) >= beatLength / multiplier ? 0 : 1;
animatableForegroundLayer.GotoFrame(animationFrame);
}
animationFrame = Math.Abs(Time.Current - timingPoint.Time) % ((timingPoint.BeatLength * 2) / multiplier) >= timingPoint.BeatLength / multiplier ? 0 : 1;
animatableForegroundLayer.GotoFrame(animationFrame);
}
private Color4 accentColour;

View File

@ -64,7 +64,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestCachedRetrievalWithFiles() => AddStep("run test", () =>
{
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID).Detach());
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID)!.Detach());
Assert.That(beatmap.BeatmapSet?.Files, Has.Count.GreaterThan(0));
@ -90,7 +90,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestForcedRefetchRetrievalWithFiles() => AddStep("run test", () =>
{
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID).Detach());
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID)!.Detach());
Assert.That(beatmap.BeatmapSet?.Files, Has.Count.GreaterThan(0));
@ -102,7 +102,7 @@ namespace osu.Game.Tests.Beatmaps
[Test]
public void TestSavePreservesCollections() => AddStep("run test", () =>
{
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID).Detach());
var beatmap = Realm.Run(r => r.Find<BeatmapInfo>(importedSet.Beatmaps.First().ID)!.Detach());
var working = beatmaps.GetWorkingBeatmap(beatmap);

View File

@ -42,7 +42,7 @@ namespace osu.Game.Tests.Database
{
return Realm.Run(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
});
});
@ -51,7 +51,7 @@ namespace osu.Game.Tests.Database
{
Realm.Write(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
foreach (var b in beatmapSetInfo.Beatmaps)
b.StarRating = -1;
});
@ -66,7 +66,7 @@ namespace osu.Game.Tests.Database
{
return Realm.Run(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
});
});
@ -79,7 +79,7 @@ namespace osu.Game.Tests.Database
{
return Realm.Run(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
});
});
@ -90,7 +90,7 @@ namespace osu.Game.Tests.Database
{
Realm.Write(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
foreach (var b in beatmapSetInfo.Beatmaps)
b.StarRating = -1;
});
@ -107,7 +107,7 @@ namespace osu.Game.Tests.Database
{
return Realm.Run(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
return beatmapSetInfo.Beatmaps.All(b => b.StarRating == -1);
});
});
@ -118,7 +118,7 @@ namespace osu.Game.Tests.Database
{
return Realm.Run(r =>
{
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID);
var beatmapSetInfo = r.Find<BeatmapSetInfo>(importedSet.ID)!;
return beatmapSetInfo.Beatmaps.All(b => b.StarRating > 0);
});
});

View File

@ -417,6 +417,60 @@ namespace osu.Game.Tests.Database
});
}
[Test]
public void TestImport_Modify_Revert()
{
RunTestWithRealmAsync(async (realm, storage) =>
{
var importer = new BeatmapImporter(storage, realm);
using var store = new RealmRulesetStore(realm, storage);
var imported = await LoadOszIntoStore(importer, realm.Realm);
await createScoreForBeatmap(realm.Realm, imported.Beatmaps.First());
var score = realm.Run(r => r.All<ScoreInfo>().Single());
string originalHash = imported.Beatmaps.First().Hash;
const string modified_hash = "new_hash";
Assert.That(imported.Beatmaps.First().Scores.Single(), Is.EqualTo(score));
Assert.That(score.BeatmapHash, Is.EqualTo(originalHash));
Assert.That(score.BeatmapInfo, Is.EqualTo(imported.Beatmaps.First()));
// imitate making local changes via editor
// ReSharper disable once MethodHasAsyncOverload
realm.Write(r =>
{
BeatmapInfo beatmap = imported.Beatmaps.First();
beatmap.Hash = modified_hash;
beatmap.ResetOnlineInfo();
beatmap.UpdateLocalScores(r);
});
Assert.That(!imported.Beatmaps.First().Scores.Any());
Assert.That(score.BeatmapInfo, Is.Null);
Assert.That(score.BeatmapHash, Is.EqualTo(originalHash));
// imitate reverting the local changes made above
// ReSharper disable once MethodHasAsyncOverload
realm.Write(r =>
{
BeatmapInfo beatmap = imported.Beatmaps.First();
beatmap.Hash = originalHash;
beatmap.ResetOnlineInfo();
beatmap.UpdateLocalScores(r);
});
Assert.That(imported.Beatmaps.First().Scores.Single(), Is.EqualTo(score));
Assert.That(score.BeatmapHash, Is.EqualTo(originalHash));
Assert.That(score.BeatmapInfo, Is.EqualTo(imported.Beatmaps.First()));
});
}
[Test]
public void TestImport_ThenModifyMapWithScore_ThenImport()
{
@ -431,19 +485,19 @@ namespace osu.Game.Tests.Database
await createScoreForBeatmap(realm.Realm, imported.Beatmaps.First());
Assert.That(imported.Beatmaps.First().Scores.Any());
// imitate making local changes via editor
// ReSharper disable once MethodHasAsyncOverload
realm.Write(_ =>
realm.Write(r =>
{
BeatmapInfo beatmap = imported.Beatmaps.First();
beatmap.Hash = "new_hash";
beatmap.ResetOnlineInfo();
beatmap.UpdateLocalScores(r);
});
// for now, making changes to a beatmap doesn't remove the backlink from the score to the beatmap.
// the logic of ensuring that scores match the beatmap is upheld via comparing the hash in usages (see: https://github.com/ppy/osu/pull/22539).
// TODO: revisit when fixing https://github.com/ppy/osu/issues/24069.
Assert.That(imported.Beatmaps.First().Scores.Any());
Assert.That(!imported.Beatmaps.First().Scores.Any());
var importedSecondTime = await importer.Import(new ImportTask(temp));
@ -461,6 +515,7 @@ namespace osu.Game.Tests.Database
Assert.That(importedFirstTimeBeatmap.Hash != importedSecondTimeBeatmap.Hash);
Assert.That(!importedFirstTimeBeatmap.Scores.Any());
Assert.That(importedSecondTimeBeatmap.Scores.Count() == 1);
Assert.That(importedSecondTimeBeatmap.Scores.Single().BeatmapInfo, Is.EqualTo(importedSecondTimeBeatmap));
});
}

View File

@ -323,7 +323,7 @@ namespace osu.Game.Tests.Database
var beatmapInfo = s.Beatmaps.First(b => b.File?.Filename != removedFilename);
scoreTargetBeatmapHash = beatmapInfo.Hash;
s.Realm.Add(new ScoreInfo(beatmapInfo, s.Realm.All<RulesetInfo>().First(), new RealmUser()));
s.Realm!.Add(new ScoreInfo(beatmapInfo, s.Realm.All<RulesetInfo>().First(), new RealmUser()));
});
realm.Run(r => r.Refresh());
@ -372,7 +372,7 @@ namespace osu.Game.Tests.Database
scoreTargetBeatmapHash = beatmapInfo.Hash;
s.Realm.Add(new ScoreInfo(beatmapInfo, s.Realm.All<RulesetInfo>().First(), new RealmUser()));
s.Realm!.Add(new ScoreInfo(beatmapInfo, s.Realm.All<RulesetInfo>().First(), new RealmUser()));
});
// locally modify beatmap
@ -383,14 +383,14 @@ namespace osu.Game.Tests.Database
beatmapInfo.Hash = new_beatmap_hash;
beatmapInfo.ResetOnlineInfo();
beatmapInfo.UpdateLocalScores(s.Realm!);
});
realm.Run(r => r.Refresh());
// for now, making changes to a beatmap doesn't remove the backlink from the score to the beatmap.
// the logic of ensuring that scores match the beatmap is upheld via comparing the hash in usages (https://github.com/ppy/osu/pull/22539).
// TODO: revisit when fixing https://github.com/ppy/osu/issues/24069.
// making changes to a beatmap doesn't remove the score from realm, but should disassociate the beatmap.
checkCount<ScoreInfo>(realm, 1);
Assert.That(realm.Run(r => r.All<ScoreInfo>().First().BeatmapInfo), Is.Null);
// reimport the original beatmap before local modifications
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOnlineCopy), importBeforeUpdate.Value);
@ -435,7 +435,7 @@ namespace osu.Game.Tests.Database
{
var beatmapInfo = s.Beatmaps.Last();
scoreTargetFilename = beatmapInfo.File?.Filename;
s.Realm.Add(new ScoreInfo(beatmapInfo, s.Realm.All<RulesetInfo>().First(), new RealmUser()));
s.Realm!.Add(new ScoreInfo(beatmapInfo, s.Realm.All<RulesetInfo>().First(), new RealmUser()));
});
realm.Run(r => r.Refresh());
@ -528,7 +528,7 @@ namespace osu.Game.Tests.Database
importBeforeUpdate.PerformWrite(s =>
{
var beatmapCollection = s.Realm.Add(new BeatmapCollection("test collection"));
var beatmapCollection = s.Realm!.Add(new BeatmapCollection("test collection"));
beatmapsToAddToCollection = s.Beatmaps.Count - (allOriginalBeatmapsInCollection ? 0 : 1);
for (int i = 0; i < beatmapsToAddToCollection; i++)
@ -543,7 +543,7 @@ namespace osu.Game.Tests.Database
importAfterUpdate.PerformRead(updated =>
{
updated.Realm.Refresh();
updated.Realm!.Refresh();
string[] hashes = updated.Realm.All<BeatmapCollection>().Single().BeatmapMD5Hashes.ToArray();
@ -593,7 +593,7 @@ namespace osu.Game.Tests.Database
importBeforeUpdate.PerformWrite(s =>
{
var beatmapCollection = s.Realm.Add(new BeatmapCollection("test collection"));
var beatmapCollection = s.Realm!.Add(new BeatmapCollection("test collection"));
originalHash = s.Beatmaps.Single(b => b.DifficultyName == "Hard").MD5Hash;
beatmapCollection.BeatmapMD5Hashes.Add(originalHash);
@ -607,7 +607,7 @@ namespace osu.Game.Tests.Database
importAfterUpdate.PerformRead(updated =>
{
updated.Realm.Refresh();
updated.Realm!.Refresh();
string[] hashes = updated.Realm.All<BeatmapCollection>().Single().BeatmapMD5Hashes.ToArray();
string updatedHash = updated.Beatmaps.Single(b => b.DifficultyName == "Hard").MD5Hash;

View File

@ -128,7 +128,7 @@ namespace osu.Game.Tests.Database
realm.RegisterCustomSubscription(r =>
{
var subscription = r.All<BeatmapInfo>().QueryAsyncWithNotifications((_, _, _) =>
var subscription = r.All<BeatmapInfo>().QueryAsyncWithNotifications((_, _) =>
{
realm.Run(_ =>
{

View File

@ -355,7 +355,7 @@ namespace osu.Game.Tests.Database
return null;
});
void gotChange(IRealmCollection<BeatmapInfo> sender, ChangeSet changes, Exception error)
void gotChange(IRealmCollection<BeatmapInfo> sender, ChangeSet? changes)
{
changesTriggered++;
}

View File

@ -1,7 +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.Collections.Generic;
using System.Linq;
using System.Threading;
@ -54,7 +53,7 @@ namespace osu.Game.Tests.Database
registration.Dispose();
});
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error)
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes)
{
lastChanges = changes;
@ -92,7 +91,7 @@ namespace osu.Game.Tests.Database
registration.Dispose();
});
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error) => lastChanges = changes;
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes) => lastChanges = changes;
}
[Test]
@ -185,7 +184,7 @@ namespace osu.Game.Tests.Database
}
});
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error)
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes)
{
if (changes == null)
resolvedItems = sender;

View File

@ -76,12 +76,12 @@ namespace osu.Game.Tests.Database
Available = true,
}));
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName).Available), Is.True);
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.True);
// Availability is updated on construction of a RealmRulesetStore
var _ = new RealmRulesetStore(realm, storage);
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName).Available), Is.False);
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.False);
});
}
@ -101,18 +101,18 @@ namespace osu.Game.Tests.Database
Available = true,
}));
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName).Available), Is.True);
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.True);
// Availability is updated on construction of a RealmRulesetStore
var _ = new RealmRulesetStore(realm, storage);
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName).Available), Is.False);
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.False);
// Simulate the ruleset getting updated
LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION;
var __ = new RealmRulesetStore(realm, storage);
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName).Available), Is.True);
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.True);
});
}

View File

@ -104,7 +104,7 @@ namespace osu.Game.Tests.Database
realm.Run(innerRealm =>
{
var binding = innerRealm.ResolveReference(tsr);
var binding = innerRealm.ResolveReference(tsr)!;
innerRealm.Write(() => binding.KeyCombination = new KeyCombination(InputKey.BackSpace));
});

View File

@ -87,10 +87,10 @@ namespace osu.Game.Tests.Models
var mock = new Mock<IScoreInfo>();
mock.Setup(m => m.User).Returns(new APIUser { Username = "user" }); // TODO: temporary.
mock.Setup(m => m.Beatmap.Metadata.Artist).Returns("artist");
mock.Setup(m => m.Beatmap.Metadata.Title).Returns("title");
mock.Setup(m => m.Beatmap.Metadata.Author.Username).Returns("author");
mock.Setup(m => m.Beatmap.DifficultyName).Returns("difficulty");
mock.Setup(m => m.Beatmap!.Metadata.Artist).Returns("artist");
mock.Setup(m => m.Beatmap!.Metadata.Title).Returns("title");
mock.Setup(m => m.Beatmap!.Metadata.Author.Username).Returns("author");
mock.Setup(m => m.Beatmap!.DifficultyName).Returns("difficulty");
Assert.That(mock.Object.GetDisplayString(), Is.EqualTo("user playing artist - title (author) [difficulty]"));
}

View File

@ -131,7 +131,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("results screen score has matching", () => (Player.GetChildScreen() as ResultsScreen)?.Score.Mods.First(), () => Is.EqualTo(playerMods.First()));
AddUntilStep("score in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null));
AddUntilStep("databased score has correct mods", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID)).Mods.First(), () => Is.EqualTo(playerMods.First()));
AddUntilStep("databased score has correct mods", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID))!.Mods.First(), () => Is.EqualTo(playerMods.First()));
}
[Test]

View File

@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Ranking
AddStep("show excess mods score", () =>
{
var score = TestResources.CreateTestScoreInfo();
score.Mods = score.BeatmapInfo.Ruleset.CreateInstance().CreateAllMods().ToArray();
score.Mods = score.BeatmapInfo!.Ruleset.CreateInstance().CreateAllMods().ToArray();
showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), score);
});
}

View File

@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Ranking
var author = new RealmUser { Username = "mapper_name" };
var score = TestResources.CreateTestScoreInfo(createTestBeatmap(author));
score.Mods = score.BeatmapInfo.Ruleset.CreateInstance().CreateAllMods().ToArray();
score.Mods = score.BeatmapInfo!.Ruleset.CreateInstance().CreateAllMods().ToArray();
showPanel(score);
});

View File

@ -405,7 +405,7 @@ namespace osu.Game.Tests.Visual.Ranking
public UnrankedSoloResultsScreen(ScoreInfo score)
: base(score, true)
{
Score.BeatmapInfo.OnlineID = 0;
Score.BeatmapInfo!.OnlineID = 0;
Score.BeatmapInfo.Status = BeatmapOnlineStatus.Pending;
}

View File

@ -163,7 +163,7 @@ namespace osu.Game.Tests.Visual.SongSelect
InputManager.Key(Key.Enter);
});
AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen());
waitForDismissed();
AddAssert("ensure selection changed", () => selected != Beatmap.Value);
}
@ -186,7 +186,7 @@ namespace osu.Game.Tests.Visual.SongSelect
InputManager.Key(Key.Down);
});
AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen());
waitForDismissed();
AddAssert("ensure selection didn't change", () => selected == Beatmap.Value);
}
@ -215,7 +215,7 @@ namespace osu.Game.Tests.Visual.SongSelect
InputManager.Key(Key.Enter);
});
AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen());
waitForDismissed();
AddAssert("ensure selection changed", () => selected != Beatmap.Value);
}
@ -244,7 +244,7 @@ namespace osu.Game.Tests.Visual.SongSelect
InputManager.ReleaseButton(MouseButton.Left);
});
AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen());
waitForDismissed();
AddAssert("ensure selection didn't change", () => selected == Beatmap.Value);
}
@ -257,7 +257,7 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect();
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen());
waitForDismissed();
AddStep("return", () => songSelect!.MakeCurrent());
AddUntilStep("wait for current", () => songSelect!.IsCurrentScreen());
@ -275,7 +275,7 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect();
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen());
waitForDismissed();
AddStep("change convert setting", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));
@ -292,7 +292,7 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect();
AddStep("push child screen", () => Stack.Push(new TestSceneOsuScreenStack.TestScreen("test child")));
AddUntilStep("wait for not current", () => !songSelect!.IsCurrentScreen());
waitForDismissed();
AddStep("update beatmap", () =>
{
@ -1011,7 +1011,7 @@ namespace osu.Game.Tests.Visual.SongSelect
});
});
AddUntilStep("wait for results screen presented", () => !songSelect!.IsCurrentScreen());
waitForDismissed();
AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(getPresentBeatmap()));
AddAssert("check ruleset is correct for score", () => Ruleset.Value.OnlineID == 0);
@ -1040,7 +1040,7 @@ namespace osu.Game.Tests.Visual.SongSelect
songSelect!.PresentScore(TestResources.CreateTestScoreInfo(getPresentBeatmap()));
});
AddUntilStep("wait for results screen presented", () => !songSelect!.IsCurrentScreen());
waitForDismissed();
AddAssert("check beatmap is correct for score", () => Beatmap.Value.BeatmapInfo.MatchesOnlineID(getPresentBeatmap()));
AddAssert("check ruleset is correct for score", () => Ruleset.Value.OnlineID == 0);
@ -1161,6 +1161,8 @@ namespace osu.Game.Tests.Visual.SongSelect
rulesets.Dispose();
}
private void waitForDismissed() => AddUntilStep("wait for not current", () => !songSelect.AsNonNull().IsCurrentScreen());
private partial class TestSongSelect : PlaySongSelect
{
public Action? StartRequested;

View File

@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.UserInterface
var testPresets = createTestPresets();
foreach (var preset in testPresets)
preset.Ruleset = realm.Find<RulesetInfo>(preset.Ruleset.ShortName);
preset.Ruleset = realm.Find<RulesetInfo>(preset.Ruleset.ShortName)!;
realm.Add(testPresets);
});
@ -103,7 +103,7 @@ namespace osu.Game.Tests.Visual.UserInterface
new ManiaModNightcore(),
new ManiaModHardRock()
},
Ruleset = r.Find<RulesetInfo>("mania")
Ruleset = r.Find<RulesetInfo>("mania")!
})));
AddUntilStep("2 panels visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 2);
@ -115,7 +115,7 @@ namespace osu.Game.Tests.Visual.UserInterface
new OsuModHidden(),
new OsuModHardRock()
},
Ruleset = r.Find<RulesetInfo>("osu")
Ruleset = r.Find<RulesetInfo>("osu")!
})));
AddUntilStep("2 panels visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 2);

View File

@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
Name = "AR0",
Description = "Too... many... circles...",
Ruleset = r.Find<RulesetInfo>(OsuRuleset.SHORT_NAME),
Ruleset = r.Find<RulesetInfo>(OsuRuleset.SHORT_NAME)!,
Mods = new[]
{
new OsuModDifficultyAdjust
@ -73,7 +73,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
Name = "Half Time 0.5x",
Description = "Very slow",
Ruleset = r.Find<RulesetInfo>(OsuRuleset.SHORT_NAME),
Ruleset = r.Find<RulesetInfo>(OsuRuleset.SHORT_NAME)!,
Mods = new[]
{
new OsuModHalfTime

View File

@ -102,7 +102,7 @@ namespace osu.Game
}
}
r.Find<RulesetInfo>(ruleset.ShortName).LastAppliedDifficultyVersion = currentVersion;
r.Find<RulesetInfo>(ruleset.ShortName)!.LastAppliedDifficultyVersion = currentVersion;
});
Logger.Log($"Finished resetting {countReset} beatmap sets for {ruleset.Name}");
@ -163,8 +163,12 @@ namespace osu.Game
{
foreach (var score in r.All<ScoreInfo>())
{
if (score.Statistics.Sum(kvp => kvp.Value) > 0 && score.MaximumStatistics.Sum(kvp => kvp.Value) == 0)
if (score.BeatmapInfo != null
&& score.Statistics.Sum(kvp => kvp.Value) > 0
&& score.MaximumStatistics.Sum(kvp => kvp.Value) == 0)
{
scoreIds.Add(score.ID);
}
}
});
@ -184,7 +188,7 @@ namespace osu.Game
// ReSharper disable once MethodHasAsyncOverload
realmAccess.Write(r =>
{
r.Find<ScoreInfo>(id).MaximumStatisticsJson = JsonConvert.SerializeObject(score.MaximumStatistics);
r.Find<ScoreInfo>(id)!.MaximumStatisticsJson = JsonConvert.SerializeObject(score.MaximumStatistics);
});
Logger.Log($"Populated maximum statistics for score {id}");
@ -204,7 +208,9 @@ namespace osu.Game
{
Logger.Log("Querying for scores that need total score conversion...");
HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>(r.All<ScoreInfo>().Where(s => s.TotalScoreVersion == 30000002).AsEnumerable().Select(s => s.ID)));
HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>(r.All<ScoreInfo>()
.Where(s => s.BeatmapInfo != null && s.TotalScoreVersion == 30000002)
.AsEnumerable().Select(s => s.ID)));
Logger.Log($"Found {scoreIds.Count} scores which require total score conversion.");
@ -237,7 +243,7 @@ namespace osu.Game
// ReSharper disable once MethodHasAsyncOverload
realmAccess.Write(r =>
{
ScoreInfo s = r.Find<ScoreInfo>(id);
ScoreInfo s = r.Find<ScoreInfo>(id)!;
s.TotalScore = newTotalScore;
s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION;
});

View File

@ -20,7 +20,6 @@ using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using Realms;
namespace osu.Game.Beatmaps
@ -69,7 +68,7 @@ namespace osu.Game.Beatmaps
Logger.Log($"Beatmap \"{updated}\" update completed successfully", LoggingTarget.Database);
original = realm.Find<BeatmapSetInfo>(original.ID);
original = realm!.Find<BeatmapSetInfo>(original.ID)!;
// Generally the import process will do this for us if the OnlineIDs match,
// but that isn't a guarantee (ie. if the .osu file doesn't have OnlineIDs populated).
@ -210,8 +209,7 @@ namespace osu.Game.Beatmaps
// Let's reattach any matching scores that exist in the database, based on hash.
foreach (BeatmapInfo beatmap in model.Beatmaps)
{
foreach (var score in realm.All<ScoreInfo>().Where(score => score.BeatmapHash == beatmap.Hash))
score.BeatmapInfo = beatmap;
beatmap.UpdateLocalScores(realm);
}
ProcessBeatmap?.Invoke(model, parameters.Batch ? MetadataLookupScope.LocalCacheFirst : MetadataLookupScope.OnlineFirst);

View File

@ -234,6 +234,22 @@ namespace osu.Game.Beatmaps
}
}
/// <summary>
/// Local scores are retained separate from a beatmap's lifetime, matched via <see cref="ScoreInfo.BeatmapHash"/>.
/// Therefore we need to detach / reattach scores when a beatmap is edited or imported.
/// </summary>
/// <param name="realm">A realm instance in an active write transaction.</param>
public void UpdateLocalScores(Realm realm)
{
// first disassociate any scores which are already attached and no longer valid.
foreach (var score in Scores)
score.BeatmapInfo = null;
// then attach any scores which match the new hash.
foreach (var score in realm.All<ScoreInfo>().Where(s => s.BeatmapHash == Hash))
score.BeatmapInfo = this;
}
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
IRulesetInfo IBeatmapInfo.Ruleset => Ruleset;

View File

@ -208,7 +208,7 @@ namespace osu.Game.Beatmaps
using (var transaction = r.BeginWrite())
{
if (!beatmapInfo.IsManaged)
beatmapInfo = r.Find<BeatmapInfo>(beatmapInfo.ID);
beatmapInfo = r.Find<BeatmapInfo>(beatmapInfo.ID)!;
beatmapInfo.Hidden = true;
transaction.Commit();
@ -227,7 +227,7 @@ namespace osu.Game.Beatmaps
using (var transaction = r.BeginWrite())
{
if (!beatmapInfo.IsManaged)
beatmapInfo = r.Find<BeatmapInfo>(beatmapInfo.ID);
beatmapInfo = r.Find<BeatmapInfo>(beatmapInfo.ID)!;
beatmapInfo.Hidden = false;
transaction.Commit();
@ -330,7 +330,7 @@ namespace osu.Game.Beatmaps
Realm.Write(r =>
{
if (!beatmapInfo.IsManaged)
beatmapInfo = r.Find<BeatmapInfo>(beatmapInfo.ID);
beatmapInfo = r.Find<BeatmapInfo>(beatmapInfo.ID)!;
Debug.Assert(beatmapInfo.BeatmapSet != null);
Debug.Assert(beatmapInfo.File != null);
@ -460,13 +460,16 @@ namespace osu.Game.Beatmaps
Realm.Write(r =>
{
var liveBeatmapSet = r.Find<BeatmapSetInfo>(setInfo.ID);
var liveBeatmapSet = r.Find<BeatmapSetInfo>(setInfo.ID)!;
setInfo.CopyChangesToRealm(liveBeatmapSet);
if (transferCollections)
beatmapInfo.TransferCollectionReferences(r, oldMd5Hash);
liveBeatmapSet.Beatmaps.Single(b => b.ID == beatmapInfo.ID)
.UpdateLocalScores(r);
// do not look up metadata.
// this is a locally-modified set now, so looking up metadata is busy work at best and harmful at worst.
ProcessBeatmap?.Invoke(liveBeatmapSet, MetadataLookupScope.None);

View File

@ -52,7 +52,7 @@ namespace osu.Game.Beatmaps
/// </summary>
/// <param name="beatmapSet">The managed beatmap set to update. A transaction will be opened to apply changes.</param>
/// <param name="lookupScope">The preferred scope to use for metadata lookup.</param>
public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapSet.Realm.Write(r =>
public void Process(BeatmapSetInfo beatmapSet, MetadataLookupScope lookupScope = MetadataLookupScope.LocalCacheFirst) => beatmapSet.Realm!.Write(_ =>
{
// Before we use below, we want to invalidate.
workingBeatmapCache.Invalidate(beatmapSet);

View File

@ -59,7 +59,7 @@ namespace osu.Game.Collections
Current.BindValueChanged(selectionChanged);
}
private void collectionsChanged(IRealmCollection<BeatmapCollection> collections, ChangeSet? changes, Exception error)
private void collectionsChanged(IRealmCollection<BeatmapCollection> collections, ChangeSet? changes)
{
var selectedItem = SelectedItem?.Value?.Collection;

View File

@ -41,7 +41,7 @@ namespace osu.Game.Collections
realmSubscription = realm.RegisterForNotifications(r => r.All<BeatmapCollection>().OrderBy(c => c.Name), collectionsChanged);
}
private void collectionsChanged(IRealmCollection<BeatmapCollection> collections, ChangeSet? changes, Exception error)
private void collectionsChanged(IRealmCollection<BeatmapCollection> collections, ChangeSet? changes)
{
Items.Clear();
Items.AddRange(collections.AsEnumerable().Select(c => c.ToLive(realm)));

View File

@ -197,7 +197,7 @@ namespace osu.Game.Collections
return true;
}
private void deleteCollection() => collection.PerformWrite(c => c.Realm.Remove(c));
private void deleteCollection() => collection.PerformWrite(c => c.Realm!.Remove(c));
}
}
}

View File

@ -19,8 +19,8 @@ namespace osu.Game.Database
IEnumerator IEnumerable.GetEnumerator() => emptySet.GetEnumerator();
public int Count => emptySet.Count;
public T this[int index] => emptySet[index];
public int IndexOf(object item) => emptySet.IndexOf((T)item);
public bool Contains(object item) => emptySet.Contains((T)item);
public int IndexOf(object? item) => item == null ? -1 : emptySet.IndexOf((T)item);
public bool Contains(object? item) => item != null && emptySet.Contains((T)item);
public event NotifyCollectionChangedEventHandler? CollectionChanged
{

View File

@ -34,13 +34,13 @@ namespace osu.Game.Database
}
public void DeleteFile(TModel item, RealmNamedFileUsage file) =>
performFileOperation(item, managed => DeleteFile(managed, managed.Files.First(f => f.Filename == file.Filename), managed.Realm));
performFileOperation(item, managed => DeleteFile(managed, managed.Files.First(f => f.Filename == file.Filename), managed.Realm!));
public void ReplaceFile(TModel item, RealmNamedFileUsage file, Stream contents) =>
performFileOperation(item, managed => ReplaceFile(file, contents, managed.Realm));
performFileOperation(item, managed => ReplaceFile(file, contents, managed.Realm!));
public void AddFile(TModel item, Stream contents, string filename) =>
performFileOperation(item, managed => AddFile(managed, contents, filename, managed.Realm));
performFileOperation(item, managed => AddFile(managed, contents, filename, managed.Realm!));
private void performFileOperation(TModel item, Action<TModel> operation)
{
@ -178,13 +178,14 @@ namespace osu.Game.Database
// (ie. if an async import finished very recently).
return Realm.Write(realm =>
{
if (!item.IsManaged)
item = realm.Find<TModel>(item.ID);
TModel? processableItem = item;
if (!processableItem.IsManaged)
processableItem = realm.Find<TModel>(item.ID);
if (item?.DeletePending != false)
if (processableItem?.DeletePending != false)
return false;
item.DeletePending = true;
processableItem.DeletePending = true;
return true;
});
}
@ -195,13 +196,14 @@ namespace osu.Game.Database
// (ie. if an async import finished very recently).
Realm.Write(realm =>
{
if (!item.IsManaged)
item = realm.Find<TModel>(item.ID);
TModel? processableItem = item;
if (!processableItem.IsManaged)
processableItem = realm.Find<TModel>(item.ID);
if (item?.DeletePending != true)
if (processableItem?.DeletePending != true)
return;
item.DeletePending = false;
processableItem.DeletePending = false;
});
}

View File

@ -535,7 +535,7 @@ namespace osu.Game.Database
lock (notificationsResetMap)
{
// Store an action which is used when blocking to ensure consumers don't use results of a stale changeset firing.
notificationsResetMap.Add(action, () => callback(new EmptyRealmSet<T>(), null, null));
notificationsResetMap.Add(action, () => callback(new EmptyRealmSet<T>(), null));
}
return RegisterCustomSubscription(action);
@ -755,10 +755,10 @@ namespace osu.Game.Database
for (int i = 0; i < itemCount; i++)
{
dynamic? oldItem = oldItems.ElementAt(i);
dynamic? newItem = newItems.ElementAt(i);
dynamic oldItem = oldItems.ElementAt(i);
dynamic newItem = newItems.ElementAt(i);
long? nullableOnlineID = oldItem?.OnlineID;
long? nullableOnlineID = oldItem.OnlineID;
newItem.OnlineID = (int)(nullableOnlineID ?? -1);
}
}
@ -795,7 +795,7 @@ namespace osu.Game.Database
for (int i = 0; i < metadataCount; i++)
{
dynamic? oldItem = oldMetadata.ElementAt(i);
dynamic oldItem = oldMetadata.ElementAt(i);
var newItem = newMetadata.ElementAt(i);
string username = oldItem.Author;
@ -818,7 +818,7 @@ namespace osu.Game.Database
for (int i = 0; i < newSettings.Count; i++)
{
dynamic? oldItem = oldSettings.ElementAt(i);
dynamic oldItem = oldSettings.ElementAt(i);
var newItem = newSettings.ElementAt(i);
long rulesetId = oldItem.RulesetID;
@ -843,7 +843,7 @@ namespace osu.Game.Database
for (int i = 0; i < newKeyBindings.Count; i++)
{
dynamic? oldItem = oldKeyBindings.ElementAt(i);
dynamic oldItem = oldKeyBindings.ElementAt(i);
var newItem = newKeyBindings.ElementAt(i);
if (oldItem.RulesetID == null)
@ -897,7 +897,7 @@ namespace osu.Game.Database
var scores = migration.NewRealm.All<ScoreInfo>();
foreach (var score in scores)
score.BeatmapHash = score.BeatmapInfo.Hash;
score.BeatmapHash = score.BeatmapInfo?.Hash ?? string.Empty;
break;
}

View File

@ -4,6 +4,7 @@
using System;
using System.Diagnostics;
using osu.Framework.Development;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Statistics;
using Realms;
@ -104,7 +105,7 @@ namespace osu.Game.Database
PerformRead(t =>
{
using (var transaction = t.Realm.BeginWrite())
using (var transaction = t.Realm!.BeginWrite())
{
perform(t);
transaction.Commit();
@ -133,7 +134,7 @@ namespace osu.Game.Database
{
Debug.Assert(ThreadSafety.IsUpdateThread);
if (dataIsFromUpdateThread && !data.Realm.IsClosed)
if (dataIsFromUpdateThread && !data.Realm.AsNonNull().IsClosed)
{
RealmLiveStatistics.USAGE_UPDATE_IMMEDIATE.Value++;
return;
@ -154,7 +155,7 @@ namespace osu.Game.Database
// To ensure that behaviour matches what we'd expect (the object *is* available), force
// a refresh to bring in any off-thread changes immediately.
realm.Refresh();
found = realm.Find<T>(ID);
found = realm.Find<T>(ID)!;
}
return found;

View File

@ -43,7 +43,7 @@ namespace osu.Game.Database
.ForMember(s => s.BeatmapSet, cc => cc.Ignore())
.AfterMap((s, d) =>
{
d.Ruleset = d.Realm.Find<RulesetInfo>(s.Ruleset.ShortName);
d.Ruleset = d.Realm!.Find<RulesetInfo>(s.Ruleset.ShortName)!;
copyChangesToRealm(s.Difficulty, d.Difficulty);
copyChangesToRealm(s.Metadata, d.Metadata);
});
@ -57,7 +57,7 @@ namespace osu.Game.Database
// Importantly, search all of realm for the beatmap (not just the set's beatmaps).
// It may have gotten detached, and if that's the case let's use this opportunity to fix
// things up.
var existingBeatmap = d.Realm.Find<BeatmapInfo>(beatmap.ID);
var existingBeatmap = d.Realm!.Find<BeatmapInfo>(beatmap.ID);
if (existingBeatmap != null)
{
@ -77,7 +77,7 @@ namespace osu.Game.Database
{
ID = beatmap.ID,
BeatmapSet = d,
Ruleset = d.Realm.Find<RulesetInfo>(beatmap.Ruleset.ShortName)
Ruleset = d.Realm.Find<RulesetInfo>(beatmap.Ruleset.ShortName)!
};
d.Beatmaps.Add(newBeatmap);
@ -282,12 +282,10 @@ namespace osu.Game.Database
/// <returns>
/// A subscription token. It must be kept alive for as long as you want to receive change notifications.
/// To stop receiving notifications, call <see cref="M:System.IDisposable.Dispose" />.
///
/// May be null in the case the provided collection is not managed.
/// </returns>
/// <seealso cref="M:Realms.CollectionExtensions.SubscribeForNotifications``1(System.Collections.Generic.IList{``0},Realms.NotificationCallbackDelegate{``0})" />
/// <seealso cref="M:Realms.CollectionExtensions.SubscribeForNotifications``1(System.Linq.IQueryable{``0},Realms.NotificationCallbackDelegate{``0})" />
public static IDisposable? QueryAsyncWithNotifications<T>(this IRealmCollection<T> collection, NotificationCallbackDelegate<T> callback)
public static IDisposable QueryAsyncWithNotifications<T>(this IRealmCollection<T> collection, NotificationCallbackDelegate<T> callback)
where T : RealmObjectBase
{
if (!RealmAccess.CurrentThreadSubscriptionsAllowed)

View File

@ -51,7 +51,7 @@ namespace osu.Game.Input.Bindings
protected override void LoadComplete()
{
realmSubscription = realm.RegisterForNotifications(queryRealmKeyBindings, (sender, _, _) =>
realmSubscription = realm.RegisterForNotifications(queryRealmKeyBindings, (sender, _) =>
{
// The first fire of this is a bit redundant as this is being called in base.LoadComplete,
// but this is safest in case the subscription is restored after a context recycle.

View File

@ -40,7 +40,7 @@ namespace osu.Game.Online
// Used to interact with manager classes that don't support interface types. Will eventually be replaced.
var beatmapSetInfo = new BeatmapSetInfo { OnlineID = TrackedItem.OnlineID };
realmSubscription = realm.RegisterForNotifications(r => r.All<BeatmapSetInfo>().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, _, _) =>
realmSubscription = realm.RegisterForNotifications(r => r.All<BeatmapSetInfo>().Where(s => s.OnlineID == TrackedItem.OnlineID && !s.DeletePending), (items, _) =>
{
if (items.Any())
Schedule(() => UpdateState(DownloadState.LocallyAvailable));

View File

@ -171,6 +171,8 @@ namespace osu.Game.Online.Chat
public abstract partial class HighlightMessageNotification : SimpleNotification
{
public override string PopInSampleName => "UI/notification-mention";
protected HighlightMessageNotification(Message message, Channel channel)
{
this.message = message;

View File

@ -107,7 +107,7 @@ namespace osu.Game.Online.Rooms
// handles changes to hash that didn't occur from the import process (ie. a user editing the beatmap in the editor, somehow).
realmSubscription?.Dispose();
realmSubscription = realm.RegisterForNotifications(_ => filteredBeatmaps(), (_, changes, _) =>
realmSubscription = realm.RegisterForNotifications(_ => filteredBeatmaps(), (_, changes) =>
{
if (changes == null)
return;

View File

@ -48,7 +48,7 @@ namespace osu.Game.Online
realmSubscription = realm.RegisterForNotifications(r => r.All<ScoreInfo>().Where(s =>
((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID)
|| (!string.IsNullOrEmpty(s.Hash) && s.Hash == TrackedItem.Hash))
&& !s.DeletePending), (items, _, _) =>
&& !s.DeletePending), (items, _) =>
{
if (items.Any())
Schedule(() => UpdateState(DownloadState.LocallyAvailable));

View File

@ -185,7 +185,7 @@ namespace osu.Game.Online.Spectator
IsPlaying = true;
// transfer state at point of beginning play
currentState.BeatmapID = score.ScoreInfo.BeatmapInfo.OnlineID;
currentState.BeatmapID = score.ScoreInfo.BeatmapInfo!.OnlineID;
currentState.RulesetID = score.ScoreInfo.RulesetID;
currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray();
currentState.State = SpectatedUserState.Playing;

View File

@ -289,9 +289,9 @@ namespace osu.Game
{
base.SetHost(host);
if (host.Window is SDL2Window sdlWindow)
if (host.Window != null)
{
sdlWindow.DragDrop += path =>
host.Window.DragDrop += path =>
{
// on macOS/iOS, URL associations are handled via SDL_DROPFILE events.
if (path.StartsWith(OSU_PROTOCOL, StringComparison.Ordinal))

View File

@ -163,7 +163,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
},
username,
#pragma warning disable 618
new StatisticText(score.MaxCombo, score.BeatmapInfo.MaxCombo, @"0\x"),
new StatisticText(score.MaxCombo, score.BeatmapInfo!.MaxCombo, @"0\x"),
#pragma warning restore 618
};

View File

@ -123,7 +123,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
accuracyColumn.Text = value.DisplayAccuracy;
maxComboColumn.Text = value.MaxCombo.ToLocalisableString(@"0\x");
ppColumn.Alpha = value.BeatmapInfo.Status.GrantsPerformancePoints() ? 1 : 0;
ppColumn.Alpha = value.BeatmapInfo!.Status.GrantsPerformancePoints() ? 1 : 0;
if (value.PP is double pp)
ppColumn.Text = pp.ToLocalisableString(@"N0");

View File

@ -225,7 +225,12 @@ namespace osu.Game.Overlays.Dialog
/// <summary>
/// Programmatically clicks the first button of the provided type.
/// </summary>
public void PerformAction<T>() where T : PopupDialogButton => Buttons.OfType<T>().First().TriggerClick();
public void PerformAction<T>() where T : PopupDialogButton
{
// Buttons are regularly added in BDL or LoadComplete, so let's schedule to ensure
// they are ready to be pressed.
Schedule(() => Buttons.OfType<T>().First().TriggerClick());
}
protected override bool OnKeyDown(KeyDownEvent e)
{

View File

@ -123,7 +123,7 @@ namespace osu.Game.Overlays.FirstRunSetup
beatmapSubscription?.Dispose();
}
private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error) => Schedule(() =>
private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes) => Schedule(() =>
{
currentlyLoadedBeatmaps.Text = FirstRunSetupBeatmapScreenStrings.CurrentlyLoadedBeatmaps(sender.Count);

View File

@ -102,7 +102,7 @@ namespace osu.Game.Overlays.Mods
Name = nameTextBox.Current.Value,
Description = descriptionTextBox.Current.Value,
Mods = selectedMods.Value.ToArray(),
Ruleset = r.Find<RulesetInfo>(ruleset.Value.ShortName)
Ruleset = r.Find<RulesetInfo>(ruleset.Value.ShortName)!
}));
this.HidePopover();

View File

@ -61,7 +61,7 @@ namespace osu.Game.Overlays.Mods
private Task? latestLoadTask;
internal bool ItemsLoaded => latestLoadTask?.IsCompleted == true;
private void asyncLoadPanels(IRealmCollection<ModPreset> presets, ChangeSet changes, Exception error)
private void asyncLoadPanels(IRealmCollection<ModPreset> presets, ChangeSet? changes)
{
cancellationTokenSource?.Cancel();

View File

@ -109,7 +109,7 @@ namespace osu.Game.Overlays.Music
beatmap.BindValueChanged(working => list.SelectedSet.Value = working.NewValue.BeatmapSetInfo.ToLive(realm), true);
}
private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet changes, Exception error)
private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet changes)
{
if (changes == null)
{

View File

@ -169,7 +169,7 @@ namespace osu.Game.Overlays
Logger.Log($"⚠️ {notification.Text}");
notification.Closed += notificationClosed;
notification.Closed += () => notificationClosed(notification);
if (notification is IHasCompletionTarget hasCompletionTarget)
hasCompletionTarget.CompletionTarget = Post;
@ -229,17 +229,20 @@ namespace osu.Game.Overlays
mainContent.FadeEdgeEffectTo(0, WaveContainer.DISAPPEAR_DURATION, Easing.In);
}
private void notificationClosed() => Schedule(() =>
private void notificationClosed(Notification notification) => Schedule(() =>
{
updateCounts();
// this debounce is currently shared between popin/popout sounds, which means one could potentially not play when the user is expecting it.
// popout is constant across all notification types, and should therefore be handled using playback concurrency instead, but seems broken at the moment.
playDebouncedSample("UI/overlay-pop-out");
playDebouncedSample(notification.PopOutSampleName);
});
private void playDebouncedSample(string sampleName)
{
if (string.IsNullOrEmpty(sampleName))
return;
if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME)
{
audio.Samples.Get(sampleName)?.Play();

View File

@ -50,7 +50,8 @@ namespace osu.Game.Overlays.Notifications
/// </summary>
public virtual bool DisplayOnTop => true;
public virtual string PopInSampleName => "UI/notification-pop-in";
public virtual string PopInSampleName => "UI/notification-default";
public virtual string PopOutSampleName => "UI/overlay-pop-out";
protected NotificationLight Light;

View File

@ -10,6 +10,8 @@ namespace osu.Game.Overlays.Notifications
{
public partial class ProgressCompletionNotification : SimpleNotification
{
public override string PopInSampleName => "UI/notification-done";
public ProgressCompletionNotification()
{
Icon = FontAwesome.Solid.Check;

View File

@ -4,6 +4,8 @@
using System;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@ -27,6 +29,8 @@ namespace osu.Game.Overlays.Notifications
protected override bool AllowFlingDismiss => false;
public override string PopOutSampleName => State is ProgressNotificationState.Cancelled ? base.PopOutSampleName : "";
/// <summary>
/// The function to post completion notifications back to.
/// </summary>
@ -122,6 +126,7 @@ namespace osu.Game.Overlays.Notifications
cancellationTokenSource.Cancel();
IconContent.FadeColour(ColourInfo.GradientVertical(Color4.Gray, Color4.Gray.Lighten(0.5f)), colour_fade_duration);
cancelSample?.Play();
loadingSpinner.Hide();
var icon = new SpriteIcon
@ -190,6 +195,8 @@ namespace osu.Game.Overlays.Notifications
private LoadingSpinner loadingSpinner = null!;
private Sample? cancelSample;
private readonly TextFlowContainer textDrawable;
public ProgressNotification()
@ -217,7 +224,7 @@ namespace osu.Game.Overlays.Notifications
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
private void load(OsuColour colours, AudioManager audioManager)
{
colourQueued = colours.YellowDark;
colourActive = colours.Blue;
@ -236,6 +243,8 @@ namespace osu.Game.Overlays.Notifications
Size = new Vector2(loading_spinner_size),
}
});
cancelSample = audioManager.Samples.Get(@"UI/notification-cancel");
}
public override void Close(bool runFlingAnimation)

View File

@ -7,7 +7,7 @@ namespace osu.Game.Overlays.Notifications
{
public partial class SimpleErrorNotification : SimpleNotification
{
public override string PopInSampleName => "UI/error-notification-pop-in";
public override string PopInSampleName => "UI/notification-error";
public SimpleErrorNotification()
{

View File

@ -440,7 +440,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
}
private void updateStoreFromButton(KeyButton button) =>
realm.WriteAsync(r => r.Find<RealmKeyBinding>(button.KeyBinding.ID).KeyCombinationString = button.KeyBinding.KeyCombinationString);
realm.WriteAsync(r => r.Find<RealmKeyBinding>(button.KeyBinding.ID)!.KeyCombinationString = button.KeyBinding.KeyCombinationString);
private void updateIsDefaultValue()
{

View File

@ -92,7 +92,7 @@ namespace osu.Game.Overlays.Settings.Sections
});
}
private void skinsChanged(IRealmCollection<SkinInfo> sender, ChangeSet changes, Exception error)
private void skinsChanged(IRealmCollection<SkinInfo> sender, ChangeSet changes)
{
// This can only mean that realm is recycling, else we would see the protected skins.
// Because we are using `Live<>` in this class, we don't need to worry about this scenario too much.

View File

@ -28,7 +28,7 @@ namespace osu.Game.Scoring
double? PP { get; }
IBeatmapInfo Beatmap { get; }
IBeatmapInfo? Beatmap { get; }
IRulesetInfo Ruleset { get; }

View File

@ -66,7 +66,7 @@ namespace osu.Game.Scoring.Legacy
{
sw.Write((byte)(score.ScoreInfo.Ruleset.OnlineID));
sw.Write(LATEST_VERSION);
sw.Write(score.ScoreInfo.BeatmapInfo.MD5Hash);
sw.Write(score.ScoreInfo.BeatmapInfo!.MD5Hash);
sw.Write(score.ScoreInfo.User.Username);
sw.Write(FormattableString.Invariant($"lazer-{score.ScoreInfo.User.Username}-{score.ScoreInfo.Date}").ComputeMD5Hash());
sw.Write((ushort)(score.ScoreInfo.GetCount300() ?? 0));

View File

@ -64,12 +64,14 @@ namespace osu.Game.Scoring
protected override void Populate(ScoreInfo model, ArchiveReader? archive, Realm realm, CancellationToken cancellationToken = default)
{
Debug.Assert(model.BeatmapInfo != null);
// Ensure the beatmap is not detached.
if (!model.BeatmapInfo.IsManaged)
model.BeatmapInfo = realm.Find<BeatmapInfo>(model.BeatmapInfo.ID);
model.BeatmapInfo = realm.Find<BeatmapInfo>(model.BeatmapInfo.ID)!;
if (!model.Ruleset.IsManaged)
model.Ruleset = realm.Find<RulesetInfo>(model.Ruleset.ShortName);
model.Ruleset = realm.Find<RulesetInfo>(model.Ruleset.ShortName)!;
// These properties are known to be non-null, but these final checks ensure a null hasn't come from somewhere (or the refetch has failed).
// Under no circumstance do we want these to be written to realm as null.
@ -101,10 +103,12 @@ namespace osu.Game.Scoring
/// <param name="score">The score to populate the statistics of.</param>
public void PopulateMaximumStatistics(ScoreInfo score)
{
Debug.Assert(score.BeatmapInfo != null);
if (score.MaximumStatistics.Select(kvp => kvp.Value).Sum() > 0)
return;
var beatmap = score.BeatmapInfo.Detach();
var beatmap = score.BeatmapInfo!.Detach();
var ruleset = score.Ruleset.Detach();
var rulesetInstance = ruleset.CreateInstance();

View File

@ -35,9 +35,16 @@ namespace osu.Game.Scoring
/// The <see cref="BeatmapInfo"/> this score was made against.
/// </summary>
/// <remarks>
/// When setting this, make sure to also set <see cref="BeatmapHash"/> to allow relational consistency when a beatmap is potentially changed.
/// <para>
/// This property may be <see langword="null"/> if the score was set on a beatmap (or a version of the beatmap) that is not available locally
/// e.g. due to online updates, or local modifications to the beatmap.
/// The property will only link to a <see cref="BeatmapInfo"/> if its <see cref="Beatmaps.BeatmapInfo.Hash"/> matches <see cref="BeatmapHash"/>.
/// </para>
/// <para>
/// Due to the above, whenever setting this, make sure to also set <see cref="BeatmapHash"/> to allow relational consistency when a beatmap is potentially changed.
/// </para>
/// </remarks>
public BeatmapInfo BeatmapInfo { get; set; } = null!;
public BeatmapInfo? BeatmapInfo { get; set; }
/// <summary>
/// The <see cref="osu.Game.Beatmaps.BeatmapInfo.Hash"/> at the point in time when the score was set.
@ -150,14 +157,12 @@ namespace osu.Game.Scoring
public int RankInt { get; set; }
IRulesetInfo IScoreInfo.Ruleset => Ruleset;
IBeatmapInfo IScoreInfo.Beatmap => BeatmapInfo;
IBeatmapInfo? IScoreInfo.Beatmap => BeatmapInfo;
IUser IScoreInfo.User => User;
IEnumerable<INamedFileUsage> IHasNamedFiles.Files => Files;
#region Properties required to make things work with existing usages
public Guid BeatmapInfoID => BeatmapInfo.ID;
public int UserID => RealmUser.OnlineID;
public int RulesetID => Ruleset.OnlineID;

View File

@ -13,7 +13,7 @@ namespace osu.Game.Scoring
/// <summary>
/// A user-presentable display title representing this score.
/// </summary>
public static string GetDisplayTitle(this IScoreInfo scoreInfo) => $"{scoreInfo.User.Username} playing {scoreInfo.Beatmap.GetDisplayTitle()}";
public static string GetDisplayTitle(this IScoreInfo scoreInfo) => $"{scoreInfo.User.Username} playing {scoreInfo.Beatmap?.GetDisplayTitle() ?? "unknown"}";
/// <summary>
/// Orders an array of <see cref="ScoreInfo"/>s by total score.

View File

@ -141,7 +141,7 @@ namespace osu.Game.Scoring
{
Realm.Run(r =>
{
var beatmapScores = r.Find<BeatmapInfo>(beatmap.ID).Scores.ToList();
var beatmapScores = r.Find<BeatmapInfo>(beatmap.ID)!.Scores.ToList();
Delete(beatmapScores, silent);
});
}

View File

@ -34,7 +34,7 @@ namespace osu.Game.Scoring
{
var score = lookup.ScoreInfo;
var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo, score.Ruleset, score.Mods, token).ConfigureAwait(false);
var attributes = await difficultyCache.GetDifficultyAsync(score.BeatmapInfo!, score.Ruleset, score.Mods, token).ConfigureAwait(false);
// Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value.
if (attributes?.Attributes == null)

View File

@ -68,7 +68,7 @@ namespace osu.Game.Screens.Play
{
IBeatmapInfo beatmap = score.ScoreInfo.BeatmapInfo;
Debug.Assert(beatmap.OnlineID > 0);
Debug.Assert(beatmap!.OnlineID > 0);
return new SubmitSoloScoreRequest(score.ScoreInfo, token, beatmap.OnlineID);
}

View File

@ -66,7 +66,7 @@ namespace osu.Game.Screens.Ranking.Expanded
[BackgroundDependencyLoader]
private void load(BeatmapDifficultyCache beatmapDifficultyCache)
{
var beatmap = score.BeatmapInfo;
var beatmap = score.BeatmapInfo!;
var metadata = beatmap.BeatmapSet?.Metadata ?? beatmap.Metadata;
string creator = metadata.Author.Username;

View File

@ -63,7 +63,7 @@ namespace osu.Game.Screens.Ranking
protected override APIRequest? FetchScores(Action<IEnumerable<ScoreInfo>>? scoresCallback)
{
if (Score.BeatmapInfo.OnlineID <= 0 || Score.BeatmapInfo.Status <= BeatmapOnlineStatus.Pending)
if (Score.BeatmapInfo!.OnlineID <= 0 || Score.BeatmapInfo.Status <= BeatmapOnlineStatus.Pending)
return null;
getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset);

View File

@ -223,7 +223,7 @@ namespace osu.Game.Screens.Select
subscriptionHiddenBeatmaps = realm.RegisterForNotifications(r => r.All<BeatmapInfo>().Where(b => b.Hidden), beatmapsChanged);
}
private void deletedBeatmapSetsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception? error)
private void deletedBeatmapSetsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes)
{
// If loading test beatmaps, avoid overwriting with realm subscription callbacks.
if (loadedTestBeatmaps)
@ -236,7 +236,7 @@ namespace osu.Game.Screens.Select
removeBeatmapSet(sender[i].ID);
}
private void beatmapSetsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception? error)
private void beatmapSetsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes)
{
// If loading test beatmaps, avoid overwriting with realm subscription callbacks.
if (loadedTestBeatmaps)
@ -255,7 +255,7 @@ namespace osu.Game.Screens.Select
foreach (var id in realmSets)
{
if (!root.BeatmapSetsByID.ContainsKey(id))
UpdateBeatmapSet(realm.Realm.Find<BeatmapSetInfo>(id).Detach());
UpdateBeatmapSet(realm.Realm.Find<BeatmapSetInfo>(id)!.Detach());
}
foreach (var id in root.BeatmapSetsByID.Keys)
@ -315,7 +315,7 @@ namespace osu.Game.Screens.Select
}
}
private void beatmapsChanged(IRealmCollection<BeatmapInfo> sender, ChangeSet? changes, Exception? error)
private void beatmapsChanged(IRealmCollection<BeatmapInfo> sender, ChangeSet? changes)
{
// we only care about actual changes in hidden status.
if (changes == null)

View File

@ -76,14 +76,12 @@ namespace osu.Game.Screens.Select
protected override void PopIn()
{
this.MoveToX(0, animation_duration, Easing.OutQuint);
this.RotateTo(0, animation_duration, Easing.OutQuint);
this.FadeIn(transition_duration);
}
protected override void PopOut()
{
this.MoveToX(-100, animation_duration, Easing.In);
this.RotateTo(10, animation_duration, Easing.In);
this.FadeOut(transition_duration * 2, Easing.In);
}

View File

@ -68,7 +68,7 @@ namespace osu.Game.Screens.Select.Carousel
localScoresChanged);
}, true);
void localScoresChanged(IRealmCollection<ScoreInfo> sender, ChangeSet? changes, Exception _)
void localScoresChanged(IRealmCollection<ScoreInfo> sender, ChangeSet? changes)
{
// This subscription may fire from changes to linked beatmaps, which we don't care about.
// It's currently not possible for a score to be modified after insertion, so we can safely ignore callbacks with only modifications.

View File

@ -193,7 +193,7 @@ namespace osu.Game.Screens.Select.Leaderboards
+ $" AND {nameof(ScoreInfo.DeletePending)} == false"
, beatmapInfo.ID, ruleset.Value.ShortName), localScoresChanged);
void localScoresChanged(IRealmCollection<ScoreInfo> sender, ChangeSet? changes, Exception exception)
void localScoresChanged(IRealmCollection<ScoreInfo> sender, ChangeSet? changes)
{
if (cancellationToken.IsCancellationRequested)
return;

View File

@ -4,9 +4,7 @@
using osu.Framework.Allocation;
using osu.Game.Overlays.Dialog;
using osu.Game.Scoring;
using System.Diagnostics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
namespace osu.Game.Screens.Select
{
@ -20,11 +18,8 @@ namespace osu.Game.Screens.Select
}
[BackgroundDependencyLoader]
private void load(BeatmapManager beatmapManager, ScoreManager scoreManager)
private void load(ScoreManager scoreManager)
{
BeatmapInfo? beatmapInfo = beatmapManager.QueryBeatmap(b => b.ID == score.BeatmapInfoID);
Debug.Assert(beatmapInfo != null);
BodyText = $"{score.User} ({score.DisplayAccuracy}, {score.Rank})";
Icon = FontAwesome.Regular.TrashAlt;

View File

@ -90,7 +90,7 @@ namespace osu.Game.Screens.Spectate
}));
}
private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> items, ChangeSet changes, Exception ___)
private void beatmapsChanged(IRealmCollection<BeatmapSetInfo> items, ChangeSet changes)
{
if (changes?.InsertedIndices == null) return;

View File

@ -38,7 +38,7 @@ namespace osu.Game.Skinning
realmSubscription?.Dispose();
}
private void skinChanged(IRealmCollection<T> sender, ChangeSet changes, Exception error) => invalidateCache();
private void skinChanged(IRealmCollection<T> sender, ChangeSet? changes) => invalidateCache();
protected override IEnumerable<string> GetFilenames(string name)
{

View File

@ -198,7 +198,7 @@ namespace osu.Game.Skinning
using (var streamContent = new MemoryStream(Encoding.UTF8.GetBytes(skinInfoJson)))
{
modelManager.AddFile(s, streamContent, skin_info_file, s.Realm);
modelManager.AddFile(s, streamContent, skin_info_file, s.Realm!);
}
// Then serialise each of the drawable component groups into respective files.
@ -213,9 +213,9 @@ namespace osu.Game.Skinning
var oldFile = s.GetFile(filename);
if (oldFile != null)
modelManager.ReplaceFile(oldFile, streamContent, s.Realm);
modelManager.ReplaceFile(oldFile, streamContent, s.Realm!);
else
modelManager.AddFile(s, streamContent, filename, s.Realm);
modelManager.AddFile(s, streamContent, filename, s.Realm!);
}
}

View File

@ -111,7 +111,7 @@ namespace osu.Game.Users
protected string Username => score.User.Username;
public BeatmapInfo BeatmapInfo => score.BeatmapInfo;
public BeatmapInfo? BeatmapInfo => score.BeatmapInfo;
public WatchingReplay(ScoreInfo score)
{

View File

@ -35,9 +35,9 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="10.20.0" />
<PackageReference Include="ppy.osu.Framework" Version="2023.625.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.625.0" />
<PackageReference Include="Realm" Version="11.1.2" />
<PackageReference Include="ppy.osu.Framework" Version="2023.707.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.707.0" />
<PackageReference Include="Sentry" Version="3.28.1" />
<PackageReference Include="SharpCompress" Version="0.32.2" />
<PackageReference Include="NUnit" Version="3.13.3" />

View File

@ -16,6 +16,6 @@
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.625.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.707.0" />
</ItemGroup>
</Project>