mirror of
https://github.com/ppy/osu
synced 2025-02-20 20:47:09 +00:00
Merge branch 'master' into osu-random-mod-logic-changes
This commit is contained in:
commit
e689973de5
@ -1,25 +1,80 @@
|
||||
// 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.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Tests.Resources;
|
||||
using Realms;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Tests.Database
|
||||
{
|
||||
[TestFixture]
|
||||
public class RealmSubscriptionRegistrationTests : RealmTest
|
||||
{
|
||||
[Test]
|
||||
public void TestSubscriptionCollectionAndPropertyChanges()
|
||||
{
|
||||
int collectionChanges = 0;
|
||||
int propertyChanges = 0;
|
||||
|
||||
ChangeSet? lastChanges = null;
|
||||
|
||||
RunTestWithRealm((realm, _) =>
|
||||
{
|
||||
var registration = realm.RegisterForNotifications(r => r.All<BeatmapSetInfo>(), onChanged);
|
||||
|
||||
realm.Run(r => r.Refresh());
|
||||
|
||||
realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
|
||||
realm.Run(r => r.Refresh());
|
||||
|
||||
Assert.That(collectionChanges, Is.EqualTo(1));
|
||||
Assert.That(propertyChanges, Is.EqualTo(0));
|
||||
Assert.That(lastChanges?.InsertedIndices, Has.One.Items);
|
||||
Assert.That(lastChanges?.ModifiedIndices, Is.Empty);
|
||||
Assert.That(lastChanges?.NewModifiedIndices, Is.Empty);
|
||||
|
||||
realm.Write(r => r.All<BeatmapSetInfo>().First().Beatmaps.First().CountdownOffset = 5);
|
||||
realm.Run(r => r.Refresh());
|
||||
|
||||
Assert.That(collectionChanges, Is.EqualTo(1));
|
||||
Assert.That(propertyChanges, Is.EqualTo(1));
|
||||
Assert.That(lastChanges?.InsertedIndices, Is.Empty);
|
||||
Assert.That(lastChanges?.ModifiedIndices, Has.One.Items);
|
||||
Assert.That(lastChanges?.NewModifiedIndices, Has.One.Items);
|
||||
|
||||
registration.Dispose();
|
||||
});
|
||||
|
||||
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error)
|
||||
{
|
||||
lastChanges = changes;
|
||||
|
||||
if (changes == null)
|
||||
return;
|
||||
|
||||
if (changes.HasCollectionChanges())
|
||||
{
|
||||
Interlocked.Increment(ref collectionChanges);
|
||||
}
|
||||
else
|
||||
{
|
||||
Interlocked.Increment(ref propertyChanges);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSubscriptionWithAsyncWrite()
|
||||
{
|
||||
|
@ -0,0 +1,40 @@
|
||||
// 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 Newtonsoft.Json;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.IO.Serialization;
|
||||
using osu.Game.Online.Solo;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Online
|
||||
{
|
||||
/// <summary>
|
||||
/// Basic testing to ensure our attribute-based naming is correctly working.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
public class TestSubmittableScoreJsonSerialization
|
||||
{
|
||||
[Test]
|
||||
public void TestScoreSerialisationViaExtensionMethod()
|
||||
{
|
||||
var score = new SubmittableScore(TestResources.CreateTestScoreInfo());
|
||||
|
||||
string serialised = score.Serialize();
|
||||
|
||||
Assert.That(serialised, Contains.Substring("large_tick_hit"));
|
||||
Assert.That(serialised, Contains.Substring("\"rank\": \"S\""));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScoreSerialisationWithoutSettings()
|
||||
{
|
||||
var score = new SubmittableScore(TestResources.CreateTestScoreInfo());
|
||||
|
||||
string serialised = JsonConvert.SerializeObject(score);
|
||||
|
||||
Assert.That(serialised, Contains.Substring("large_tick_hit"));
|
||||
Assert.That(serialised, Contains.Substring("\"rank\":\"S\""));
|
||||
}
|
||||
}
|
||||
}
|
@ -43,8 +43,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
|
||||
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
|
||||
Dependencies.Cache(Realm);
|
||||
|
||||
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
@ -52,8 +50,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AvailabilityTracker.SelectedItem.BindTo(selectedItem);
|
||||
|
||||
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
|
||||
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
|
||||
|
||||
selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo)
|
||||
{
|
||||
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
|
||||
@ -92,16 +92,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
OsuButton readyButton = null;
|
||||
|
||||
AddAssert("ensure ready button enabled", () =>
|
||||
AddUntilStep("ensure ready button enabled", () =>
|
||||
{
|
||||
readyButton = button.ChildrenOfType<OsuButton>().Single();
|
||||
return readyButton.Enabled.Value;
|
||||
});
|
||||
|
||||
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
|
||||
AddAssert("ready button disabled", () => !readyButton.Enabled.Value);
|
||||
AddUntilStep("ready button disabled", () => !readyButton.Enabled.Value);
|
||||
AddStep("undelete beatmap", () => beatmaps.Undelete(importedSet));
|
||||
AddAssert("ready button enabled back", () => readyButton.Enabled.Value);
|
||||
AddUntilStep("ready button enabled back", () => readyButton.Enabled.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddUntilStep("ensure manager loaded", () => beatmaps != null);
|
||||
ensureSoleilyRemoved();
|
||||
createButtonWithBeatmap(createSoleily());
|
||||
AddAssert("button state not downloaded", () => downloadButton.DownloadState == DownloadState.NotDownloaded);
|
||||
AddUntilStep("button state not downloaded", () => downloadButton.DownloadState == DownloadState.NotDownloaded);
|
||||
AddStep("import soleily", () => beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()));
|
||||
|
||||
AddUntilStep("wait for beatmap import", () => beatmaps.GetAllUsableBeatmapSets().Any(b => b.OnlineID == 241526));
|
||||
@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
createButtonWithBeatmap(createSoleily());
|
||||
AddUntilStep("button state downloaded", () => downloadButton.DownloadState == DownloadState.LocallyAvailable);
|
||||
ensureSoleilyRemoved();
|
||||
AddAssert("button state not downloaded", () => downloadButton.DownloadState == DownloadState.NotDownloaded);
|
||||
AddUntilStep("button state not downloaded", () => downloadButton.DownloadState == DownloadState.NotDownloaded);
|
||||
}
|
||||
|
||||
private void ensureSoleilyRemoved()
|
||||
|
@ -33,16 +33,25 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
|
||||
private TestResultsScreen resultsScreen;
|
||||
|
||||
private int currentScoreId;
|
||||
private int lowestScoreId; // Score ID of the lowest score in the list.
|
||||
private int highestScoreId; // Score ID of the highest score in the list.
|
||||
|
||||
private bool requestComplete;
|
||||
private int totalCount;
|
||||
private ScoreInfo userScore;
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
currentScoreId = 1;
|
||||
lowestScoreId = 1;
|
||||
highestScoreId = 1;
|
||||
requestComplete = false;
|
||||
totalCount = 0;
|
||||
|
||||
userScore = TestResources.CreateTestScoreInfo();
|
||||
userScore.TotalScore = 0;
|
||||
userScore.Statistics = new Dictionary<HitResult, int>();
|
||||
|
||||
bindHandler();
|
||||
|
||||
// beatmap is required to be an actual beatmap so the scores can get their scores correctly calculated for standardised scoring.
|
||||
@ -53,15 +62,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
[Test]
|
||||
public void TestShowWithUserScore()
|
||||
{
|
||||
ScoreInfo userScore = null;
|
||||
|
||||
AddStep("bind user score info handler", () =>
|
||||
{
|
||||
userScore = TestResources.CreateTestScoreInfo();
|
||||
userScore.OnlineID = currentScoreId++;
|
||||
|
||||
bindHandler(userScore: userScore);
|
||||
});
|
||||
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
|
||||
|
||||
createResults(() => userScore);
|
||||
|
||||
@ -81,15 +82,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
[Test]
|
||||
public void TestShowUserScoreWithDelay()
|
||||
{
|
||||
ScoreInfo userScore = null;
|
||||
|
||||
AddStep("bind user score info handler", () =>
|
||||
{
|
||||
userScore = TestResources.CreateTestScoreInfo();
|
||||
userScore.OnlineID = currentScoreId++;
|
||||
|
||||
bindHandler(true, userScore);
|
||||
});
|
||||
AddStep("bind user score info handler", () => bindHandler(true, userScore));
|
||||
|
||||
createResults(() => userScore);
|
||||
|
||||
@ -124,7 +117,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
|
||||
waitForDisplay();
|
||||
|
||||
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
|
||||
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() >= beforePanelCount + scores_per_result);
|
||||
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
|
||||
}
|
||||
}
|
||||
@ -132,15 +125,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
[Test]
|
||||
public void TestFetchWhenScrolledToTheLeft()
|
||||
{
|
||||
ScoreInfo userScore = null;
|
||||
|
||||
AddStep("bind user score info handler", () =>
|
||||
{
|
||||
userScore = TestResources.CreateTestScoreInfo();
|
||||
userScore.OnlineID = currentScoreId++;
|
||||
|
||||
bindHandler(userScore: userScore);
|
||||
});
|
||||
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
|
||||
|
||||
createResults(() => userScore);
|
||||
|
||||
@ -156,7 +141,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible);
|
||||
waitForDisplay();
|
||||
|
||||
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
|
||||
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() >= beforePanelCount + scores_per_result);
|
||||
AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden);
|
||||
}
|
||||
}
|
||||
@ -245,16 +230,13 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
{
|
||||
var multiplayerUserScore = new MultiplayerScore
|
||||
{
|
||||
ID = (int)(userScore.OnlineID > 0 ? userScore.OnlineID : currentScoreId++),
|
||||
ID = highestScoreId,
|
||||
Accuracy = userScore.Accuracy,
|
||||
EndedAt = userScore.Date,
|
||||
Passed = userScore.Passed,
|
||||
Rank = userScore.Rank,
|
||||
Position = real_user_position,
|
||||
MaxCombo = userScore.MaxCombo,
|
||||
TotalScore = userScore.TotalScore,
|
||||
User = userScore.User,
|
||||
Statistics = userScore.Statistics,
|
||||
ScoresAround = new MultiplayerScoresAround
|
||||
{
|
||||
Higher = new MultiplayerScores(),
|
||||
@ -268,38 +250,32 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
{
|
||||
multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore
|
||||
{
|
||||
ID = currentScoreId++,
|
||||
ID = --highestScoreId,
|
||||
Accuracy = userScore.Accuracy,
|
||||
EndedAt = userScore.Date,
|
||||
Passed = true,
|
||||
Rank = userScore.Rank,
|
||||
MaxCombo = userScore.MaxCombo,
|
||||
TotalScore = userScore.TotalScore - i,
|
||||
User = new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Username = $"peppy{i}",
|
||||
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||
},
|
||||
Statistics = userScore.Statistics
|
||||
});
|
||||
|
||||
multiplayerUserScore.ScoresAround.Higher.Scores.Add(new MultiplayerScore
|
||||
{
|
||||
ID = currentScoreId++,
|
||||
ID = ++lowestScoreId,
|
||||
Accuracy = userScore.Accuracy,
|
||||
EndedAt = userScore.Date,
|
||||
Passed = true,
|
||||
Rank = userScore.Rank,
|
||||
MaxCombo = userScore.MaxCombo,
|
||||
TotalScore = userScore.TotalScore + i,
|
||||
User = new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Username = $"peppy{i}",
|
||||
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||
},
|
||||
Statistics = userScore.Statistics
|
||||
});
|
||||
|
||||
totalCount += 2;
|
||||
@ -315,33 +291,23 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
{
|
||||
var result = new IndexedMultiplayerScores();
|
||||
|
||||
long startTotalScore = req.Cursor?.Properties["total_score"].ToObject<long>() ?? 1000000;
|
||||
string sort = req.IndexParams?.Properties["sort"].ToObject<string>() ?? "score_desc";
|
||||
|
||||
for (int i = 1; i <= scores_per_result; i++)
|
||||
{
|
||||
result.Scores.Add(new MultiplayerScore
|
||||
{
|
||||
ID = currentScoreId++,
|
||||
ID = sort == "score_asc" ? --highestScoreId : ++lowestScoreId,
|
||||
Accuracy = 1,
|
||||
EndedAt = DateTimeOffset.Now,
|
||||
Passed = true,
|
||||
Rank = ScoreRank.X,
|
||||
MaxCombo = 1000,
|
||||
TotalScore = startTotalScore + (sort == "score_asc" ? i : -i),
|
||||
User = new APIUser
|
||||
{
|
||||
Id = 2,
|
||||
Username = $"peppy{i}",
|
||||
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||
},
|
||||
Statistics = new Dictionary<HitResult, int>
|
||||
{
|
||||
{ HitResult.Miss, 1 },
|
||||
{ HitResult.Meh, 50 },
|
||||
{ HitResult.Good, 100 },
|
||||
{ HitResult.Great, 300 }
|
||||
}
|
||||
});
|
||||
|
||||
totalCount++;
|
||||
@ -367,7 +333,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
{
|
||||
Properties = new Dictionary<string, JToken>
|
||||
{
|
||||
{ "sort", JToken.FromObject(scores.Scores[^1].TotalScore > scores.Scores[^2].TotalScore ? "score_asc" : "score_desc") }
|
||||
{ "sort", JToken.FromObject(scores.Scores[^1].ID > scores.Scores[^2].ID ? "score_asc" : "score_desc") }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -2,12 +2,14 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Tests.Resources;
|
||||
@ -208,13 +210,19 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
public void TestKeyboardNavigation()
|
||||
{
|
||||
var lowestScore = TestResources.CreateTestScoreInfo();
|
||||
lowestScore.MaxCombo = 100;
|
||||
lowestScore.OnlineID = 3;
|
||||
lowestScore.TotalScore = 0;
|
||||
lowestScore.Statistics = new Dictionary<HitResult, int>();
|
||||
|
||||
var middleScore = TestResources.CreateTestScoreInfo();
|
||||
middleScore.MaxCombo = 200;
|
||||
middleScore.OnlineID = 2;
|
||||
middleScore.TotalScore = 0;
|
||||
middleScore.Statistics = new Dictionary<HitResult, int>();
|
||||
|
||||
var highestScore = TestResources.CreateTestScoreInfo();
|
||||
highestScore.MaxCombo = 300;
|
||||
highestScore.OnlineID = 1;
|
||||
highestScore.TotalScore = 0;
|
||||
highestScore.Statistics = new Dictionary<HitResult, int>();
|
||||
|
||||
createListStep(() => new ScorePanelList());
|
||||
|
||||
|
@ -284,14 +284,13 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
public void TestDummy()
|
||||
{
|
||||
createSongSelect();
|
||||
AddAssert("dummy selected", () => songSelect.CurrentBeatmap == defaultBeatmap);
|
||||
AddUntilStep("dummy selected", () => songSelect.CurrentBeatmap == defaultBeatmap);
|
||||
|
||||
AddUntilStep("dummy shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap == defaultBeatmap);
|
||||
|
||||
addManyTestMaps();
|
||||
AddWaitStep("wait for select", 3);
|
||||
|
||||
AddAssert("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap);
|
||||
AddUntilStep("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -299,9 +298,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
{
|
||||
createSongSelect();
|
||||
addManyTestMaps();
|
||||
AddWaitStep("wait for add", 3);
|
||||
|
||||
AddAssert("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap);
|
||||
AddUntilStep("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap);
|
||||
|
||||
AddStep(@"Sort by Artist", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist));
|
||||
AddStep(@"Sort by Title", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title));
|
||||
@ -571,6 +569,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
createSongSelect();
|
||||
|
||||
AddUntilStep("wait for selection", () => !Beatmap.IsDefault);
|
||||
|
||||
AddStep("press ctrl+enter", () =>
|
||||
{
|
||||
InputManager.PressKey(Key.ControlLeft);
|
||||
@ -605,6 +605,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
addRulesetImportStep(0);
|
||||
createSongSelect();
|
||||
|
||||
AddUntilStep("wait for selection", () => !Beatmap.IsDefault);
|
||||
|
||||
DrawableCarouselBeatmapSet set = null;
|
||||
AddStep("Find the DrawableCarouselBeatmapSet", () =>
|
||||
{
|
||||
@ -844,6 +846,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
|
||||
createSongSelect();
|
||||
|
||||
AddUntilStep("wait for selection", () => !Beatmap.IsDefault);
|
||||
|
||||
AddStep("present score", () =>
|
||||
{
|
||||
// this beatmap change should be overridden by the present.
|
||||
|
@ -124,7 +124,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
});
|
||||
|
||||
AddUntilStep("Became present", () => topLocalRank.IsPresent);
|
||||
AddAssert("Correct rank", () => topLocalRank.Rank == ScoreRank.B);
|
||||
AddUntilStep("Correct rank", () => topLocalRank.Rank == ScoreRank.B);
|
||||
|
||||
AddStep("Add higher score for current user", () =>
|
||||
{
|
||||
@ -137,7 +137,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
scoreManager.Import(testScoreInfo2);
|
||||
});
|
||||
|
||||
AddAssert("Correct rank", () => topLocalRank.Rank == ScoreRank.S);
|
||||
AddUntilStep("Correct rank", () => topLocalRank.Rank == ScoreRank.S);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,40 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneDifficultyMultiplierDisplay : OsuTestScene
|
||||
{
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
|
||||
|
||||
[Test]
|
||||
public void TestDifficultyMultiplierDisplay()
|
||||
{
|
||||
DifficultyMultiplierDisplay multiplierDisplay = null;
|
||||
|
||||
AddStep("create content", () => Child = multiplierDisplay = new DifficultyMultiplierDisplay
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
});
|
||||
|
||||
AddStep("set multiplier below 1", () => multiplierDisplay.Current.Value = 0.5);
|
||||
AddStep("set multiplier to 1", () => multiplierDisplay.Current.Value = 1);
|
||||
AddStep("set multiplier above 1", () => multiplierDisplay.Current.Value = 1.5);
|
||||
|
||||
AddSliderStep("set multiplier", 0, 2, 1d, multiplier =>
|
||||
{
|
||||
if (multiplierDisplay != null)
|
||||
multiplierDisplay.Current.Value = multiplier;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -105,6 +105,8 @@ namespace osu.Game.Database
|
||||
|
||||
public Realm Realm => ensureUpdateRealm();
|
||||
|
||||
private const string realm_extension = @".realm";
|
||||
|
||||
private Realm ensureUpdateRealm()
|
||||
{
|
||||
if (isSendingNotificationResetEvents)
|
||||
@ -149,11 +151,18 @@ namespace osu.Game.Database
|
||||
|
||||
Filename = filename;
|
||||
|
||||
const string realm_extension = @".realm";
|
||||
|
||||
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
|
||||
Filename += realm_extension;
|
||||
|
||||
string newerVersionFilename = $"{Filename.Replace(realm_extension, string.Empty)}_newer_version{realm_extension}";
|
||||
|
||||
// Attempt to recover a newer database version if available.
|
||||
if (storage.Exists(newerVersionFilename))
|
||||
{
|
||||
Logger.Log(@"A newer realm database has been found, attempting recovery...", LoggingTarget.Database);
|
||||
attemptRecoverFromFile(newerVersionFilename);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// This method triggers the first `getRealmInstance` call, which will implicitly run realm migrations and bring the schema up-to-date.
|
||||
@ -161,15 +170,78 @@ namespace osu.Game.Database
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Error(e, "Realm startup failed with unrecoverable error; starting with a fresh database. A backup of your database has been made.");
|
||||
// See https://github.com/realm/realm-core/blob/master/src%2Frealm%2Fobject-store%2Fobject_store.cpp#L1016-L1022
|
||||
// This is the best way we can detect a schema version downgrade.
|
||||
if (e.Message.StartsWith(@"Provided schema version", StringComparison.Ordinal))
|
||||
{
|
||||
Logger.Error(e, "Your local database is too new to work with this version of osu!. Please close osu! and install the latest release to recover your data.");
|
||||
|
||||
CreateBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_corrupt{realm_extension}");
|
||||
storage.Delete(Filename);
|
||||
// If a newer version database already exists, don't backup again. We can presume that the first backup is the one we care about.
|
||||
if (!storage.Exists(newerVersionFilename))
|
||||
CreateBackup(newerVersionFilename);
|
||||
|
||||
storage.Delete(Filename);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Error(e, "Realm startup failed with unrecoverable error; starting with a fresh database. A backup of your database has been made.");
|
||||
CreateBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_corrupt{realm_extension}");
|
||||
storage.Delete(Filename);
|
||||
}
|
||||
|
||||
cleanupPendingDeletions();
|
||||
}
|
||||
}
|
||||
|
||||
private void attemptRecoverFromFile(string recoveryFilename)
|
||||
{
|
||||
Logger.Log($@"Performing recovery from {recoveryFilename}", LoggingTarget.Database);
|
||||
|
||||
// First check the user hasn't started to use the database that is in place..
|
||||
try
|
||||
{
|
||||
using (var realm = Realm.GetInstance(getConfiguration()))
|
||||
{
|
||||
if (realm.All<ScoreInfo>().Any())
|
||||
{
|
||||
Logger.Log(@"Recovery aborted as the existing database has scores set already.", LoggingTarget.Database);
|
||||
Logger.Log(@"To perform recovery, delete client.realm while osu! is not running.", LoggingTarget.Database);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Even if reading the in place database fails, still attempt to recover.
|
||||
}
|
||||
|
||||
// Then check that the database we are about to attempt recovery can actually be recovered on this version..
|
||||
try
|
||||
{
|
||||
using (Realm.GetInstance(getConfiguration(recoveryFilename)))
|
||||
{
|
||||
// Don't need to do anything, just check that opening the realm works correctly.
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
Logger.Log(@"Recovery aborted as the newer version could not be loaded by this osu! version.", LoggingTarget.Database);
|
||||
return;
|
||||
}
|
||||
|
||||
// For extra safety, also store the temporarily-used database which we are about to replace.
|
||||
CreateBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_newer_version_before_recovery{realm_extension}");
|
||||
|
||||
storage.Delete(Filename);
|
||||
|
||||
using (var inputStream = storage.GetStream(recoveryFilename))
|
||||
using (var outputStream = storage.GetStream(Filename, FileAccess.Write, FileMode.Create))
|
||||
inputStream.CopyTo(outputStream);
|
||||
|
||||
storage.Delete(recoveryFilename);
|
||||
Logger.Log(@"Recovery complete!", LoggingTarget.Database);
|
||||
}
|
||||
|
||||
private void cleanupPendingDeletions()
|
||||
{
|
||||
using (var realm = getRealmInstance())
|
||||
@ -476,7 +548,7 @@ namespace osu.Game.Database
|
||||
}
|
||||
}
|
||||
|
||||
private RealmConfiguration getConfiguration()
|
||||
private RealmConfiguration getConfiguration(string? filename = null)
|
||||
{
|
||||
// This is currently the only usage of temporary files at the osu! side.
|
||||
// If we use the temporary folder in more situations in the future, this should be moved to a higher level (helper method or OsuGameBase).
|
||||
@ -484,7 +556,7 @@ namespace osu.Game.Database
|
||||
if (!Directory.Exists(tempPathLocation))
|
||||
Directory.CreateDirectory(tempPathLocation);
|
||||
|
||||
return new RealmConfiguration(storage.GetFullPath(Filename, true))
|
||||
return new RealmConfiguration(storage.GetFullPath(filename ?? Filename, true))
|
||||
{
|
||||
SchemaVersion = schema_version,
|
||||
MigrationCallback = onMigration,
|
||||
|
@ -4,6 +4,8 @@
|
||||
using System;
|
||||
using Realms;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
public static class RealmExtensions
|
||||
@ -22,5 +24,14 @@ namespace osu.Game.Database
|
||||
transaction.Commit();
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the provided change set has changes to the top level collection.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Realm subscriptions fire on both collection and property changes (including *all* nested properties).
|
||||
/// Quite often we only care about changes at a collection level. This can be used to guard and early-return when no such changes are in a callback.
|
||||
/// </remarks>
|
||||
public static bool HasCollectionChanges(this ChangeSet changes) => changes.InsertedIndices.Length > 0 || changes.DeletedIndices.Length > 0 || changes.Moves.Length > 0;
|
||||
}
|
||||
}
|
||||
|
@ -46,9 +46,6 @@ namespace osu.Game.Online.Solo
|
||||
[JsonProperty("mods")]
|
||||
public APIMod[] Mods { get; set; }
|
||||
|
||||
[JsonProperty("user")]
|
||||
public APIUser User { get; set; }
|
||||
|
||||
[JsonProperty("statistics")]
|
||||
public Dictionary<HitResult, int> Statistics { get; set; }
|
||||
|
||||
@ -67,7 +64,6 @@ namespace osu.Game.Online.Solo
|
||||
RulesetID = score.RulesetID;
|
||||
Passed = score.Passed;
|
||||
Mods = score.APIMods;
|
||||
User = score.User;
|
||||
Statistics = score.Statistics;
|
||||
}
|
||||
}
|
||||
|
@ -79,7 +79,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
||||
var beatmapInfo = new BeatmapInfo
|
||||
{
|
||||
MaxCombo = apiBeatmap.MaxCombo,
|
||||
Status = apiBeatmap.Status
|
||||
Status = apiBeatmap.Status,
|
||||
MD5Hash = apiBeatmap.MD5Hash
|
||||
};
|
||||
|
||||
scoreManager.OrderByTotalScoreAsync(value.Scores.Select(s => s.CreateScoreInfo(rulesets, beatmapInfo)).ToArray(), loadCancellationSource.Token)
|
||||
|
185
osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs
Normal file
185
osu.Game/Overlays/Mods/DifficultyMultiplierDisplay.cs
Normal file
@ -0,0 +1,185 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
public class DifficultyMultiplierDisplay : CompositeDrawable, IHasCurrentValue<double>
|
||||
{
|
||||
public Bindable<double> Current
|
||||
{
|
||||
get => current.Current;
|
||||
set => current.Current = value;
|
||||
}
|
||||
|
||||
private readonly BindableNumberWithCurrent<double> current = new BindableNumberWithCurrent<double>(1)
|
||||
{
|
||||
Precision = 0.01
|
||||
};
|
||||
|
||||
private readonly Box underlayBackground;
|
||||
private readonly Box contentBackground;
|
||||
private readonly FillFlowContainer multiplierFlow;
|
||||
private readonly MultiplierCounter multiplierCounter;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; }
|
||||
|
||||
private const float height = 42;
|
||||
private const float multiplier_value_area_width = 56;
|
||||
private const float transition_duration = 200;
|
||||
|
||||
public DifficultyMultiplierDisplay()
|
||||
{
|
||||
Height = height;
|
||||
AutoSizeAxes = Axes.X;
|
||||
|
||||
InternalChild = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Masking = true,
|
||||
CornerRadius = ModPanel.CORNER_RADIUS,
|
||||
Shear = new Vector2(ModPanel.SHEAR_X, 0),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
underlayBackground = new Box
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Width = multiplier_value_area_width + ModPanel.CORNER_RADIUS
|
||||
},
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.Absolute, multiplier_value_area_width)
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Masking = true,
|
||||
CornerRadius = ModPanel.CORNER_RADIUS,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
contentBackground = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Margin = new MarginPadding { Horizontal = 18 },
|
||||
Shear = new Vector2(-ModPanel.SHEAR_X, 0),
|
||||
Text = "Difficulty Multiplier",
|
||||
Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold)
|
||||
}
|
||||
}
|
||||
},
|
||||
multiplierFlow = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Shear = new Vector2(-ModPanel.SHEAR_X, 0),
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(2, 0),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
multiplierCounter = new MultiplierCounter
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Current = { BindTarget = Current }
|
||||
},
|
||||
new SpriteIcon
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Icon = FontAwesome.Solid.Times,
|
||||
Size = new Vector2(7),
|
||||
Margin = new MarginPadding { Top = 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
contentBackground.Colour = colourProvider.Background4;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
current.BindValueChanged(_ => updateState(), true);
|
||||
FinishTransforms(true);
|
||||
// required to prevent the counter initially rolling up from 0 to 1
|
||||
// due to `Current.Value` having a nonstandard default value of 1.
|
||||
multiplierCounter.SetCountWithoutRolling(Current.Value);
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
if (Current.IsDefault)
|
||||
{
|
||||
underlayBackground.FadeColour(colourProvider.Background3, transition_duration, Easing.OutQuint);
|
||||
multiplierFlow.FadeColour(Colour4.White, transition_duration, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
var backgroundColour = Current.Value < 1
|
||||
? colours.ForModType(ModType.DifficultyReduction)
|
||||
: colours.ForModType(ModType.DifficultyIncrease);
|
||||
|
||||
underlayBackground.FadeColour(backgroundColour, transition_duration, Easing.OutQuint);
|
||||
multiplierFlow.FadeColour(colourProvider.Background5, transition_duration, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
|
||||
private class MultiplierCounter : RollingCounter<double>
|
||||
{
|
||||
protected override double RollingDuration => 500;
|
||||
|
||||
protected override LocalisableString FormatCount(double count) => count.ToLocalisableString(@"N2");
|
||||
|
||||
protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -134,6 +134,9 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
|
||||
private void updateSelectedSkinFromConfig()
|
||||
{
|
||||
if (!skinDropdown.Items.Any())
|
||||
return;
|
||||
|
||||
Live<SkinInfo> skin = null;
|
||||
|
||||
if (Guid.TryParse(configBindable.Value, out var configId))
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using osu.Framework.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Scoring
|
||||
@ -16,6 +17,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// Indicates that the object has not been judged yet.
|
||||
/// </summary>
|
||||
[Description(@"")]
|
||||
[EnumMember(Value = "none")]
|
||||
[Order(14)]
|
||||
None,
|
||||
|
||||
@ -27,32 +29,39 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// "too far in the future). It should also define when a forced miss should be triggered (as a result of no user input in time).
|
||||
/// </remarks>
|
||||
[Description(@"Miss")]
|
||||
[EnumMember(Value = "miss")]
|
||||
[Order(5)]
|
||||
Miss,
|
||||
|
||||
[Description(@"Meh")]
|
||||
[EnumMember(Value = "meh")]
|
||||
[Order(4)]
|
||||
Meh,
|
||||
|
||||
[Description(@"OK")]
|
||||
[EnumMember(Value = "ok")]
|
||||
[Order(3)]
|
||||
Ok,
|
||||
|
||||
[Description(@"Good")]
|
||||
[EnumMember(Value = "good")]
|
||||
[Order(2)]
|
||||
Good,
|
||||
|
||||
[Description(@"Great")]
|
||||
[EnumMember(Value = "great")]
|
||||
[Order(1)]
|
||||
Great,
|
||||
|
||||
[Description(@"Perfect")]
|
||||
[EnumMember(Value = "perfect")]
|
||||
[Order(0)]
|
||||
Perfect,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates small tick miss.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "small_tick_miss")]
|
||||
[Order(11)]
|
||||
SmallTickMiss,
|
||||
|
||||
@ -60,12 +69,14 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// Indicates a small tick hit.
|
||||
/// </summary>
|
||||
[Description(@"S Tick")]
|
||||
[EnumMember(Value = "small_tick_hit")]
|
||||
[Order(7)]
|
||||
SmallTickHit,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates a large tick miss.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "large_tick_miss")]
|
||||
[Order(10)]
|
||||
LargeTickMiss,
|
||||
|
||||
@ -73,6 +84,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// Indicates a large tick hit.
|
||||
/// </summary>
|
||||
[Description(@"L Tick")]
|
||||
[EnumMember(Value = "large_tick_hit")]
|
||||
[Order(6)]
|
||||
LargeTickHit,
|
||||
|
||||
@ -80,6 +92,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// Indicates a small bonus.
|
||||
/// </summary>
|
||||
[Description("S Bonus")]
|
||||
[EnumMember(Value = "small_bonus")]
|
||||
[Order(9)]
|
||||
SmallBonus,
|
||||
|
||||
@ -87,18 +100,21 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// Indicates a large bonus.
|
||||
/// </summary>
|
||||
[Description("L Bonus")]
|
||||
[EnumMember(Value = "large_bonus")]
|
||||
[Order(8)]
|
||||
LargeBonus,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates a miss that should be ignored for scoring purposes.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "ignore_miss")]
|
||||
[Order(13)]
|
||||
IgnoreMiss,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates a hit that should be ignored for scoring purposes.
|
||||
/// </summary>
|
||||
[EnumMember(Value = "ignore_hit")]
|
||||
[Order(12)]
|
||||
IgnoreHit,
|
||||
}
|
||||
@ -133,6 +149,30 @@ namespace osu.Game.Rulesets.Scoring
|
||||
public static bool AffectsAccuracy(this HitResult result)
|
||||
=> IsScorable(result) && !IsBonus(result);
|
||||
|
||||
/// <summary>
|
||||
/// Whether a <see cref="HitResult"/> is a non-tick and non-bonus result.
|
||||
/// </summary>
|
||||
public static bool IsBasic(this HitResult result)
|
||||
=> IsScorable(result) && !IsTick(result) && !IsBonus(result);
|
||||
|
||||
/// <summary>
|
||||
/// Whether a <see cref="HitResult"/> should be counted as a tick.
|
||||
/// </summary>
|
||||
public static bool IsTick(this HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
case HitResult.LargeTickHit:
|
||||
case HitResult.LargeTickMiss:
|
||||
case HitResult.SmallTickHit:
|
||||
case HitResult.SmallTickMiss:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether a <see cref="HitResult"/> should be counted as bonus score.
|
||||
/// </summary>
|
||||
|
@ -132,7 +132,7 @@ namespace osu.Game.Scoring
|
||||
public async Task<long> GetTotalScoreAsync([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// TODO: This is required for playlist aggregate scores. They should likely not be getting here in the first place.
|
||||
if (string.IsNullOrEmpty(score.BeatmapInfo.Hash))
|
||||
if (string.IsNullOrEmpty(score.BeatmapInfo.MD5Hash))
|
||||
return score.TotalScore;
|
||||
|
||||
int beatmapMaxCombo;
|
||||
|
@ -191,6 +191,11 @@ namespace osu.Game.Screens.Select.Leaderboards
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
// 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.
|
||||
if (changes?.HasCollectionChanges() == false)
|
||||
return;
|
||||
|
||||
var scores = sender.AsEnumerable();
|
||||
|
||||
if (filterMods && !mods.Value.Any())
|
||||
|
@ -64,7 +64,7 @@ namespace osu.Game.Users.Drawables
|
||||
|
||||
private void openProfile()
|
||||
{
|
||||
if (user?.Id > 1)
|
||||
if (user?.Id > 1 || !string.IsNullOrEmpty(user?.Username))
|
||||
game?.ShowUser(user);
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user