mirror of
https://github.com/osukey/osukey.git
synced 2025-05-17 11:37:32 +09: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.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Database;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
using Realms;
|
using Realms;
|
||||||
|
|
||||||
#nullable enable
|
|
||||||
|
|
||||||
namespace osu.Game.Tests.Database
|
namespace osu.Game.Tests.Database
|
||||||
{
|
{
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class RealmSubscriptionRegistrationTests : RealmTest
|
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]
|
[Test]
|
||||||
public void TestSubscriptionWithAsyncWrite()
|
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(rulesets = new RealmRulesetStore(Realm));
|
||||||
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
|
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
|
||||||
Dependencies.Cache(Realm);
|
Dependencies.Cache(Realm);
|
||||||
|
|
||||||
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
@ -52,8 +50,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
AvailabilityTracker.SelectedItem.BindTo(selectedItem);
|
AvailabilityTracker.SelectedItem.BindTo(selectedItem);
|
||||||
|
|
||||||
|
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||||
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
|
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
|
||||||
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
|
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
|
||||||
|
|
||||||
selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo)
|
selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo)
|
||||||
{
|
{
|
||||||
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
|
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
|
||||||
@ -92,16 +92,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
OsuButton readyButton = null;
|
OsuButton readyButton = null;
|
||||||
|
|
||||||
AddAssert("ensure ready button enabled", () =>
|
AddUntilStep("ensure ready button enabled", () =>
|
||||||
{
|
{
|
||||||
readyButton = button.ChildrenOfType<OsuButton>().Single();
|
readyButton = button.ChildrenOfType<OsuButton>().Single();
|
||||||
return readyButton.Enabled.Value;
|
return readyButton.Enabled.Value;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
|
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));
|
AddStep("undelete beatmap", () => beatmaps.Undelete(importedSet));
|
||||||
AddAssert("ready button enabled back", () => readyButton.Enabled.Value);
|
AddUntilStep("ready button enabled back", () => readyButton.Enabled.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
AddUntilStep("ensure manager loaded", () => beatmaps != null);
|
AddUntilStep("ensure manager loaded", () => beatmaps != null);
|
||||||
ensureSoleilyRemoved();
|
ensureSoleilyRemoved();
|
||||||
createButtonWithBeatmap(createSoleily());
|
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()));
|
AddStep("import soleily", () => beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()));
|
||||||
|
|
||||||
AddUntilStep("wait for beatmap import", () => beatmaps.GetAllUsableBeatmapSets().Any(b => b.OnlineID == 241526));
|
AddUntilStep("wait for beatmap import", () => beatmaps.GetAllUsableBeatmapSets().Any(b => b.OnlineID == 241526));
|
||||||
@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
createButtonWithBeatmap(createSoleily());
|
createButtonWithBeatmap(createSoleily());
|
||||||
AddUntilStep("button state downloaded", () => downloadButton.DownloadState == DownloadState.LocallyAvailable);
|
AddUntilStep("button state downloaded", () => downloadButton.DownloadState == DownloadState.LocallyAvailable);
|
||||||
ensureSoleilyRemoved();
|
ensureSoleilyRemoved();
|
||||||
AddAssert("button state not downloaded", () => downloadButton.DownloadState == DownloadState.NotDownloaded);
|
AddUntilStep("button state not downloaded", () => downloadButton.DownloadState == DownloadState.NotDownloaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ensureSoleilyRemoved()
|
private void ensureSoleilyRemoved()
|
||||||
|
@ -33,16 +33,25 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
|
|
||||||
private TestResultsScreen resultsScreen;
|
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 bool requestComplete;
|
||||||
private int totalCount;
|
private int totalCount;
|
||||||
|
private ScoreInfo userScore;
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void Setup() => Schedule(() =>
|
public void Setup() => Schedule(() =>
|
||||||
{
|
{
|
||||||
currentScoreId = 1;
|
lowestScoreId = 1;
|
||||||
|
highestScoreId = 1;
|
||||||
requestComplete = false;
|
requestComplete = false;
|
||||||
totalCount = 0;
|
totalCount = 0;
|
||||||
|
|
||||||
|
userScore = TestResources.CreateTestScoreInfo();
|
||||||
|
userScore.TotalScore = 0;
|
||||||
|
userScore.Statistics = new Dictionary<HitResult, int>();
|
||||||
|
|
||||||
bindHandler();
|
bindHandler();
|
||||||
|
|
||||||
// beatmap is required to be an actual beatmap so the scores can get their scores correctly calculated for standardised scoring.
|
// 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]
|
[Test]
|
||||||
public void TestShowWithUserScore()
|
public void TestShowWithUserScore()
|
||||||
{
|
{
|
||||||
ScoreInfo userScore = null;
|
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
|
||||||
|
|
||||||
AddStep("bind user score info handler", () =>
|
|
||||||
{
|
|
||||||
userScore = TestResources.CreateTestScoreInfo();
|
|
||||||
userScore.OnlineID = currentScoreId++;
|
|
||||||
|
|
||||||
bindHandler(userScore: userScore);
|
|
||||||
});
|
|
||||||
|
|
||||||
createResults(() => userScore);
|
createResults(() => userScore);
|
||||||
|
|
||||||
@ -81,15 +82,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestShowUserScoreWithDelay()
|
public void TestShowUserScoreWithDelay()
|
||||||
{
|
{
|
||||||
ScoreInfo userScore = null;
|
AddStep("bind user score info handler", () => bindHandler(true, userScore));
|
||||||
|
|
||||||
AddStep("bind user score info handler", () =>
|
|
||||||
{
|
|
||||||
userScore = TestResources.CreateTestScoreInfo();
|
|
||||||
userScore.OnlineID = currentScoreId++;
|
|
||||||
|
|
||||||
bindHandler(true, userScore);
|
|
||||||
});
|
|
||||||
|
|
||||||
createResults(() => userScore);
|
createResults(() => userScore);
|
||||||
|
|
||||||
@ -124,7 +117,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
|
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
|
||||||
waitForDisplay();
|
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);
|
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -132,15 +125,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestFetchWhenScrolledToTheLeft()
|
public void TestFetchWhenScrolledToTheLeft()
|
||||||
{
|
{
|
||||||
ScoreInfo userScore = null;
|
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
|
||||||
|
|
||||||
AddStep("bind user score info handler", () =>
|
|
||||||
{
|
|
||||||
userScore = TestResources.CreateTestScoreInfo();
|
|
||||||
userScore.OnlineID = currentScoreId++;
|
|
||||||
|
|
||||||
bindHandler(userScore: userScore);
|
|
||||||
});
|
|
||||||
|
|
||||||
createResults(() => userScore);
|
createResults(() => userScore);
|
||||||
|
|
||||||
@ -156,7 +141,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible);
|
AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible);
|
||||||
waitForDisplay();
|
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);
|
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
|
var multiplayerUserScore = new MultiplayerScore
|
||||||
{
|
{
|
||||||
ID = (int)(userScore.OnlineID > 0 ? userScore.OnlineID : currentScoreId++),
|
ID = highestScoreId,
|
||||||
Accuracy = userScore.Accuracy,
|
Accuracy = userScore.Accuracy,
|
||||||
EndedAt = userScore.Date,
|
|
||||||
Passed = userScore.Passed,
|
Passed = userScore.Passed,
|
||||||
Rank = userScore.Rank,
|
Rank = userScore.Rank,
|
||||||
Position = real_user_position,
|
Position = real_user_position,
|
||||||
MaxCombo = userScore.MaxCombo,
|
MaxCombo = userScore.MaxCombo,
|
||||||
TotalScore = userScore.TotalScore,
|
|
||||||
User = userScore.User,
|
User = userScore.User,
|
||||||
Statistics = userScore.Statistics,
|
|
||||||
ScoresAround = new MultiplayerScoresAround
|
ScoresAround = new MultiplayerScoresAround
|
||||||
{
|
{
|
||||||
Higher = new MultiplayerScores(),
|
Higher = new MultiplayerScores(),
|
||||||
@ -268,38 +250,32 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
{
|
{
|
||||||
multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore
|
multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore
|
||||||
{
|
{
|
||||||
ID = currentScoreId++,
|
ID = --highestScoreId,
|
||||||
Accuracy = userScore.Accuracy,
|
Accuracy = userScore.Accuracy,
|
||||||
EndedAt = userScore.Date,
|
|
||||||
Passed = true,
|
Passed = true,
|
||||||
Rank = userScore.Rank,
|
Rank = userScore.Rank,
|
||||||
MaxCombo = userScore.MaxCombo,
|
MaxCombo = userScore.MaxCombo,
|
||||||
TotalScore = userScore.TotalScore - i,
|
|
||||||
User = new APIUser
|
User = new APIUser
|
||||||
{
|
{
|
||||||
Id = 2,
|
Id = 2,
|
||||||
Username = $"peppy{i}",
|
Username = $"peppy{i}",
|
||||||
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||||
},
|
},
|
||||||
Statistics = userScore.Statistics
|
|
||||||
});
|
});
|
||||||
|
|
||||||
multiplayerUserScore.ScoresAround.Higher.Scores.Add(new MultiplayerScore
|
multiplayerUserScore.ScoresAround.Higher.Scores.Add(new MultiplayerScore
|
||||||
{
|
{
|
||||||
ID = currentScoreId++,
|
ID = ++lowestScoreId,
|
||||||
Accuracy = userScore.Accuracy,
|
Accuracy = userScore.Accuracy,
|
||||||
EndedAt = userScore.Date,
|
|
||||||
Passed = true,
|
Passed = true,
|
||||||
Rank = userScore.Rank,
|
Rank = userScore.Rank,
|
||||||
MaxCombo = userScore.MaxCombo,
|
MaxCombo = userScore.MaxCombo,
|
||||||
TotalScore = userScore.TotalScore + i,
|
|
||||||
User = new APIUser
|
User = new APIUser
|
||||||
{
|
{
|
||||||
Id = 2,
|
Id = 2,
|
||||||
Username = $"peppy{i}",
|
Username = $"peppy{i}",
|
||||||
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||||
},
|
},
|
||||||
Statistics = userScore.Statistics
|
|
||||||
});
|
});
|
||||||
|
|
||||||
totalCount += 2;
|
totalCount += 2;
|
||||||
@ -315,33 +291,23 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
{
|
{
|
||||||
var result = new IndexedMultiplayerScores();
|
var result = new IndexedMultiplayerScores();
|
||||||
|
|
||||||
long startTotalScore = req.Cursor?.Properties["total_score"].ToObject<long>() ?? 1000000;
|
|
||||||
string sort = req.IndexParams?.Properties["sort"].ToObject<string>() ?? "score_desc";
|
string sort = req.IndexParams?.Properties["sort"].ToObject<string>() ?? "score_desc";
|
||||||
|
|
||||||
for (int i = 1; i <= scores_per_result; i++)
|
for (int i = 1; i <= scores_per_result; i++)
|
||||||
{
|
{
|
||||||
result.Scores.Add(new MultiplayerScore
|
result.Scores.Add(new MultiplayerScore
|
||||||
{
|
{
|
||||||
ID = currentScoreId++,
|
ID = sort == "score_asc" ? --highestScoreId : ++lowestScoreId,
|
||||||
Accuracy = 1,
|
Accuracy = 1,
|
||||||
EndedAt = DateTimeOffset.Now,
|
|
||||||
Passed = true,
|
Passed = true,
|
||||||
Rank = ScoreRank.X,
|
Rank = ScoreRank.X,
|
||||||
MaxCombo = 1000,
|
MaxCombo = 1000,
|
||||||
TotalScore = startTotalScore + (sort == "score_asc" ? i : -i),
|
|
||||||
User = new APIUser
|
User = new APIUser
|
||||||
{
|
{
|
||||||
Id = 2,
|
Id = 2,
|
||||||
Username = $"peppy{i}",
|
Username = $"peppy{i}",
|
||||||
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
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++;
|
totalCount++;
|
||||||
@ -367,7 +333,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
|||||||
{
|
{
|
||||||
Properties = new Dictionary<string, JToken>
|
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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Models;
|
using osu.Game.Models;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.Ranking;
|
using osu.Game.Screens.Ranking;
|
||||||
using osu.Game.Tests.Resources;
|
using osu.Game.Tests.Resources;
|
||||||
@ -208,13 +210,19 @@ namespace osu.Game.Tests.Visual.Ranking
|
|||||||
public void TestKeyboardNavigation()
|
public void TestKeyboardNavigation()
|
||||||
{
|
{
|
||||||
var lowestScore = TestResources.CreateTestScoreInfo();
|
var lowestScore = TestResources.CreateTestScoreInfo();
|
||||||
lowestScore.MaxCombo = 100;
|
lowestScore.OnlineID = 3;
|
||||||
|
lowestScore.TotalScore = 0;
|
||||||
|
lowestScore.Statistics = new Dictionary<HitResult, int>();
|
||||||
|
|
||||||
var middleScore = TestResources.CreateTestScoreInfo();
|
var middleScore = TestResources.CreateTestScoreInfo();
|
||||||
middleScore.MaxCombo = 200;
|
middleScore.OnlineID = 2;
|
||||||
|
middleScore.TotalScore = 0;
|
||||||
|
middleScore.Statistics = new Dictionary<HitResult, int>();
|
||||||
|
|
||||||
var highestScore = TestResources.CreateTestScoreInfo();
|
var highestScore = TestResources.CreateTestScoreInfo();
|
||||||
highestScore.MaxCombo = 300;
|
highestScore.OnlineID = 1;
|
||||||
|
highestScore.TotalScore = 0;
|
||||||
|
highestScore.Statistics = new Dictionary<HitResult, int>();
|
||||||
|
|
||||||
createListStep(() => new ScorePanelList());
|
createListStep(() => new ScorePanelList());
|
||||||
|
|
||||||
|
@ -284,14 +284,13 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
public void TestDummy()
|
public void TestDummy()
|
||||||
{
|
{
|
||||||
createSongSelect();
|
createSongSelect();
|
||||||
AddAssert("dummy selected", () => songSelect.CurrentBeatmap == defaultBeatmap);
|
AddUntilStep("dummy selected", () => songSelect.CurrentBeatmap == defaultBeatmap);
|
||||||
|
|
||||||
AddUntilStep("dummy shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap == defaultBeatmap);
|
AddUntilStep("dummy shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap == defaultBeatmap);
|
||||||
|
|
||||||
addManyTestMaps();
|
addManyTestMaps();
|
||||||
AddWaitStep("wait for select", 3);
|
|
||||||
|
|
||||||
AddAssert("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap);
|
AddUntilStep("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -299,9 +298,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
{
|
{
|
||||||
createSongSelect();
|
createSongSelect();
|
||||||
addManyTestMaps();
|
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 Artist", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist));
|
||||||
AddStep(@"Sort by Title", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title));
|
AddStep(@"Sort by Title", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title));
|
||||||
@ -571,6 +569,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
createSongSelect();
|
createSongSelect();
|
||||||
|
|
||||||
|
AddUntilStep("wait for selection", () => !Beatmap.IsDefault);
|
||||||
|
|
||||||
AddStep("press ctrl+enter", () =>
|
AddStep("press ctrl+enter", () =>
|
||||||
{
|
{
|
||||||
InputManager.PressKey(Key.ControlLeft);
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
@ -605,6 +605,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
addRulesetImportStep(0);
|
addRulesetImportStep(0);
|
||||||
createSongSelect();
|
createSongSelect();
|
||||||
|
|
||||||
|
AddUntilStep("wait for selection", () => !Beatmap.IsDefault);
|
||||||
|
|
||||||
DrawableCarouselBeatmapSet set = null;
|
DrawableCarouselBeatmapSet set = null;
|
||||||
AddStep("Find the DrawableCarouselBeatmapSet", () =>
|
AddStep("Find the DrawableCarouselBeatmapSet", () =>
|
||||||
{
|
{
|
||||||
@ -844,6 +846,8 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
createSongSelect();
|
createSongSelect();
|
||||||
|
|
||||||
|
AddUntilStep("wait for selection", () => !Beatmap.IsDefault);
|
||||||
|
|
||||||
AddStep("present score", () =>
|
AddStep("present score", () =>
|
||||||
{
|
{
|
||||||
// this beatmap change should be overridden by the present.
|
// this beatmap change should be overridden by the present.
|
||||||
|
@ -124,7 +124,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("Became present", () => topLocalRank.IsPresent);
|
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", () =>
|
AddStep("Add higher score for current user", () =>
|
||||||
{
|
{
|
||||||
@ -137,7 +137,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
scoreManager.Import(testScoreInfo2);
|
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();
|
public Realm Realm => ensureUpdateRealm();
|
||||||
|
|
||||||
|
private const string realm_extension = @".realm";
|
||||||
|
|
||||||
private Realm ensureUpdateRealm()
|
private Realm ensureUpdateRealm()
|
||||||
{
|
{
|
||||||
if (isSendingNotificationResetEvents)
|
if (isSendingNotificationResetEvents)
|
||||||
@ -149,11 +151,18 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
Filename = filename;
|
Filename = filename;
|
||||||
|
|
||||||
const string realm_extension = @".realm";
|
|
||||||
|
|
||||||
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
|
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
|
||||||
Filename += realm_extension;
|
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
|
try
|
||||||
{
|
{
|
||||||
// This method triggers the first `getRealmInstance` call, which will implicitly run realm migrations and bring the schema up-to-date.
|
// 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)
|
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.");
|
||||||
|
|
||||||
|
// 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}");
|
CreateBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_corrupt{realm_extension}");
|
||||||
storage.Delete(Filename);
|
storage.Delete(Filename);
|
||||||
|
}
|
||||||
|
|
||||||
cleanupPendingDeletions();
|
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()
|
private void cleanupPendingDeletions()
|
||||||
{
|
{
|
||||||
using (var realm = getRealmInstance())
|
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.
|
// 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).
|
// 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))
|
if (!Directory.Exists(tempPathLocation))
|
||||||
Directory.CreateDirectory(tempPathLocation);
|
Directory.CreateDirectory(tempPathLocation);
|
||||||
|
|
||||||
return new RealmConfiguration(storage.GetFullPath(Filename, true))
|
return new RealmConfiguration(storage.GetFullPath(filename ?? Filename, true))
|
||||||
{
|
{
|
||||||
SchemaVersion = schema_version,
|
SchemaVersion = schema_version,
|
||||||
MigrationCallback = onMigration,
|
MigrationCallback = onMigration,
|
||||||
|
@ -4,6 +4,8 @@
|
|||||||
using System;
|
using System;
|
||||||
using Realms;
|
using Realms;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
namespace osu.Game.Database
|
namespace osu.Game.Database
|
||||||
{
|
{
|
||||||
public static class RealmExtensions
|
public static class RealmExtensions
|
||||||
@ -22,5 +24,14 @@ namespace osu.Game.Database
|
|||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
return result;
|
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")]
|
[JsonProperty("mods")]
|
||||||
public APIMod[] Mods { get; set; }
|
public APIMod[] Mods { get; set; }
|
||||||
|
|
||||||
[JsonProperty("user")]
|
|
||||||
public APIUser User { get; set; }
|
|
||||||
|
|
||||||
[JsonProperty("statistics")]
|
[JsonProperty("statistics")]
|
||||||
public Dictionary<HitResult, int> Statistics { get; set; }
|
public Dictionary<HitResult, int> Statistics { get; set; }
|
||||||
|
|
||||||
@ -67,7 +64,6 @@ namespace osu.Game.Online.Solo
|
|||||||
RulesetID = score.RulesetID;
|
RulesetID = score.RulesetID;
|
||||||
Passed = score.Passed;
|
Passed = score.Passed;
|
||||||
Mods = score.APIMods;
|
Mods = score.APIMods;
|
||||||
User = score.User;
|
|
||||||
Statistics = score.Statistics;
|
Statistics = score.Statistics;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,7 +79,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
|||||||
var beatmapInfo = new BeatmapInfo
|
var beatmapInfo = new BeatmapInfo
|
||||||
{
|
{
|
||||||
MaxCombo = apiBeatmap.MaxCombo,
|
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)
|
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()
|
private void updateSelectedSkinFromConfig()
|
||||||
{
|
{
|
||||||
|
if (!skinDropdown.Items.Any())
|
||||||
|
return;
|
||||||
|
|
||||||
Live<SkinInfo> skin = null;
|
Live<SkinInfo> skin = null;
|
||||||
|
|
||||||
if (Guid.TryParse(configBindable.Value, out var configId))
|
if (Guid.TryParse(configBindable.Value, out var configId))
|
||||||
|
@ -5,6 +5,7 @@ using System;
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Runtime.Serialization;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Scoring
|
namespace osu.Game.Rulesets.Scoring
|
||||||
@ -16,6 +17,7 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
/// Indicates that the object has not been judged yet.
|
/// Indicates that the object has not been judged yet.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Description(@"")]
|
[Description(@"")]
|
||||||
|
[EnumMember(Value = "none")]
|
||||||
[Order(14)]
|
[Order(14)]
|
||||||
None,
|
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).
|
/// "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>
|
/// </remarks>
|
||||||
[Description(@"Miss")]
|
[Description(@"Miss")]
|
||||||
|
[EnumMember(Value = "miss")]
|
||||||
[Order(5)]
|
[Order(5)]
|
||||||
Miss,
|
Miss,
|
||||||
|
|
||||||
[Description(@"Meh")]
|
[Description(@"Meh")]
|
||||||
|
[EnumMember(Value = "meh")]
|
||||||
[Order(4)]
|
[Order(4)]
|
||||||
Meh,
|
Meh,
|
||||||
|
|
||||||
[Description(@"OK")]
|
[Description(@"OK")]
|
||||||
|
[EnumMember(Value = "ok")]
|
||||||
[Order(3)]
|
[Order(3)]
|
||||||
Ok,
|
Ok,
|
||||||
|
|
||||||
[Description(@"Good")]
|
[Description(@"Good")]
|
||||||
|
[EnumMember(Value = "good")]
|
||||||
[Order(2)]
|
[Order(2)]
|
||||||
Good,
|
Good,
|
||||||
|
|
||||||
[Description(@"Great")]
|
[Description(@"Great")]
|
||||||
|
[EnumMember(Value = "great")]
|
||||||
[Order(1)]
|
[Order(1)]
|
||||||
Great,
|
Great,
|
||||||
|
|
||||||
[Description(@"Perfect")]
|
[Description(@"Perfect")]
|
||||||
|
[EnumMember(Value = "perfect")]
|
||||||
[Order(0)]
|
[Order(0)]
|
||||||
Perfect,
|
Perfect,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates small tick miss.
|
/// Indicates small tick miss.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[EnumMember(Value = "small_tick_miss")]
|
||||||
[Order(11)]
|
[Order(11)]
|
||||||
SmallTickMiss,
|
SmallTickMiss,
|
||||||
|
|
||||||
@ -60,12 +69,14 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
/// Indicates a small tick hit.
|
/// Indicates a small tick hit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Description(@"S Tick")]
|
[Description(@"S Tick")]
|
||||||
|
[EnumMember(Value = "small_tick_hit")]
|
||||||
[Order(7)]
|
[Order(7)]
|
||||||
SmallTickHit,
|
SmallTickHit,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates a large tick miss.
|
/// Indicates a large tick miss.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[EnumMember(Value = "large_tick_miss")]
|
||||||
[Order(10)]
|
[Order(10)]
|
||||||
LargeTickMiss,
|
LargeTickMiss,
|
||||||
|
|
||||||
@ -73,6 +84,7 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
/// Indicates a large tick hit.
|
/// Indicates a large tick hit.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Description(@"L Tick")]
|
[Description(@"L Tick")]
|
||||||
|
[EnumMember(Value = "large_tick_hit")]
|
||||||
[Order(6)]
|
[Order(6)]
|
||||||
LargeTickHit,
|
LargeTickHit,
|
||||||
|
|
||||||
@ -80,6 +92,7 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
/// Indicates a small bonus.
|
/// Indicates a small bonus.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Description("S Bonus")]
|
[Description("S Bonus")]
|
||||||
|
[EnumMember(Value = "small_bonus")]
|
||||||
[Order(9)]
|
[Order(9)]
|
||||||
SmallBonus,
|
SmallBonus,
|
||||||
|
|
||||||
@ -87,18 +100,21 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
/// Indicates a large bonus.
|
/// Indicates a large bonus.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Description("L Bonus")]
|
[Description("L Bonus")]
|
||||||
|
[EnumMember(Value = "large_bonus")]
|
||||||
[Order(8)]
|
[Order(8)]
|
||||||
LargeBonus,
|
LargeBonus,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates a miss that should be ignored for scoring purposes.
|
/// Indicates a miss that should be ignored for scoring purposes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[EnumMember(Value = "ignore_miss")]
|
||||||
[Order(13)]
|
[Order(13)]
|
||||||
IgnoreMiss,
|
IgnoreMiss,
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates a hit that should be ignored for scoring purposes.
|
/// Indicates a hit that should be ignored for scoring purposes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[EnumMember(Value = "ignore_hit")]
|
||||||
[Order(12)]
|
[Order(12)]
|
||||||
IgnoreHit,
|
IgnoreHit,
|
||||||
}
|
}
|
||||||
@ -133,6 +149,30 @@ namespace osu.Game.Rulesets.Scoring
|
|||||||
public static bool AffectsAccuracy(this HitResult result)
|
public static bool AffectsAccuracy(this HitResult result)
|
||||||
=> IsScorable(result) && !IsBonus(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>
|
/// <summary>
|
||||||
/// Whether a <see cref="HitResult"/> should be counted as bonus score.
|
/// Whether a <see cref="HitResult"/> should be counted as bonus score.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -132,7 +132,7 @@ namespace osu.Game.Scoring
|
|||||||
public async Task<long> GetTotalScoreAsync([NotNull] ScoreInfo score, ScoringMode mode = ScoringMode.Standardised, CancellationToken cancellationToken = default)
|
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.
|
// 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;
|
return score.TotalScore;
|
||||||
|
|
||||||
int beatmapMaxCombo;
|
int beatmapMaxCombo;
|
||||||
|
@ -191,6 +191,11 @@ namespace osu.Game.Screens.Select.Leaderboards
|
|||||||
if (cancellationToken.IsCancellationRequested)
|
if (cancellationToken.IsCancellationRequested)
|
||||||
return;
|
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();
|
var scores = sender.AsEnumerable();
|
||||||
|
|
||||||
if (filterMods && !mods.Value.Any())
|
if (filterMods && !mods.Value.Any())
|
||||||
|
@ -64,7 +64,7 @@ namespace osu.Game.Users.Drawables
|
|||||||
|
|
||||||
private void openProfile()
|
private void openProfile()
|
||||||
{
|
{
|
||||||
if (user?.Id > 1)
|
if (user?.Id > 1 || !string.IsNullOrEmpty(user?.Username))
|
||||||
game?.ShowUser(user);
|
game?.ShowUser(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user