1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 15:22:55 +08:00

Merge branch 'master' into Make-deletion-confirmation-less-confusing

This commit is contained in:
Dean Herbert 2024-05-09 14:40:11 +08:00 committed by GitHub
commit 58636d4bfb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 155 additions and 13 deletions

View File

@ -14,6 +14,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Legacy;
using osu.Game.IO.Legacy;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
@ -31,6 +32,7 @@ using osu.Game.Rulesets.Taiko;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Tests.Resources;
using osu.Game.Users;
namespace osu.Game.Tests.Beatmaps.Formats
{
@ -224,6 +226,12 @@ namespace osu.Game.Tests.Beatmaps.Formats
new OsuModDoubleTime { SpeedChange = { Value = 1.1 } }
};
scoreInfo.OnlineID = 123123;
scoreInfo.User = new APIUser
{
Username = "spaceman_atlas",
Id = 3035836,
CountryCode = CountryCode.PL
};
scoreInfo.ClientVersion = "2023.1221.0";
var beatmap = new TestBeatmap(ruleset);
@ -248,6 +256,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(decodedAfterEncode.ScoreInfo.MaximumStatistics, Is.EqualTo(scoreInfo.MaximumStatistics));
Assert.That(decodedAfterEncode.ScoreInfo.Mods, Is.EqualTo(scoreInfo.Mods));
Assert.That(decodedAfterEncode.ScoreInfo.ClientVersion, Is.EqualTo("2023.1221.0"));
Assert.That(decodedAfterEncode.ScoreInfo.RealmUser.OnlineID, Is.EqualTo(3035836));
});
}

View File

@ -287,7 +287,7 @@ namespace osu.Game.Tests.Scores.IO
}
[Test]
public void TestUserLookedUpForOnlineScore()
public void TestUserLookedUpByUsernameForOnlineScoreIfUserIDMissing()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
@ -301,6 +301,9 @@ namespace osu.Game.Tests.Scores.IO
switch (req)
{
case GetUserRequest userRequest:
if (userRequest.Lookup != "Test user")
return false;
userRequest.TriggerSuccess(new APIUser
{
Username = "Test user",
@ -350,7 +353,7 @@ namespace osu.Game.Tests.Scores.IO
}
[Test]
public void TestUserLookedUpForLegacyOnlineScore()
public void TestUserLookedUpByUsernameForLegacyOnlineScore()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
@ -364,6 +367,9 @@ namespace osu.Game.Tests.Scores.IO
switch (req)
{
case GetUserRequest userRequest:
if (userRequest.Lookup != "Test user")
return false;
userRequest.TriggerSuccess(new APIUser
{
Username = "Test user",
@ -413,7 +419,7 @@ namespace osu.Game.Tests.Scores.IO
}
[Test]
public void TestUserNotLookedUpForOfflineScore()
public void TestUserNotLookedUpForOfflineScoreIfUserIDMissing()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
@ -427,6 +433,9 @@ namespace osu.Game.Tests.Scores.IO
switch (req)
{
case GetUserRequest userRequest:
if (userRequest.Lookup != "Test user")
return false;
userRequest.TriggerSuccess(new APIUser
{
Username = "Test user",
@ -476,6 +485,73 @@ namespace osu.Game.Tests.Scores.IO
}
}
[Test]
public void TestUserLookedUpByOnlineIDIfPresent([Values] bool isOnlineScore)
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = LoadOsuIntoHost(host, true);
var api = (DummyAPIAccess)osu.API;
api.HandleRequest = req =>
{
switch (req)
{
case GetUserRequest userRequest:
if (userRequest.Lookup != "5555")
return false;
userRequest.TriggerSuccess(new APIUser
{
Username = "Some other guy",
CountryCode = CountryCode.DE,
Id = 5555
});
return true;
default:
return false;
}
};
var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
var toImport = new ScoreInfo
{
Rank = ScoreRank.B,
TotalScore = 987654,
Accuracy = 0.8,
MaxCombo = 500,
Combo = 250,
User = new APIUser { Id = 5555 },
Date = DateTimeOffset.Now,
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapInfo = beatmap.Beatmaps.First()
};
if (isOnlineScore)
toImport.OnlineID = 12345;
var imported = LoadScoreIntoOsu(osu, toImport);
Assert.AreEqual(toImport.Rank, imported.Rank);
Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
Assert.AreEqual(toImport.Accuracy, imported.Accuracy);
Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo);
Assert.AreEqual(toImport.Date, imported.Date);
Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
Assert.AreEqual("Some other guy", imported.RealmUser.Username);
Assert.AreEqual(5555, imported.RealmUser.OnlineID);
Assert.AreEqual(CountryCode.DE, imported.RealmUser.CountryCode);
}
finally
{
host.Exit();
}
}
}
public static ScoreInfo LoadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null)
{
// clone to avoid attaching the input score to realm.

View File

@ -43,6 +43,9 @@ namespace osu.Game.Scoring.Legacy
[JsonConverter(typeof(StringEnumConverter))]
public ScoreRank? Rank;
[JsonProperty("user_id")]
public int UserID = -1;
public static LegacyReplaySoloScoreInfo FromScore(ScoreInfo score) => new LegacyReplaySoloScoreInfo
{
OnlineID = score.OnlineID,
@ -51,6 +54,7 @@ namespace osu.Game.Scoring.Legacy
MaximumStatistics = score.MaximumStatistics.Where(kvp => kvp.Value != 0).ToDictionary(),
ClientVersion = score.ClientVersion,
Rank = score.Rank,
UserID = score.User.OnlineID,
};
}
}

View File

@ -131,6 +131,8 @@ namespace osu.Game.Scoring.Legacy
score.ScoreInfo.Mods = readScore.Mods.Select(m => m.ToMod(currentRuleset)).ToArray();
score.ScoreInfo.ClientVersion = readScore.ClientVersion;
decodedRank = readScore.Rank;
if (readScore.UserID > 1)
score.ScoreInfo.RealmUser.OnlineID = readScore.UserID;
});
}
}

View File

@ -103,6 +103,14 @@ namespace osu.Game.Scoring
}
// Very naive local caching to improve performance of large score imports (where the username is usually the same for most or all scores).
// TODO: `UserLookupCache` cannot currently be used here because of async foibles.
// It only supports lookups by user ID (username would require web changes), and even then the ID lookups cannot be used.
// That is because that component provides an async interface, and async functions cannot be consumed safely here due to the rigid structure of `RealmArchiveModelImporter`.
// The importer has two paths, one async and one sync; the async path runs the sync path in a task.
// This means that sometimes `PostImport()` is called from a sync context, and sometimes from an async one, whilst itself being a sync method.
// That in turn makes `.GetResultSafely()` not callable inside `PostImport()`, as it will throw when called from an async context,
private readonly Dictionary<int, APIUser> idLookupCache = new Dictionary<int, APIUser>();
private readonly Dictionary<string, APIUser> usernameLookupCache = new Dictionary<string, APIUser>();
protected override void PostImport(ScoreInfo model, Realm realm, ImportParameters parameters)
@ -127,24 +135,34 @@ namespace osu.Game.Scoring
if (model.RealmUser.OnlineID == APIUser.SYSTEM_USER_ID)
return;
if (model.OnlineID < 0 && model.LegacyOnlineID <= 0)
return;
string username = model.RealmUser.Username;
if (usernameLookupCache.TryGetValue(username, out var existing))
if (model.RealmUser.OnlineID > 1)
{
model.User = existing;
model.User = lookupUserById(model.RealmUser.OnlineID) ?? model.User;
return;
}
var userRequest = new GetUserRequest(username);
if (model.OnlineID < 0 && model.LegacyOnlineID <= 0)
return;
model.User = lookupUserByName(model.RealmUser.Username) ?? model.User;
}
private APIUser? lookupUserById(int id)
{
if (idLookupCache.TryGetValue(id, out var existing))
{
return existing;
}
var userRequest = new GetUserRequest(id);
api.Perform(userRequest);
if (userRequest.Response is APIUser user)
{
usernameLookupCache.TryAdd(username, new APIUser
APIUser cachedUser;
idLookupCache.TryAdd(id, cachedUser = new APIUser
{
// Because this is a permanent cache, let's only store the pieces we're interested in,
// rather than the full API response. If we start to store more than these three fields
@ -154,8 +172,41 @@ namespace osu.Game.Scoring
CountryCode = user.CountryCode,
});
model.User = user;
return cachedUser;
}
return null;
}
private APIUser? lookupUserByName(string username)
{
if (usernameLookupCache.TryGetValue(username, out var existing))
{
return existing;
}
var userRequest = new GetUserRequest(username);
api.Perform(userRequest);
if (userRequest.Response is APIUser user)
{
APIUser cachedUser;
usernameLookupCache.TryAdd(username, cachedUser = new APIUser
{
// Because this is a permanent cache, let's only store the pieces we're interested in,
// rather than the full API response. If we start to store more than these three fields
// in realm, this should be undone.
Id = user.Id,
Username = user.Username,
CountryCode = user.CountryCode,
});
return cachedUser;
}
return null;
}
}
}