-
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
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
base: master
Are you sure you want to change the base?
Conversation
[Resolved] | ||
private IBindable<RulesetInfo> ruleset { get; set; } = null!; |
There was a problem hiding this comment.
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".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#27128 (comment) is relevant.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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); | ||
} |
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
both
SoloStatisticsWatcher
andDifficultyRecommender
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.
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. |
@frenzibyte whenever you get back into the flow it might be good to revisit this one (and at least fix conflicts). |
…pendency optional
Should be addressed in 663b769 |
Tested changes against a local full stack, works as expected. |
…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.
As per the proposal in #27128 (comment), the branch has taken another round, making |
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
1b0a5f9
to
caf56af
Compare
/// <remarks> | ||
/// This returns null when accessed from <see cref="IAPIProvider.LocalUser"/>. Use <see cref="LocalUserStatisticsProvider"/> instead. | ||
/// </remarks> |
There was a problem hiding this comment.
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; } |
There was a problem hiding this comment.
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
Also no longer cancels previous API requests as there's no actual need to do it.
There was a problem hiding this 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
Will take one final look at this. |
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.