diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 93cda34ef7..ab88be1511 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -87,6 +87,34 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestDecodeLegacyOnlineID() + { + var decoder = new TestLegacyScoreDecoder(); + + using (var resourceStream = TestResources.OpenResource("Replays/taiko-replay-with-legacy-online-id.osr")) + { + var score = decoder.Parse(resourceStream); + + Assert.That(score.ScoreInfo.OnlineID, Is.EqualTo(-1)); + Assert.That(score.ScoreInfo.LegacyOnlineID, Is.EqualTo(255)); + } + } + + [Test] + public void TestDecodeNewOnlineID() + { + var decoder = new TestLegacyScoreDecoder(); + + using (var resourceStream = TestResources.OpenResource("Replays/taiko-replay-with-new-online-id.osr")) + { + var score = decoder.Parse(resourceStream); + + Assert.That(score.ScoreInfo.OnlineID, Is.EqualTo(258)); + Assert.That(score.ScoreInfo.LegacyOnlineID, Is.EqualTo(-1)); + } + } + [TestCase(3, true)] [TestCase(6, false)] [TestCase(LegacyBeatmapDecoder.LATEST_VERSION, false)] diff --git a/osu.Game.Tests/Resources/Replays/taiko-replay-with-legacy-online-id.osr b/osu.Game.Tests/Resources/Replays/taiko-replay-with-legacy-online-id.osr new file mode 100644 index 0000000000..85ad28c7ca Binary files /dev/null and b/osu.Game.Tests/Resources/Replays/taiko-replay-with-legacy-online-id.osr differ diff --git a/osu.Game.Tests/Resources/Replays/taiko-replay-with-new-online-id.osr b/osu.Game.Tests/Resources/Replays/taiko-replay-with-new-online-id.osr new file mode 100644 index 0000000000..63e05f5fcd Binary files /dev/null and b/osu.Game.Tests/Resources/Replays/taiko-replay-with-new-online-id.osr differ diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs index 6ccf73d8ff..5b32f380b9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs @@ -213,7 +213,7 @@ namespace osu.Game.Tests.Visual.Gameplay OnlineID = hasOnlineId ? online_score_id : 0, Ruleset = new OsuRuleset().RulesetInfo, BeatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First(), - Hash = replayAvailable ? "online" : string.Empty, + HasOnlineReplay = replayAvailable, User = new APIUser { Id = 39828, diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index a6d4fb0b52..7fa4f8c836 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -247,6 +247,7 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("end spectator before retry", () => Game.SpectatorClient.EndPlaying(player.GameplayState)); AddStep("attempt to retry", () => player.ChildrenOfType().First().Action()); + AddAssert("old player score marked failed", () => player.Score.ScoreInfo.Rank, () => Is.EqualTo(ScoreRank.F)); AddUntilStep("wait for old player gone", () => Game.ScreenStack.CurrentScreen != player); AddUntilStep("get new player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); @@ -259,6 +260,7 @@ namespace osu.Game.Tests.Visual.Navigation var getOriginalPlayer = playToCompletion(); AddStep("attempt to retry", () => getOriginalPlayer().ChildrenOfType().First().Action()); + AddAssert("original play isn't failed", () => getOriginalPlayer().Score.ScoreInfo.Rank, () => Is.Not.EqualTo(ScoreRank.F)); AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player); } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs index 88904bf85b..c17a9ddf5f 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneSkinEditorNavigation.cs @@ -5,6 +5,7 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; @@ -18,6 +19,7 @@ using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD.HitErrorMeters; +using osu.Game.Skinning; using osu.Game.Tests.Beatmaps.IO; using osuTK; using osuTK.Input; @@ -31,7 +33,7 @@ namespace osu.Game.Tests.Visual.Navigation private SkinEditor skinEditor => Game.ChildrenOfType().FirstOrDefault(); [Test] - public void TestEditComponentDuringGameplay() + public void TestEditComponentFromGameplayScene() { advanceToSongSelect(); openSkinEditor(); @@ -69,6 +71,28 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("value is less than default", () => hitErrorMeter.JudgementLineThickness.Value < hitErrorMeter.JudgementLineThickness.Default); } + [Test] + public void TestMutateProtectedSkinDuringGameplay() + { + advanceToSongSelect(); + AddStep("set default skin", () => Game.Dependencies.Get().CurrentSkinInfo.SetDefault()); + + AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely()); + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("enable NF", () => Game.SelectedMods.Value = new[] { new OsuModNoFail() }); + AddStep("enter gameplay", () => InputManager.Key(Key.Enter)); + + AddUntilStep("wait for player", () => + { + DismissAnyNotifications(); + return Game.ScreenStack.CurrentScreen is Player; + }); + + openSkinEditor(); + AddUntilStep("current skin is mutable", () => !Game.Dependencies.Get().CurrentSkin.Value.SkinInfo.Value.Protected); + } + [Test] public void TestComponentsDeselectedOnSkinEditorHide() { diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 146482e6fb..ab2e867255 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -362,7 +362,7 @@ namespace osu.Game.Tests.Visual.Ranking { var score = TestResources.CreateTestScoreInfo(); score.TotalScore += 10 - i; - score.Hash = $"test{i}"; + score.HasOnlineReplay = true; scores.Add(score); } diff --git a/osu.Game/Database/IHasRealmFiles.cs b/osu.Game/Database/IHasRealmFiles.cs index 79ea719583..b301bb04de 100644 --- a/osu.Game/Database/IHasRealmFiles.cs +++ b/osu.Game/Database/IHasRealmFiles.cs @@ -10,13 +10,15 @@ namespace osu.Game.Database /// /// A model that contains a list of files it is responsible for. /// - public interface IHasRealmFiles + public interface IHasRealmFiles : IHasNamedFiles { /// /// Available files in this model, with locally filenames. /// When performing lookups, consider using or to do case-insensitive lookups. /// - IList Files { get; } + new IList Files { get; } + + IEnumerable IHasNamedFiles.Files => Files; /// /// A combined hash representing the model, based on the files it contains. diff --git a/osu.Game/Database/ImportParameters.cs b/osu.Game/Database/ImportParameters.cs index 83ca0ac694..8d37597afc 100644 --- a/osu.Game/Database/ImportParameters.cs +++ b/osu.Game/Database/ImportParameters.cs @@ -21,5 +21,11 @@ namespace osu.Game.Database /// Whether this import should use hard links rather than file copy operations if available. /// public bool PreferHardLinks { get; set; } + + /// + /// If set to , this import will not respect . + /// This is useful for cases where an import must complete even if gameplay is in progress. + /// + public bool ImportImmediately { get; set; } } } diff --git a/osu.Game/Database/RealmAccess.cs b/osu.Game/Database/RealmAccess.cs index 89bc631cfd..e9f49ec662 100644 --- a/osu.Game/Database/RealmAccess.cs +++ b/osu.Game/Database/RealmAccess.cs @@ -87,8 +87,9 @@ namespace osu.Game.Database /// 33 2023-08-16 Reset default chat toggle key binding to avoid conflict with newly added leaderboard toggle key binding. /// 34 2023-08-21 Add BackgroundReprocessingFailed flag to ScoreInfo to track upgrade failures. /// 35 2023-10-16 Clear key combinations of keybindings that are assigned to more than one action in a given settings section. + /// 36 2023-10-26 Add LegacyOnlineID to ScoreInfo. Move osu_scores_*_high IDs stored in OnlineID to LegacyOnlineID. Reset anomalous OnlineIDs. /// - private const int schema_version = 35; + private const int schema_version = 36; /// /// Lock object which is held during sections, blocking realm retrieval during blocking periods. @@ -1075,6 +1076,24 @@ namespace osu.Game.Database break; } + + case 36: + { + foreach (var score in migration.NewRealm.All()) + { + if (score.OnlineID > 0) + { + score.LegacyOnlineID = score.OnlineID; + score.OnlineID = -1; + } + else + { + score.LegacyOnlineID = score.OnlineID = -1; + } + } + + break; + } } Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms"); diff --git a/osu.Game/Database/RealmArchiveModelImporter.cs b/osu.Game/Database/RealmArchiveModelImporter.cs index 730465e1b0..5383040eb4 100644 --- a/osu.Game/Database/RealmArchiveModelImporter.cs +++ b/osu.Game/Database/RealmArchiveModelImporter.cs @@ -261,7 +261,7 @@ namespace osu.Game.Database /// An optional cancellation token. public virtual Live? ImportModel(TModel item, ArchiveReader? archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default) => Realm.Run(realm => { - pauseIfNecessary(cancellationToken); + pauseIfNecessary(parameters, cancellationToken); TModel? existing; @@ -560,9 +560,9 @@ namespace osu.Game.Database /// Whether to perform deletion. protected virtual bool ShouldDeleteArchive(string path) => false; - private void pauseIfNecessary(CancellationToken cancellationToken) + private void pauseIfNecessary(ImportParameters importParameters, CancellationToken cancellationToken) { - if (!PauseImports) + if (!PauseImports || importParameters.ImportImmediately) return; Logger.Log($@"{GetType().Name} is being paused."); diff --git a/osu.Game/Extensions/ModelExtensions.cs b/osu.Game/Extensions/ModelExtensions.cs index efb3c4d633..eef9b63b62 100644 --- a/osu.Game/Extensions/ModelExtensions.cs +++ b/osu.Game/Extensions/ModelExtensions.cs @@ -114,8 +114,24 @@ namespace osu.Game.Extensions /// /// The instance to compare. /// The other instance to compare against. - /// Whether online IDs match. If either instance is missing an online ID, this will return false. - public static bool MatchesOnlineID(this IScoreInfo? instance, IScoreInfo? other) => matchesOnlineID(instance, other); + /// + /// Whether online IDs match. + /// Both and are checked, in that order. + /// If either instance is missing an online ID, this will return false. + /// + public static bool MatchesOnlineID(this IScoreInfo? instance, IScoreInfo? other) + { + if (matchesOnlineID(instance, other)) + return true; + + if (instance == null || other == null) + return false; + + if (instance.LegacyOnlineID < 0 || other.LegacyOnlineID < 0) + return false; + + return instance.LegacyOnlineID.Equals(other.LegacyOnlineID); + } private static bool matchesOnlineID(this IHasOnlineID? instance, IHasOnlineID? other) { diff --git a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs index 15f4bace96..ac2d8152b1 100644 --- a/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/SoloScoreInfo.cs @@ -7,16 +7,16 @@ using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using osu.Game.Beatmaps; -using osu.Game.Database; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Users; namespace osu.Game.Online.API.Requests.Responses { [Serializable] - public class SoloScoreInfo : IHasOnlineID + public class SoloScoreInfo : IScoreInfo { [JsonProperty("beatmap_id")] public int BeatmapID { get; set; } @@ -138,6 +138,18 @@ namespace osu.Game.Online.API.Requests.Responses #endregion + #region IScoreInfo + + public long OnlineID => (long?)ID ?? -1; + + IUser IScoreInfo.User => User!; + DateTimeOffset IScoreInfo.Date => EndedAt; + long IScoreInfo.LegacyOnlineID => (long?)LegacyScoreId ?? -1; + IBeatmapInfo IScoreInfo.Beatmap => Beatmap!; + IRulesetInfo IScoreInfo.Ruleset => Beatmap!.Ruleset; + + #endregion + public override string ToString() => $"score_id: {ID} user_id: {UserID}"; /// @@ -178,6 +190,7 @@ namespace osu.Game.Online.API.Requests.Responses var score = new ScoreInfo { OnlineID = OnlineID, + LegacyOnlineID = (long?)LegacyScoreId ?? -1, User = User ?? new APIUser { Id = UserID }, BeatmapInfo = new BeatmapInfo { OnlineID = BeatmapID }, Ruleset = new RulesetInfo { OnlineID = RulesetID }, @@ -189,7 +202,7 @@ namespace osu.Game.Online.API.Requests.Responses Statistics = Statistics, MaximumStatistics = MaximumStatistics, Date = EndedAt, - Hash = HasReplay ? "online" : string.Empty, // TODO: temporary? + HasOnlineReplay = HasReplay, Mods = mods, PP = PP, }; @@ -223,7 +236,5 @@ namespace osu.Game.Online.API.Requests.Responses Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value), MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value), }; - - public long OnlineID => (long?)ID ?? -1; } } diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs index d5e0c7a970..f1b9584d57 100644 --- a/osu.Game/Online/Rooms/MultiplayerScore.cs +++ b/osu.Game/Online/Rooms/MultiplayerScore.cs @@ -58,6 +58,9 @@ namespace osu.Game.Online.Rooms [JsonProperty("position")] public int? Position { get; set; } + [JsonProperty("has_replay")] + public bool HasReplay { get; set; } + /// /// Any scores in the room around this score. /// @@ -84,7 +87,7 @@ namespace osu.Game.Online.Rooms User = User, Accuracy = Accuracy, Date = EndedAt, - Hash = string.Empty, // todo: temporary? + HasOnlineReplay = HasReplay, Rank = Rank, Mods = Mods?.Select(m => m.ToMod(rulesetInstance)).ToArray() ?? Array.Empty(), Position = Position, diff --git a/osu.Game/Online/ScoreDownloadTracker.cs b/osu.Game/Online/ScoreDownloadTracker.cs index de42292372..dfdac24d19 100644 --- a/osu.Game/Online/ScoreDownloadTracker.cs +++ b/osu.Game/Online/ScoreDownloadTracker.cs @@ -39,7 +39,8 @@ namespace osu.Game.Online var scoreInfo = new ScoreInfo { ID = TrackedItem.ID, - OnlineID = TrackedItem.OnlineID + OnlineID = TrackedItem.OnlineID, + LegacyOnlineID = TrackedItem.LegacyOnlineID }; Downloader.DownloadBegan += downloadBegan; @@ -47,6 +48,7 @@ namespace osu.Game.Online realmSubscription = realm.RegisterForNotifications(r => r.All().Where(s => ((s.OnlineID > 0 && s.OnlineID == TrackedItem.OnlineID) + || (s.LegacyOnlineID > 0 && s.LegacyOnlineID == TrackedItem.LegacyOnlineID) || (!string.IsNullOrEmpty(s.Hash) && s.Hash == TrackedItem.Hash)) && !s.DeletePending), (items, _) => { diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 885077a8e8..2f11964f6a 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -678,6 +678,9 @@ namespace osu.Game if (score.OnlineID > 0) databasedScoreInfo = ScoreManager.Query(s => s.OnlineID == score.OnlineID); + if (score.LegacyOnlineID > 0) + databasedScoreInfo ??= ScoreManager.Query(s => s.LegacyOnlineID == score.LegacyOnlineID); + if (score is ScoreInfo scoreInfo) databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == scoreInfo.Hash); diff --git a/osu.Game/Scoring/IScoreInfo.cs b/osu.Game/Scoring/IScoreInfo.cs index d17558f800..a1d076b8c2 100644 --- a/osu.Game/Scoring/IScoreInfo.cs +++ b/osu.Game/Scoring/IScoreInfo.cs @@ -9,7 +9,7 @@ using osu.Game.Users; namespace osu.Game.Scoring { - public interface IScoreInfo : IHasOnlineID, IHasNamedFiles + public interface IScoreInfo : IHasOnlineID { IUser User { get; } @@ -22,7 +22,7 @@ namespace osu.Game.Scoring double Accuracy { get; } - bool HasReplay { get; } + long LegacyOnlineID { get; } DateTimeOffset Date { get; } diff --git a/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs b/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs index f2e8cf141b..d34edf7bdf 100644 --- a/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs +++ b/osu.Game/Scoring/Legacy/LegacyReplaySoloScoreInfo.cs @@ -19,6 +19,13 @@ namespace osu.Game.Scoring.Legacy [JsonObject(MemberSerialization.OptIn)] public class LegacyReplaySoloScoreInfo { + /// + /// The value of this property should correspond to + /// (i.e. come from the `solo_scores` ID scheme). + /// + [JsonProperty("online_id")] + public long OnlineID { get; set; } = -1; + [JsonProperty("mods")] public APIMod[] Mods { get; set; } = Array.Empty(); @@ -30,6 +37,7 @@ namespace osu.Game.Scoring.Legacy public static LegacyReplaySoloScoreInfo FromScore(ScoreInfo score) => new LegacyReplaySoloScoreInfo { + OnlineID = score.OnlineID, Mods = score.APIMods, Statistics = score.Statistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value), MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(kvp => kvp.Key, kvp => kvp.Value), diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index 8c00110909..c5e6e3bcce 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -101,9 +101,9 @@ namespace osu.Game.Scoring.Legacy byte[] compressedReplay = sr.ReadByteArray(); if (version >= 20140721) - scoreInfo.OnlineID = sr.ReadInt64(); + scoreInfo.LegacyOnlineID = sr.ReadInt64(); else if (version >= 20121008) - scoreInfo.OnlineID = sr.ReadInt32(); + scoreInfo.LegacyOnlineID = sr.ReadInt32(); byte[] compressedScoreInfo = null; @@ -121,6 +121,7 @@ namespace osu.Game.Scoring.Legacy Debug.Assert(readScore != null); + score.ScoreInfo.OnlineID = readScore.OnlineID; score.ScoreInfo.Statistics = readScore.Statistics; score.ScoreInfo.MaximumStatistics = readScore.MaximumStatistics; score.ScoreInfo.Mods = readScore.Mods.Select(m => m.ToMod(currentRuleset)).ToArray(); diff --git a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs index a95964ac52..872f09dda6 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreEncoder.cs @@ -84,7 +84,7 @@ namespace osu.Game.Scoring.Legacy sw.Write(getHpGraphFormatted()); sw.Write(score.ScoreInfo.Date.DateTime); sw.WriteByteArray(createReplayData()); - sw.Write((long)0); + sw.Write(score.ScoreInfo.LegacyOnlineID); writeModSpecificData(score.ScoreInfo, sw); sw.WriteByteArray(createScoreInfoData()); } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 8531408555..d712702331 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -94,15 +94,32 @@ namespace osu.Game.Scoring public double Accuracy { get; set; } - public bool HasReplay => !string.IsNullOrEmpty(Hash); + [Ignored] + public bool HasOnlineReplay { get; set; } public DateTimeOffset Date { get; set; } public double? PP { get; set; } + /// + /// The online ID of this score. + /// + /// + /// In the osu-web database, this ID (if present) comes from the new solo_scores table. + /// [Indexed] public long OnlineID { get; set; } = -1; + /// + /// The legacy online ID of this score. + /// + /// + /// In the osu-web database, this ID (if present) comes from the legacy osu_scores_*_high tables. + /// This ID is also stored to replays set on osu!stable. + /// + [Indexed] + public long LegacyOnlineID { get; set; } = -1; + [MapTo("User")] public RealmUser RealmUser { get; set; } = null!; @@ -168,7 +185,6 @@ namespace osu.Game.Scoring IRulesetInfo IScoreInfo.Ruleset => Ruleset; IBeatmapInfo? IScoreInfo.Beatmap => BeatmapInfo; IUser IScoreInfo.User => User; - IEnumerable IHasNamedFiles.Files => Files; #region Properties required to make things work with existing usages diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 31b5bd8365..02d9e0a280 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -150,7 +150,11 @@ namespace osu.Game.Scoring public Task Import(ImportTask[] imports, ImportParameters parameters = default) => scoreImporter.Import(imports, parameters); - public override bool IsAvailableLocally(ScoreInfo model) => Realm.Run(realm => realm.All().Any(s => s.OnlineID == model.OnlineID)); + public override bool IsAvailableLocally(ScoreInfo model) + => Realm.Run(realm => realm.All() + // this basically inlines `ModelExtension.MatchesOnlineID(IScoreInfo, IScoreInfo)`, + // because that method can't be used here, as realm can't translate it to its query language. + .Any(s => s.OnlineID == model.OnlineID || s.LegacyOnlineID == model.LegacyOnlineID)); public IEnumerable HandledExtensions => scoreImporter.HandledExtensions; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs index 9708a94cd7..f665ed2d41 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs @@ -10,7 +10,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer public partial class MultiplayerResultsScreen : PlaylistsResultsScreen { public MultiplayerResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem) - : base(score, roomId, playlistItem, false, false) + : base(score, roomId, playlistItem, false) { } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 97bfa35d49..8c7fc551ba 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -1110,13 +1110,14 @@ namespace osu.Game.Screens.Play failAnimationContainer?.Stop(); PauseOverlay?.StopAllSamples(); - if (LoadedBeatmapSuccessfully) + if (LoadedBeatmapSuccessfully && !GameplayState.HasPassed) { - if (!GameplayState.HasPassed && !GameplayState.HasFailed) + Debug.Assert(resultsDisplayDelegate == null); + + if (!GameplayState.HasFailed) GameplayState.HasQuit = true; - // if arriving here and the results screen preparation task hasn't run, it's safe to say the user has not completed the beatmap. - if (prepareScoreForDisplayTask == null && DrawableRuleset.ReplayScore == null) + if (DrawableRuleset.ReplayScore == null) ScoreProcessor.FailScore(Score.ScoreInfo); } @@ -1166,13 +1167,6 @@ namespace osu.Game.Screens.Play // the import process will re-attach managed beatmap/rulesets to this score. we don't want this for now, so create a temporary copy to import. var importableScore = score.ScoreInfo.DeepClone(); - // For the time being, online ID responses are not really useful for anything. - // In addition, the IDs provided via new (lazer) endpoints are based on a different autoincrement from legacy (stable) scores. - // - // Until we better define the server-side logic behind this, let's not store the online ID to avoid potential unique constraint - // conflicts across various systems (ie. solo and multiplayer). - importableScore.OnlineID = -1; - var imported = scoreManager.Import(importableScore, replayReader); imported.PerformRead(s => diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index b6166e97f6..df5f9c7a8a 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Ranking if (State.Value == DownloadState.LocallyAvailable) return ReplayAvailability.Local; - if (Score.Value?.HasReplay == true) + if (Score.Value?.HasOnlineReplay == true) return ReplayAvailability.Online; return ReplayAvailability.NotAvailable; diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index f187b8a302..da08a26a58 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; +using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Solo; @@ -67,7 +68,7 @@ namespace osu.Game.Screens.Ranking return null; getScoreRequest = new GetScoresRequest(Score.BeatmapInfo, Score.Ruleset); - getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineID != Score.OnlineID).Select(s => s.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo))); + getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => !s.MatchesOnlineID(Score)).Select(s => s.ToScoreInfo(rulesets, Beatmap.Value.BeatmapInfo))); return getScoreRequest; } diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs index c2b80b7ead..9763d3b57e 100644 --- a/osu.Game/Skinning/SkinInfo.cs +++ b/osu.Game/Skinning/SkinInfo.cs @@ -14,7 +14,7 @@ namespace osu.Game.Skinning { [MapTo("Skin")] [JsonObject(MemberSerialization.OptIn)] - public class SkinInfo : RealmObject, IHasRealmFiles, IEquatable, IHasGuidPrimaryKey, ISoftDelete, IHasNamedFiles + public class SkinInfo : RealmObject, IHasRealmFiles, IEquatable, IHasGuidPrimaryKey, ISoftDelete { internal static readonly Guid TRIANGLES_SKIN = new Guid("2991CFD8-2140-469A-BCB9-2EC23FBCE4AD"); internal static readonly Guid ARGON_SKIN = new Guid("CFFA69DE-B3E3-4DEE-8563-3C4F425C05D0"); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index ca46d3af0c..59c2a8bca0 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -182,7 +182,10 @@ namespace osu.Game.Skinning Name = NamingUtils.GetNextBestName(existingSkinNames, $@"{s.Name} (modified)") }; - var result = skinImporter.ImportModel(skinInfo); + var result = skinImporter.ImportModel(skinInfo, parameters: new ImportParameters + { + ImportImmediately = true // to avoid possible deadlocks when editing skin during gameplay. + }); if (result != null) {