Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce UserStatisticsProvider component and add support for respecting selected ruleset #27128

Open
wants to merge 26 commits into
base: master
Choose a base branch
from

Conversation

frenzibyte
Copy link
Member

@frenzibyte frenzibyte commented Feb 11, 2024

I was initially leaning towards rewriting SoloStatisticsWatcher to become a general-purpose component surrounding around providing user statistics, but after brief discussion yesterday, I've settled on making a separate component for that instead, to keep complexity of each component to a minimum.

osu.Game/OsuGameBase.cs Outdated Show resolved Hide resolved
Comment on lines 28 to 29
[Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't get what's with the insistence of having this bindable. i'd rather it didn't exist and this component didn't do any "ruleset tracking".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#27128 (comment) is relevant.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really think that comment is relevant. The question here is more of an architectural one no? Like you can still cache the per-ruleset statistics, but instead of exposing the ruleset-specific bindable it would be up to the consumers (DiscordRichPresence and UserRankPanel).

That said, I think I'm okay with it existing as it is. At least until a use case comes up where we want to retrieve this for something that isn't the current global ruleset. Which arguable exists – the single usage you've piped through GetStatisticsFor. Is there a reason this flow doesn't do the online retrieval?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will say it does feel weird having this exist alongside UserStatisticsWatcher with data being awkwardly ferried between the two. Like they are both basically doing the same thing and the cross-communication is only required to keep the new component in sync?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the sake of making each component have not but a single purpose, LocalUserStatisticsProvider and UserStatisticsWatcher have been split. The first exposes local user statistics to the game codebase, while the second has a very specific purpose tied to score submission which is to listen to submission events and display a change in user statistics while updating the former component with the new statistics.

I can understand that there's still a level of awkwardness between the two components since they both look up statistics online, and I wouldn't mind merging them together, but I would have to hear @bdach's opinion on that since he doesn't like that to be the case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm more focused on the fact that you added cross-talk between them, for the sake of saving one request I guess? If we're keeping both, I'd see that being removed.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cross talk must exist for score submission to update the statistics exposed by LocalUserStatisticsProvider, the only way to avoid that is merging both components together.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bdach Can I get your five minutes attention on the current state of this PR and make sure you're still happy with the direction as it stands? The "required" cross-talk here makes things more complicated than I'd hope, and while I haven't looked into whether this can be improved or removed, I'd like your take on it.

Copy link
Collaborator

@bdach bdach Nov 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My position on this is unchanged: I would like the bindable to be gone.

The reasoning is extremely simple: when faced between two choices, one of which introduces ambient global state, and one of which does not, always choose the one that does not introduce the ambient global state unless you absolutely have to. It is simpler.

As I see it, LocalUserStatisticsProvider should be a dumb lookup facilitator / a cache. Same way something like DifficultyCache is. It should expose a dictionary for anyone to use in any way they see fit. Keyed by (userId, rulesetId). Maybe it could even expose bindables, like DifficultyCache does, if you need that.

As for the interaction with UserStatisticsWatcher - LocalUserStatisticsProvider should expose a method to invalidate and refetch data for a given cache key, and UserStatisticsWatcher should use that to retrieve its rank update.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, will apply as proposed.

Comment on lines 79 to 84
private void requestStatistics(RulesetInfo ruleset)
{
currentRequest = new GetUserRequest(api.LocalUser.Value.OnlineID, ruleset);
currentRequest.Success += u => statistics.Value = allStatistics[ruleset.ShortName] = u.Statistics;
api.Queue(currentRequest);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both SoloStatisticsWatcher and DifficultyRecommender do initial population of statistics for their own uses for all rulesets. so why is this doing the same in a third place?

this is why i was floating potentially exposing the internal dictionary from SoloStatisticsWatcher.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not emphasise too much about this, but I have mentioned in OP the following:

This could be improved by fetching statistics of all rulesets once the user is logged in, but it would require performing four consecutive GetUserRequests for each ruleset to provide the statistics in full detail (i.e. when performing a GetUsersRequest, the RulesetsStatistics dictionary does not provide information about the country ranking in each ruleset).

SoloStatisticsWatcher is initialising user statistics for all rulesets using a GetUsersRequest, the response of that request does not include country ranking and potentially other attributes.

If anything, I would foresee this new component exposing statistics for all rulesets by a method or a dictionary, and DifficultyRecommender turning into an extension method, and SoloStatisticsWatcher becoming completely dependent on LocalUserStatisticsProvider and do away with the API request logic in the watcher.

However, right now it's sort-of not pretty to achieve that, as I have to make the component perform one-time four consecutive API requests to receive full user statistics on each ruleset, and in complete honestly, I have no idea whether this is something that you would be fine with or not.

Copy link
Collaborator

@bdach bdach Feb 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at this point i'd probably ask if it is possible for us to receive full statistics in a single request because this feels wasteful.

@ppy/team-web do you think we could change some user-fetching endpoint to return complete rank data for all rulesets in one request, including country ranking?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

country ranking is rather expensive

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

both SoloStatisticsWatcher and DifficultyRecommender do initial population of statistics for their own uses for all rulesets. so why is this doing the same in a third place?

this is why i was floating potentially exposing the internal dictionary from SoloStatisticsWatcher.

Looking back at this, since the purpose of LocalUserStatisticsProvider is to track latest statistics and provide the latest one, I've made UserStatisticsWatcher fully rely on it and remove its local part of fetching initial statistics and tracking statistics in general.

@Joehuu
Copy link
Member

Joehuu commented Aug 29, 2024

Whenever this gets updated again, it should also be applied to discord rich presence as the rank is only fetched at game start / login and never updated.

@peppy
Copy link
Member

peppy commented Sep 30, 2024

@frenzibyte whenever you get back into the flow it might be good to revisit this one (and at least fix conflicts).

@frenzibyte
Copy link
Member Author

Whenever this gets updated again, it should also be applied to discord rich presence as the rank is only fetched at game start / login and never updated.

Should be addressed in 663b769

@frenzibyte
Copy link
Member Author

Tested changes against a local full stack, works as expected.

@peppy peppy self-requested a review November 13, 2024 06:19
peppy and others added 5 commits November 13, 2024 15:21
…Provider`

This also throws away the logic of updating
`API.LocalUser.Value.Statistics`. Components should rely on
`LocalUserStatisticsProvider` instead for proper behaviour and ability
to update on statistics updates.
@frenzibyte
Copy link
Member Author

As per the proposal in #27128 (comment), the branch has taken another round, making LocalUserStatisticsProvider completely independent of the game-wide ruleset bindable's state, and acting purely as a cache for user statistics per ruleset.

Nothing behaviourally different, just reduce number of redundant calls.
/// A bindable communicating updates to the local user's statistics on any ruleset.
/// This does not guarantee the presence of old statistics, as it is invoked on initial population of statistics.
/// </summary>
public IBindable<UserStatisticsUpdate> StatisticsUpdate => statisticsUpdate;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may seem odd at first but it's a comfortable method of communicating user statistics updates to any consuming component without going through the boring method of binding to an event, creating a callback function, and unbinding on disposal.

It's also shown to be directly useful for DifficultyRecommender to update its state with any update to any ruleset.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure @bdach's whole point was to not have this, but you've still got it present. Am I missing something.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it is about the component not tracking changes to the game-wide ruleset bindable. This bindable's main and sole purpose is to let any component be aware if a statistics has changed as a result of user logging out and logging in or submitting a new score. It is more of an event than a bindable but it's written as a bindable to make it easier to use rather than potentially fall in memory leak caused by not unsubscribing from the event.

Copy link
Member

@peppy peppy Nov 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just make it an event if it's not accurate content wise...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@@ -9,7 +9,7 @@ namespace osu.Game.Online
/// <summary>
/// Contains data about the change in a user's profile statistics after completing a score.
/// </summary>
public class UserStatisticsUpdate
public class ScoreBasedUserStatisticsUpdate
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit on the iffy side and it may not sit well with everyone, but with LocalUserStatisticsProvider.StatisticsUpdate requiring a model to contain change to user statistics on a specific ruleset, this class needed to be renamed to something else. The other option would be to pick a different name for the StatisticsUpdate model, but I think renaming this model is better since it is focused on scores anyway.

osu.Game/Online/UserStatisticsWatcher.cs Outdated Show resolved Hide resolved
Comment on lines +227 to +229
/// <remarks>
/// This returns null when accessed from <see cref="IAPIProvider.LocalUser"/>. Use <see cref="LocalUserStatisticsProvider"/> instead.
/// </remarks>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh how i love this model class (heavy sarcasm)

/// <summary>
/// The current user's online statistics.
/// </summary>
IBindable<UserStatistics?> Statistics { get; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For one, very happy to see this gone from here

Copy link
Collaborator

@bdach bdach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added one precautionary guard (0a3f3c3) but otherwise seems fine

@peppy
Copy link
Member

peppy commented Nov 25, 2024

Will take one final look at this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Rank on player card don't show the other rulesets
5 participants