1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-22 00:47:24 +08:00

Merge branch 'master' into ui/segmented-graph

This commit is contained in:
Dean Herbert 2023-01-12 19:02:49 +09:00 committed by GitHub
commit 123c477e00
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
87 changed files with 1099 additions and 543 deletions

View File

@ -2,137 +2,87 @@
Thank you for showing interest in the development of osu!. We aim to provide a good collaborating environment for everyone involved, and as such have decided to list some of the most important things to keep in mind in the process. The guidelines below have been chosen based on past experience.
These are not "official rules" *per se*, but following them will help everyone deal with things in the most efficient manner.
## Table of contents
1. [I would like to submit an issue!](#i-would-like-to-submit-an-issue)
2. [I would like to submit a pull request!](#i-would-like-to-submit-a-pull-request)
1. [Reporting bugs](#reporting-bugs)
2. [Providing general feedback](#providing-general-feedback)
3. [Issue or discussion?](#issue-or-discussion)
4. [Submitting pull requests](#submitting-pull-requests)
5. [Resources](#resources)
## I would like to submit an issue!
## Reporting bugs
Issues, bug reports and feature suggestions are welcomed, though please keep in mind that at any point in time, hundreds of issues are open, which vary in severity and the amount of time needed to address them. As such it's not uncommon for issues to remain unresolved for a long time or even closed outright if they are deemed not important enough to fix in the foreseeable future. Issues that are required to "go live" or otherwise achieve parity with stable are prioritised the most.
A **bug** is a situation in which there is something clearly *and objectively* wrong with the game. Examples of applicable bug reports are:
* **Before submitting an issue, try searching existing issues first.**
- The game crashes to desktop when I start a beatmap
- Friends appear twice in the friend listing
- The game slows down a lot when I play this specific map
- A piece of text is overlapping another piece of text on the screen
For housekeeping purposes, we close issues that overlap with or duplicate other pre-existing issues - you can help us not to have to do that by searching existing issues yourself first. The issue search box, as well as the issue tag system, are tools you can use to check if an issue has been reported before.
To track bug reports, we primarily use GitHub **issues**. When opening an issue, please keep in mind the following:
* **When submitting a bug report, please try to include as much detail as possible.**
- Before opening the issue, please search for any similar existing issues using the text search bar and the issue labels. This includes both open and closed issues (we may have already fixed something, but the fix hasn't yet been released).
- When opening the issue, please fill out as much of the issue template as you can. In particular, please make sure to include logs and screenshots as much as possible. The instructions on how to find the log files are included in the issue template.
- We may ask you for follow-up information to reproduce or debug the problem. Please look out for this and provide follow-up info if we request it.
Bugs are not equal - some of them will be reproducible every time on pretty much all hardware, while others will be hard to track down due to being specific to particular hardware or even somewhat random in nature. As such, providing as much detail as possible when reporting a bug is hugely appreciated. A good starting set of information consists of:
If we cannot reproduce the issue, it is deemed low priority, or it is deemed to be specific to your setup in some way, the issue may be downgraded to a discussion. This will be done by a maintainer for you.
* the in-game logs, which are located at:
* `%AppData%/osu/logs` (on Windows),
* `~/.local/share/osu/logs` (on Linux),
* `~/Library/Application Support/osu/logs` (on macOS),
* `Android/data/sh.ppy.osulazer/files/logs` (on Android),
* on iOS they can be obtained by connecting your device to your desktop and [copying the `logs` directory from the app's own document storage using iTunes](https://support.apple.com/en-us/HT201301#copy-to-computer),
* your system specifications (including the operating system and platform you are playing on),
* a reproduction scenario (list of steps you have performed leading up to the occurrence of the bug),
* a video or picture of the bug, if at all possible.
## Providing general feedback
* **Provide more information when asked to do so.**
If you wish to:
Sometimes when a bug is more elusive or complicated, none of the information listed above will pinpoint a concrete cause of the problem. In this case we will most likely ask you for additional info, such as a Windows Event Log dump or a copy of your local osu! database (`client.db`). Providing that information is beneficial to both parties - we can track down the problem better, and hopefully fix it for you at some point once we know where it is!
- provide *subjective* feedback on the game (about how the UI looks, about how the default skin works, about game mechanics, about how the PP and scoring systems work, etc.),
- suggest a new feature to be added to the game,
- report a non-specific problem with the game that you think may be connected to your hardware or operating system specifically,
* **When submitting a feature proposal, please describe it in the most understandable way you can.**
then it is generally best to start with a **discussion** first. Discussions are a good avenue to group subjective feedback on a single topic, or gauge interest in a particular feature request.
Communicating your idea for a feature can often be hard, and we would like to avoid any misunderstandings. As such, please try to explain your idea in a short, but understandable manner - it's best to avoid jargon or terms and references that could be considered obscure. A mock-up picture (doesn't have to be good!) of the feature can also go a long way in explaining.
When opening a discussion, please keep in mind the following:
* **Refrain from posting "+1" comments.**
- Use the search function to see if your idea has been proposed before, or if there is already a thread about a particular issue you wish to raise.
- If proposing a feature, please try to explain the feature in as much detail as possible.
- If you're reporting a non-specific problem, please provide applicable logs, screenshots, or video that illustrate the issue.
If an issue has already been created, saying that you also experience it without providing any additional details doesn't really help us in any way. To express support for a proposal or indicate that you are also affected by a particular bug, you can use comment reactions instead.
If a discussion gathers enough traction, then it may be converted into an issue. This will be done by a maintainer for you.
* **Refrain from asking if an issue has been resolved yet.**
## Issue or discussion?
As mentioned above, the issue tracker has hundreds of issues open at any given time. Currently the game is being worked on by two members of the core team, and a handful of outside contributors who offer their free time to help out. As such, it can happen that an issue gets placed on the backburner due to being less important; generally posting a comment demanding its resolution some months or years after it is reported is not very likely to increase its priority.
We realise that the line between an issue and a discussion may be fuzzy, so while we ask you to use your best judgement based on the description above, please don't think about it too hard either. Feedback in a slightly wrong place is better than no feedback at all.
* **Avoid long discussions about non-development topics.**
When in doubt, it's probably best to start with a discussion first. We will escalate to issues as needed.
GitHub is mostly a developer space, and as such isn't really fit for lengthened discussions about gameplay mechanics (which might not even be in any way confirmed for the final release) and similar non-technical matters. Such matters are probably best addressed at the osu! forums.
## Submitting pull requests
## I would like to submit a pull request!
While pull requests from unaffiliated contributors are welcome, please note that due to significant community interest and limited review throughput, the core team's primary focus is on the issues which are currently [on the roadmap](https://github.com/orgs/ppy/projects/7/views/6). Reviewing PRs that fall outside of the scope of the roadmap is done on a best-effort basis, so please be aware that it may take a while before a core maintainer gets around to review your change.
We also welcome pull requests from unaffiliated contributors. The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues that you can work on; we also mark issues that we think would be good for newcomers with the [`good-first-issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue) label.
The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. We also have a [`good-first-issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue) label, although from experience it is not used very often, as it is relatively rare that we can spot an issue that will definitively be a good first issue for a new contributor regardless of their programming experience.
However, do keep in mind that the core team is committed to bringing osu!(lazer) up to par with osu!(stable) first and foremost, so depending on what your contribution concerns, it might not be merged and released right away. Our approach to managing issues and their priorities is described [in the wiki](https://github.com/ppy/osu/wiki/Project-management).
In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive.
Here are some key things to note before jumping in:
If you'd like to propose a subjective change to one of the visual aspects of the game, or there is a bigger task you'd like to work on, but there is no corresponding issue or discussion thread yet for it, **please open a discussion or issue first** to avoid wasted effort. This in particular applies if you want to work on [one of the available designs from the osu! public Figma library](https://www.figma.com/file/6m10GiGEncVFWmgOoSyakH/osu!-Figma-Library).
* **Make sure you are comfortable with C\# and your development environment.**
Aside from the above, below is a brief checklist of things to watch out when you're preparing your code changes:
While we are accepting of all kinds of contributions, we also have a certain quality standard we'd like to uphold and limited time to review your code. Therefore, we would like to avoid providing entry-level advice, and as such if you're not very familiar with C\# as a programming language, we'd recommend that you start off with a few personal projects to get acquainted with the language's syntax, toolchain and principles of object-oriented programming first.
- Make sure you're comfortable with the principles of object-oriented programming, the syntax of C\# and your development environment.
- Make sure you are familiar with [git](https://git-scm.com/) and [the pull request workflow](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/proposing-changes-to-your-work-with-pull-requests).
- Please do not make code changes via the GitHub web interface.
- Please add tests for your changes. We expect most new features and bugfixes to have test coverage, unless the effort of adding them is prohibitive. The visual testing methodology we use is described in more detail [here](https://github.com/ppy/osu-framework/wiki/Development-and-Testing).
- Please run tests and code style analysis (via `InspectCode.{ps1,sh}` scripts in the root of this repository) before opening the PR. This is particularly important if you're a first-time contributor, as CI will not run for your PR until we allow it to do so.
In addition, please take the time to take a look at and get acquainted with the [development and testing](https://github.com/ppy/osu-framework/wiki/Development-and-Testing) procedure we have set up.
After you're done with your changes and you wish to open the PR, please observe the following recommendations:
* **Make sure you are familiar with git and the pull request workflow.**
- Please submit the pull request from a [topic branch](https://git-scm.com/book/en/v2/Git-Branching-Branching-Workflows#_topic_branch) (not `master`), and keep the *Allow edits from maintainers* check box selected, so that we can push fixes to your PR if necessary.
- Please avoid pushing untested or incomplete code.
- Please do not force-push or rebase unless we ask you to.
- Please do not merge `master` continually if there are no conflicts to resolve. We will do this for you when the change is ready for merge.
[git](https://git-scm.com/) is a distributed version control system that might not be very intuitive at the beginning if you're not familiar with version control. In particular, projects using git have a particular workflow for submitting code changes, which is called the pull request workflow.
We are highly committed to quality when it comes to the osu! project. This means that contributions from less experienced community members can take multiple rounds of review to get to a mergeable state. We try our utmost best to never conflate a person with the code they authored, and to keep the discussion focused on the code at all times. Please consider our comments and requests a learning experience.
To make things run more smoothly, we recommend that you look up some online resources to familiarise yourself with the git vocabulary and commands, and practice working with forks and submitting pull requests at your own pace. A high-level overview of the process can be found in [this article by GitHub](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/proposing-changes-to-your-work-with-pull-requests).
If you're uncertain about some part of the codebase or some inner workings of the game and framework, please reach out either by leaving a comment in the relevant issue, discussion, or PR thread, or by posting a message in the [development Discord server](https://discord.gg/ppy). We will try to help you as much as we can.
* **Double-check designs before starting work on new functionality.**
## Resources
When implementing new features, keep in mind that we already have a lot of the UI designed. If you wish to work on something with the intention of having it included in the official distribution, please open an issue for discussion and we will give you what you need from a design perspective to proceed. If you want to make *changes* to the design, we recommend you open an issue with your intentions before spending too much time to ensure no effort is wasted.
* **Make sure to submit pull requests off of a topic branch.**
As described in the article linked in the previous point, topic branches help you parallelise your work and separate it from the main `master` branch, and additionally are easier for maintainers to work with. Working with multiple `master` branches across many remotes is difficult to keep track of, and it's easy to make a mistake and push to the wrong `master` branch by accident.
* **Refrain from making changes through the GitHub web interface.**
Even though GitHub provides an option to edit code or replace files in the repository using the web interface, we strongly discourage using it in most scenarios. Editing files this way is inefficient and likely to introduce whitespace or file encoding changes that make it more difficult to review the code.
Code written through the web interface will also very likely be questioned outright by the reviewers, as it is likely that it has not been properly tested or that it will fail continuous integration checks. We strongly encourage using an IDE like [Visual Studio](https://visualstudio.microsoft.com/), [Visual Studio Code](https://code.visualstudio.com/) or [JetBrains Rider](https://www.jetbrains.com/rider/) instead.
* **Add tests for your code whenever possible.**
Automated tests are an essential part of a quality and reliable codebase. They help to make the code more maintainable by ensuring it is safe to reorganise (or refactor) the code in various ways, and also prevent regressions - bugs that resurface after having been fixed at some point in the past. If it is viable, please put in the time to add tests, so that the changes you make can last for a (hopefully) very long time.
* **Run tests before opening a pull request.**
Tying into the previous point, sometimes changes in one part of the codebase can result in unpredictable changes in behaviour in other pieces of the code. This is why it is best to always try to run tests before opening a PR.
Continuous integration will always run the tests for you (and us), too, but it is best not to rely on it, as there might be many builds queued at any time. Running tests on your own will help you be more certain that at the point of clicking the "Create pull request" button, your changes are as ready as can be.
* **Run code style analysis before opening a pull request.**
As part of continuous integration, we also run code style analysis, which is supposed to make sure that your code is formatted the same way as all the pre-existing code in the repository. The reason we enforce a particular code style everywhere is to make sure the codebase is consistent in that regard - having one whitespace convention in one place and another one elsewhere causes disorganisation.
* **Make sure that the pull request is complete before opening it.**
Whether it's fixing a bug or implementing new functionality, it's best that you make sure that the change you want to submit as a pull request is as complete as it can be before clicking the *Create pull request* button. Having to track if a pull request is ready for review or not places additional burden on reviewers.
Draft pull requests are an option, but use them sparingly and within reason. They are best suited to discuss code changes that cannot be easily described in natural language or have a potential large impact on the future direction of the project. When in doubt, don't open drafts unless a maintainer asks you to do so.
* **Only push code when it's ready.**
As an extension of the above, when making changes to an already-open PR, please try to only push changes you are reasonably certain of. Pushing after every commit causes the continuous integration build queue to grow in size, slowing down work and taking up time that could be spent verifying other changes.
* **Make sure to keep the *Allow edits from maintainers* check box checked.**
To speed up the merging process, collaborators and team members will sometimes want to push changes to your branch themselves, to make minor code style adjustments or to otherwise refactor the code without having to describe how they'd like the code to look like in painstaking detail. Having the *Allow edits from maintainers* check box checked lets them do that; without it they are forced to report issues back to you and wait for you to address them.
* **Refrain from continually merging the master branch back to the PR.**
Unless there are merge conflicts that need resolution, there is no need to keep merging `master` back to a branch over and over again. One of the maintainers will merge `master` themselves before merging the PR itself anyway, and continual merge commits can cause CI to get overwhelmed due to queueing up too many builds.
* **Refrain from force-pushing to the PR branch.**
Force-pushing should be avoided, as it can lead to accidentally overwriting a maintainer's changes or CI building wrong commits. We value all history in the project, so there is no need to squash or amend commits in most cases.
The cases in which force-pushing is warranted are very rare (such as accidentally leaking sensitive info in one of the files committed, adding unrelated files, or mis-merging a dependent PR).
* **Be patient when waiting for the code to be reviewed and merged.**
As much as we'd like to review all contributions as fast as possible, our time is limited, as team members have to work on their own tasks in addition to reviewing code. As such, work needs to be prioritised, and it can unfortunately take weeks or months for your PR to be merged, depending on how important it is deemed to be.
* **Don't mistake criticism of code for criticism of your person.**
As mentioned before, we are highly committed to quality when it comes to the osu! project. This means that contributions from less experienced community members can take multiple rounds of review to get to a mergeable state. We try our utmost best to never conflate a person with the code they authored, and to keep the discussion focused on the code at all times. Please consider our comments and requests a learning experience, and don't treat it as a personal attack.
* **Feel free to reach out for help.**
If you're uncertain about some part of the codebase or some inner workings of the game and framework, please reach out either by leaving a comment in the relevant issue or PR thread, or by posting a message in the [development Discord server](https://discord.gg/ppy). We will try to help you as much as we can.
When it comes to which form of communication is best, GitHub generally lends better to longer-form discussions, while Discord is better for snappy call-and-response answers. Use your best discretion when deciding, and try to keep a single discussion in one place instead of moving back and forth.
- [Development roadmap](https://github.com/orgs/ppy/projects/7/views/6): What the core team is currently working on
- [`ppy/osu-framework` wiki](https://github.com/ppy/osu-framework/wiki): Contains introductory information about osu!framework, the bespoke 2D game framework we use for the game
- [`ppy/osu` wiki](https://github.com/ppy/osu/wiki): Contains articles about various technical aspects of the game
- [Public Figma library](https://www.figma.com/file/6m10GiGEncVFWmgOoSyakH/osu!-Figma-Library): Contains finished and draft designs for osu!

View File

@ -101,9 +101,7 @@ JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it
## Contributing
When it comes to contributing to the project, the two main things you can do to help out are reporting issues and submitting pull requests. Based on past experiences, we have prepared a [list of contributing guidelines](CONTRIBUTING.md) that should hopefully ease you into our collaboration process and answer the most frequently-asked questions.
Note that while we already have certain standards in place, nothing is set in stone. If you have an issue with the way code is structured, with any libraries we are using, or with any processes involved with contributing, *please* bring it up. We welcome all feedback so we can make contributing to this project as painless as possible.
When it comes to contributing to the project, the two main things you can do to help out are reporting issues and submitting pull requests. Please refer to the [contributing guidelines](CONTRIBUTING.md) to understand how to help in the most effective way possible.
If you wish to help with localisation efforts, head over to [crowdin](https://crowdin.com/project/osu-web).

View File

@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Mania
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new ManiaHealthProcessor(drainStartTime, 0.5);
public override HealthProcessor CreateHealthProcessor(double drainStartTime) => new ManiaHealthProcessor(drainStartTime);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this);

View File

@ -11,8 +11,8 @@ namespace osu.Game.Rulesets.Mania.Scoring
public partial class ManiaHealthProcessor : DrainingHealthProcessor
{
/// <inheritdoc/>
public ManiaHealthProcessor(double drainStartTime, double drainLenience = 0)
: base(drainStartTime, drainLenience)
public ManiaHealthProcessor(double drainStartTime)
: base(drainStartTime, 1.0)
{
}

View File

@ -1,6 +1,7 @@
// 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 System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
@ -29,8 +30,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private readonly bool hasNumber;
protected Drawable CircleSprite = null!;
protected Drawable OverlaySprite = null!;
protected LegacyKiaiFlashingDrawable CircleSprite = null!;
protected LegacyKiaiFlashingDrawable OverlaySprite = null!;
protected Container OverlayLayer { get; private set; } = null!;
@ -65,7 +66,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
// at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it.
// the conditional above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist.
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png.
InternalChildren = new[]
{
CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) })
@ -114,7 +114,21 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
base.LoadComplete();
accentColour.BindValueChanged(colour => CircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
accentColour.BindValueChanged(colour =>
{
Color4 objectColour = colour.NewValue;
int add = Math.Max(25, 300 - (int)(objectColour.R * 255) - (int)(objectColour.G * 255) - (int)(objectColour.B * 255));
var kiaiTintColour = new Color4(
(byte)Math.Min((byte)(objectColour.R * 255) + add, 255),
(byte)Math.Min((byte)(objectColour.G * 255) + add, 255),
(byte)Math.Min((byte)(objectColour.B * 255) + add, 255),
255);
CircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue);
OverlaySprite.KiaiGlowColour = CircleSprite.KiaiGlowColour = LegacyColourCompatibility.DisallowZeroAlpha(kiaiTintColour);
}, true);
if (hasNumber)
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);

View File

@ -42,7 +42,9 @@ namespace osu.Game.Tests.Skins
// Covers longest combo counter
"Archives/modified-default-20221012.osk",
// Covers TextElement and BeatmapInfoDrawable
"Archives/modified-default-20221102.osk"
"Archives/modified-default-20221102.osk",
// Covers BPM counter.
"Archives/modified-default-20221205.osk"
};
/// <summary>

View File

@ -27,6 +27,7 @@ using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
@ -80,7 +81,25 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for return to playlist screen", () => playlistScreen.CurrentSubScreen is PlaylistsRoomSubScreen);
AddStep("go back to song select", () =>
{
InputManager.MoveMouseTo(playlistScreen.ChildrenOfType<PurpleRoundedButton>().Single(b => b.Text == "Edit playlist"));
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for song select", () => (playlistScreen.CurrentSubScreen as PlaylistsSongSelect)?.BeatmapSetsLoaded == true);
AddStep("press home button", () =>
{
InputManager.MoveMouseTo(Game.Toolbar.ChildrenOfType<ToolbarHomeButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddAssert("confirmation dialog shown", () => Game.ChildrenOfType<DialogOverlay>().Single().CurrentDialog is not null);
pushEscape();
pushEscape();
AddAssert("confirmation dialog shown", () => Game.ChildrenOfType<DialogOverlay>().Single().CurrentDialog is not null);
AddStep("confirm exit", () => InputManager.Key(Key.Enter));

View File

@ -14,6 +14,8 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapSet.Scores;
using osu.Game.Resources.Localisation.Web;
@ -241,6 +243,44 @@ namespace osu.Game.Tests.Visual.Online
AddStep(@"show without reload", overlay.Show);
}
[TestCase(BeatmapSetLookupType.BeatmapId)]
[TestCase(BeatmapSetLookupType.SetId)]
public void TestFetchLookupType(BeatmapSetLookupType lookupType)
{
string type = string.Empty;
AddStep("register request handling", () =>
{
((DummyAPIAccess)API).HandleRequest = req =>
{
switch (req)
{
case GetBeatmapSetRequest getBeatmapSet:
type = getBeatmapSet.Type.ToString();
return true;
}
return false;
};
});
AddStep(@"fetch", () =>
{
switch (lookupType)
{
case BeatmapSetLookupType.BeatmapId:
overlay.FetchAndShowBeatmap(55);
break;
case BeatmapSetLookupType.SetId:
overlay.FetchAndShowBeatmapSet(55);
break;
}
});
AddAssert(@"type is correct", () => type == lookupType.ToString());
}
private APIBeatmapSet createManyDifficultiesBeatmapSet()
{
var set = getBeatmapSet();

View File

@ -9,7 +9,9 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Profile;
using osu.Game.Overlays.Profile.Sections;
using osu.Game.Rulesets.Osu;
namespace osu.Game.Tests.Visual.Online
{
@ -37,8 +39,8 @@ namespace osu.Game.Tests.Visual.Online
Child = section = new HistoricalSection(),
});
AddStep("Show peppy", () => section.User.Value = new APIUser { Id = 2 });
AddStep("Show WubWoofWolf", () => section.User.Value = new APIUser { Id = 39828 });
AddStep("Show peppy", () => section.User.Value = new UserProfileData(new APIUser { Id = 2 }, new OsuRuleset().RulesetInfo));
AddStep("Show WubWoofWolf", () => section.User.Value = new UserProfileData(new APIUser { Id = 39828 }, new OsuRuleset().RulesetInfo));
}
}
}

View File

@ -1,8 +1,6 @@
// 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 disable
using osu.Game.Overlays.Profile.Sections.Historical;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -14,6 +12,8 @@ using System.Linq;
using osu.Framework.Testing;
using osu.Framework.Graphics.Shapes;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Profile;
using osu.Game.Rulesets.Osu;
namespace osu.Game.Tests.Visual.Online
{
@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.Online
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Red);
private readonly Bindable<APIUser> user = new Bindable<APIUser>();
private readonly Bindable<UserProfileData?> user = new Bindable<UserProfileData?>();
private readonly PlayHistorySubsection section;
public TestScenePlayHistorySubsection()
@ -45,49 +45,49 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestNullValues()
{
AddStep("Load user", () => user.Value = user_with_null_values);
AddStep("Load user", () => user.Value = new UserProfileData(user_with_null_values, new OsuRuleset().RulesetInfo));
AddAssert("Section is hidden", () => section.Alpha == 0);
}
[Test]
public void TestEmptyValues()
{
AddStep("Load user", () => user.Value = user_with_empty_values);
AddStep("Load user", () => user.Value = new UserProfileData(user_with_empty_values, new OsuRuleset().RulesetInfo));
AddAssert("Section is hidden", () => section.Alpha == 0);
}
[Test]
public void TestOneValue()
{
AddStep("Load user", () => user.Value = user_with_one_value);
AddStep("Load user", () => user.Value = new UserProfileData(user_with_one_value, new OsuRuleset().RulesetInfo));
AddAssert("Section is hidden", () => section.Alpha == 0);
}
[Test]
public void TestTwoValues()
{
AddStep("Load user", () => user.Value = user_with_two_values);
AddStep("Load user", () => user.Value = new UserProfileData(user_with_two_values, new OsuRuleset().RulesetInfo));
AddAssert("Section is visible", () => section.Alpha == 1);
}
[Test]
public void TestConstantValues()
{
AddStep("Load user", () => user.Value = user_with_constant_values);
AddStep("Load user", () => user.Value = new UserProfileData(user_with_constant_values, new OsuRuleset().RulesetInfo));
AddAssert("Section is visible", () => section.Alpha == 1);
}
[Test]
public void TestConstantZeroValues()
{
AddStep("Load user", () => user.Value = user_with_zero_values);
AddStep("Load user", () => user.Value = new UserProfileData(user_with_zero_values, new OsuRuleset().RulesetInfo));
AddAssert("Section is visible", () => section.Alpha == 1);
}
[Test]
public void TestFilledValues()
{
AddStep("Load user", () => user.Value = user_with_filled_values);
AddStep("Load user", () => user.Value = new UserProfileData(user_with_filled_values, new OsuRuleset().RulesetInfo));
AddAssert("Section is visible", () => section.Alpha == 1);
AddAssert("Array length is the same", () => user_with_filled_values.MonthlyPlayCounts.Length == getChartValuesLength());
}
@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestMissingValues()
{
AddStep("Load user", () => user.Value = user_with_missing_values);
AddStep("Load user", () => user.Value = new UserProfileData(user_with_missing_values, new OsuRuleset().RulesetInfo));
AddAssert("Section is visible", () => section.Alpha == 1);
AddAssert("Array length is 7", () => getChartValuesLength() == 7);
}

View File

@ -11,6 +11,7 @@ using osu.Framework.Bindables;
using osu.Game.Overlays;
using osu.Framework.Allocation;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Profile;
namespace osu.Game.Tests.Visual.Online
{
@ -21,25 +22,24 @@ namespace osu.Game.Tests.Visual.Online
public TestSceneProfileRulesetSelector()
{
ProfileRulesetSelector selector;
var user = new Bindable<APIUser?>();
var user = new Bindable<UserProfileData?>();
Child = selector = new ProfileRulesetSelector
Child = new ProfileRulesetSelector
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
User = { BindTarget = user }
};
AddStep("User on osu ruleset", () => user.Value = new UserProfileData(new APIUser { Id = 0, PlayMode = "osu" }, new OsuRuleset().RulesetInfo));
AddStep("User on taiko ruleset", () => user.Value = new UserProfileData(new APIUser { Id = 1, PlayMode = "osu" }, new TaikoRuleset().RulesetInfo));
AddStep("User on catch ruleset", () => user.Value = new UserProfileData(new APIUser { Id = 2, PlayMode = "osu" }, new CatchRuleset().RulesetInfo));
AddStep("User on mania ruleset", () => user.Value = new UserProfileData(new APIUser { Id = 3, PlayMode = "osu" }, new ManiaRuleset().RulesetInfo));
AddStep("set osu! as default", () => selector.SetDefaultRuleset(new OsuRuleset().RulesetInfo));
AddStep("set taiko as default", () => selector.SetDefaultRuleset(new TaikoRuleset().RulesetInfo));
AddStep("set catch as default", () => selector.SetDefaultRuleset(new CatchRuleset().RulesetInfo));
AddStep("set mania as default", () => selector.SetDefaultRuleset(new ManiaRuleset().RulesetInfo));
AddStep("User with osu as default", () => user.Value = new UserProfileData(new APIUser { Id = 0, PlayMode = "osu" }, new OsuRuleset().RulesetInfo));
AddStep("User with taiko as default", () => user.Value = new UserProfileData(new APIUser { Id = 1, PlayMode = "taiko" }, new OsuRuleset().RulesetInfo));
AddStep("User with catch as default", () => user.Value = new UserProfileData(new APIUser { Id = 2, PlayMode = "fruits" }, new OsuRuleset().RulesetInfo));
AddStep("User with mania as default", () => user.Value = new UserProfileData(new APIUser { Id = 3, PlayMode = "mania" }, new OsuRuleset().RulesetInfo));
AddStep("User with osu as default", () => user.Value = new APIUser { Id = 0, PlayMode = "osu" });
AddStep("User with taiko as default", () => user.Value = new APIUser { Id = 1, PlayMode = "taiko" });
AddStep("User with catch as default", () => user.Value = new APIUser { Id = 2, PlayMode = "fruits" });
AddStep("User with mania as default", () => user.Value = new APIUser { Id = 3, PlayMode = "mania" });
AddStep("null user", () => user.Value = null);
}
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Testing;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Profile;
using osu.Game.Rulesets.Osu;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Online
@ -29,36 +30,37 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestBasic()
{
AddStep("Show example user", () => header.User.Value = TestSceneUserProfileOverlay.TEST_USER);
AddStep("Show example user", () => header.User.Value = new UserProfileData(TestSceneUserProfileOverlay.TEST_USER, new OsuRuleset().RulesetInfo));
}
[Test]
public void TestOnlineState()
{
AddStep("Show online user", () => header.User.Value = new APIUser
AddStep("Show online user", () => header.User.Value = new UserProfileData(new APIUser
{
Id = 1001,
Username = "IAmOnline",
LastVisit = DateTimeOffset.Now,
IsOnline = true,
});
}, new OsuRuleset().RulesetInfo));
AddStep("Show offline user", () => header.User.Value = new APIUser
AddStep("Show offline user", () => header.User.Value = new UserProfileData(new APIUser
{
Id = 1002,
Username = "IAmOffline",
LastVisit = DateTimeOffset.Now.AddDays(-10),
IsOnline = false,
});
}, new OsuRuleset().RulesetInfo));
}
[Test]
public void TestRankedState()
{
AddStep("Show ranked user", () => header.User.Value = new APIUser
AddStep("Show ranked user", () => header.User.Value = new UserProfileData(new APIUser
{
Id = 2001,
Username = "RankedUser",
Groups = new[] { new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" } },
Statistics = new UserStatistics
{
IsRanked = true,
@ -70,9 +72,9 @@ namespace osu.Game.Tests.Visual.Online
Data = Enumerable.Range(2345, 45).Concat(Enumerable.Range(2109, 40)).ToArray()
},
}
});
}, new OsuRuleset().RulesetInfo));
AddStep("Show unranked user", () => header.User.Value = new APIUser
AddStep("Show unranked user", () => header.User.Value = new UserProfileData(new APIUser
{
Id = 2002,
Username = "UnrankedUser",
@ -86,7 +88,7 @@ namespace osu.Game.Tests.Visual.Online
Data = Enumerable.Range(2345, 85).ToArray()
},
}
});
}, new OsuRuleset().RulesetInfo));
}
}
}

View File

@ -4,9 +4,12 @@
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Profile;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Online
@ -14,9 +17,66 @@ namespace osu.Game.Tests.Visual.Online
[TestFixture]
public partial class TestSceneUserProfileOverlay : OsuTestScene
{
protected override bool UseOnlineAPI => true;
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
private readonly TestUserProfileOverlay profile;
private UserProfileOverlay profile = null!;
[SetUpSteps]
public void SetUp()
{
AddStep("create profile overlay", () => Child = profile = new UserProfileOverlay());
}
[Test]
public void TestBlank()
{
AddStep("show overlay", () => profile.Show());
}
[Test]
public void TestActualUser()
{
AddStep("set up request handling", () =>
{
dummyAPI.HandleRequest = req =>
{
if (req is GetUserRequest getUserRequest)
{
getUserRequest.TriggerSuccess(TEST_USER);
return true;
}
return false;
};
});
AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 }));
AddToggleStep("toggle visibility", visible => profile.State.Value = visible ? Visibility.Visible : Visibility.Hidden);
AddStep("log out", () => dummyAPI.Logout());
AddStep("log back in", () => dummyAPI.Login("username", "password"));
}
[Test]
public void TestLoading()
{
GetUserRequest pendingRequest = null!;
AddStep("set up request handling", () =>
{
dummyAPI.HandleRequest = req =>
{
if (req is GetUserRequest getUserRequest)
{
pendingRequest = getUserRequest;
return true;
}
return false;
};
});
AddStep("show user", () => profile.ShowUser(new APIUser { Id = 1 }));
AddWaitStep("wait some", 3);
AddStep("complete request", () => pendingRequest.TriggerSuccess(TEST_USER));
}
public static readonly APIUser TEST_USER = new APIUser
{
@ -27,6 +87,11 @@ namespace osu.Game.Tests.Visual.Online
JoinDate = DateTimeOffset.Now.AddDays(-1),
LastVisit = DateTimeOffset.Now,
ProfileOrder = new[] { "me" },
Groups = new[]
{
new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko" } }
},
Statistics = new UserStatistics
{
IsRanked = true,
@ -63,61 +128,7 @@ namespace osu.Game.Tests.Visual.Online
Title = "osu!volunteer",
Colour = "ff0000",
Achievements = Array.Empty<APIUserAchievement>(),
PlayMode = "osu"
};
public TestSceneUserProfileOverlay()
{
Add(profile = new TestUserProfileOverlay());
}
protected override void LoadComplete()
{
base.LoadComplete();
AddStep("Show offline dummy", () => profile.ShowUser(TEST_USER));
AddStep("Show null dummy", () => profile.ShowUser(new APIUser
{
Username = @"Null",
Id = 1,
}));
AddStep("Show ppy", () => profile.ShowUser(new APIUser
{
Username = @"peppy",
Id = 2,
IsSupporter = true,
CountryCode = CountryCode.AU,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg"
}));
AddStep("Show flyte", () => profile.ShowUser(new APIUser
{
Username = @"flyte",
Id = 3103765,
CountryCode = CountryCode.JP,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
}));
AddStep("Show bancho", () => profile.ShowUser(new APIUser
{
Username = @"BanchoBot",
Id = 3,
IsBot = true,
CountryCode = CountryCode.SH,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c4.jpg"
}));
AddStep("Show ppy from username", () => profile.ShowUser(new APIUser { Username = @"peppy" }));
AddStep("Show flyte from username", () => profile.ShowUser(new APIUser { Username = @"flyte" }));
AddStep("Hide", profile.Hide);
AddStep("Show without reload", profile.Show);
}
private partial class TestUserProfileOverlay : UserProfileOverlay
{
public new ProfileHeader Header => base.Header;
}
}
}

View File

@ -10,7 +10,9 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Profile;
using osu.Game.Overlays.Profile.Sections;
using osu.Game.Rulesets.Osu;
namespace osu.Game.Tests.Visual.Online
{
@ -44,7 +46,7 @@ namespace osu.Game.Tests.Visual.Online
}
});
AddStep("Show cookiezi", () => ranks.User.Value = new APIUser { Id = 124493 });
AddStep("Show cookiezi", () => ranks.User.Value = new UserProfileData(new APIUser { Id = 124493 }, new OsuRuleset().RulesetInfo));
}
}
}

View File

@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.SongSelect
var visibleBeatmapPanels = carousel.Items.OfType<DrawableCarouselBeatmap>().Where(p => p.IsPresent).ToArray();
return visibleBeatmapPanels.Length == 1
&& visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 0) == 1;
&& visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item)!.BeatmapInfo.Ruleset.OnlineID == 0) == 1;
});
AddStep("filter to ruleset 1", () => carousel.Filter(new FilterCriteria
@ -86,8 +86,8 @@ namespace osu.Game.Tests.Visual.SongSelect
var visibleBeatmapPanels = carousel.Items.OfType<DrawableCarouselBeatmap>().Where(p => p.IsPresent).ToArray();
return visibleBeatmapPanels.Length == 2
&& visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 0) == 1
&& visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 1) == 1;
&& visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item)!.BeatmapInfo.Ruleset.OnlineID == 0) == 1
&& visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item)!.BeatmapInfo.Ruleset.OnlineID == 1) == 1;
});
AddStep("filter to ruleset 2", () => carousel.Filter(new FilterCriteria
@ -101,8 +101,8 @@ namespace osu.Game.Tests.Visual.SongSelect
var visibleBeatmapPanels = carousel.Items.OfType<DrawableCarouselBeatmap>().Where(p => p.IsPresent).ToArray();
return visibleBeatmapPanels.Length == 2
&& visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 0) == 1
&& visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 2) == 1;
&& visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item!).BeatmapInfo.Ruleset.OnlineID == 0) == 1
&& visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item!).BeatmapInfo.Ruleset.OnlineID == 2) == 1;
});
}
@ -1069,7 +1069,7 @@ namespace osu.Game.Tests.Visual.SongSelect
return Precision.AlmostEquals(
carousel.ScreenSpaceDrawQuad.Centre,
carousel.Items
.First(i => i.Item.State.Value == CarouselItemState.Selected)
.First(i => i.Item?.State.Value == CarouselItemState.Selected)
.ScreenSpaceDrawQuad.Centre, 100);
});
}
@ -1103,7 +1103,7 @@ namespace osu.Game.Tests.Visual.SongSelect
if (currentlySelected == null)
return true;
return currentlySelected.Item.Visible;
return currentlySelected.Item!.Visible;
}
private void checkInvisibleDifficultiesUnselectable()

View File

@ -208,7 +208,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select next and enter", () =>
{
InputManager.MoveMouseTo(songSelect!.Carousel.ChildrenOfType<DrawableCarouselBeatmap>()
.First(b => !((CarouselBeatmap)b.Item).BeatmapInfo.Equals(songSelect!.Carousel.SelectedBeatmapInfo)));
.First(b => !((CarouselBeatmap)b.Item!).BeatmapInfo.Equals(songSelect!.Carousel.SelectedBeatmapInfo)));
InputManager.Click(MouseButton.Left);
@ -235,7 +235,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select next and enter", () =>
{
InputManager.MoveMouseTo(songSelect!.Carousel.ChildrenOfType<DrawableCarouselBeatmap>()
.First(b => !((CarouselBeatmap)b.Item).BeatmapInfo.Equals(songSelect!.Carousel.SelectedBeatmapInfo)));
.First(b => !((CarouselBeatmap)b.Item!).BeatmapInfo.Equals(songSelect!.Carousel.SelectedBeatmapInfo)));
InputManager.PressButton(MouseButton.Left);
@ -614,7 +614,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("selected only shows expected ruleset (plus converts)", () =>
{
var selectedPanel = songSelect!.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().First(s => s.Item.State.Value == CarouselItemState.Selected);
var selectedPanel = songSelect!.Carousel.ChildrenOfType<DrawableCarouselBeatmapSet>().First(s => s.Item!.State.Value == CarouselItemState.Selected);
// special case for converts checked here.
return selectedPanel.ChildrenOfType<FilterableDifficultyIcon>().All(i =>

View File

@ -0,0 +1,106 @@
// 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.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays.Volume;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
using Box = osu.Framework.Graphics.Shapes.Box;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneOverlayContainer : OsuManualInputManagerTestScene
{
[SetUp]
public void SetUp() => Schedule(() => Child = new TestOverlay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.5f)
});
[Test]
public void TestScrollBlocked()
{
OsuScrollContainer scroll = null!;
AddStep("add scroll container", () =>
{
Add(scroll = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue,
Child = new Box
{
RelativeSizeAxes = Axes.X,
Height = DrawHeight * 10,
Colour = ColourInfo.GradientVertical(Colour4.Black, Colour4.White),
}
});
});
AddStep("perform scroll", () =>
{
InputManager.MoveMouseTo(Content);
InputManager.ScrollVerticalBy(-10);
});
AddAssert("scroll didn't receive input", () => scroll.Current == 0);
}
[Test]
public void TestAltScrollNotBlocked()
{
bool scrollReceived = false;
AddStep("add volume control receptor", () => Add(new VolumeControlReceptor
{
RelativeSizeAxes = Axes.Both,
Depth = float.MaxValue,
ScrollActionRequested = (_, _, _) => scrollReceived = true,
}));
AddStep("hold alt", () => InputManager.PressKey(Key.AltLeft));
AddStep("perform scroll", () =>
{
InputManager.MoveMouseTo(Content);
InputManager.ScrollVerticalBy(10);
});
AddAssert("receptor received scroll input", () => scrollReceived);
AddStep("release alt", () => InputManager.ReleaseKey(Key.AltLeft));
}
private partial class TestOverlay : OsuFocusedOverlayContainer
{
[BackgroundDependencyLoader]
private void load()
{
State.Value = Visibility.Visible;
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Overlay content",
Colour = Color4.Black,
},
};
}
}
}
}

View File

@ -33,13 +33,15 @@ namespace osu.Game.Database
UserFileStorage = storage.GetStorageForDirectory(@"files");
}
protected virtual string GetFilename(TModel item) => item.GetDisplayString();
/// <summary>
/// Exports an item to a legacy (.zip based) package.
/// </summary>
/// <param name="item">The item to export.</param>
public void Export(TModel item)
{
string itemFilename = item.GetDisplayString().GetValidFilename();
string itemFilename = GetFilename(item).GetValidFilename();
IEnumerable<string> existingExports = exportStorage.GetFiles("", $"{itemFilename}*{FileExtension}");

View File

@ -20,6 +20,14 @@ namespace osu.Game.Database
{
}
protected override string GetFilename(ScoreInfo score)
{
string scoreString = score.GetDisplayString();
string filename = $"{scoreString} ({score.Date.LocalDateTime:yyyy-MM-dd_HH-mm})";
return filename;
}
public override void ExportModelTo(ScoreInfo model, Stream outputStream)
{
var file = model.Files.SingleOrDefault();

View File

@ -154,9 +154,12 @@ namespace osu.Game.Database
}
else
{
notification.CompletionText = imported.Count == 1
? $"Imported {imported.First().GetDisplayString()}!"
: $"Imported {imported.Count} {HumanisedModelName}s!";
if (tasks.Length > imported.Count)
notification.CompletionText = $"Imported {imported.Count} of {tasks.Length} {HumanisedModelName}s.";
else if (imported.Count > 1)
notification.CompletionText = $"Imported {imported.Count} {HumanisedModelName}s!";
else
notification.CompletionText = $"Imported {imported.First().GetDisplayString()}!";
if (imported.Count > 0 && PresentImport != null)
{

View File

@ -25,8 +25,6 @@ namespace osu.Game.Graphics.Containers
protected virtual string PopInSampleName => "UI/overlay-pop-in";
protected virtual string PopOutSampleName => "UI/overlay-pop-out";
protected override bool BlockScrollInput => false;
protected override bool BlockNonPositionalInput => true;
/// <summary>
@ -90,6 +88,15 @@ namespace osu.Game.Graphics.Containers
base.OnMouseUp(e);
}
protected override bool OnScroll(ScrollEvent e)
{
// allow for controlling volume when alt is held.
// mostly for compatibility with osu-stable.
if (e.AltPressed) return false;
return true;
}
public virtual bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat)

View File

@ -54,11 +54,6 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString RestartAndReOpenRequiredForCompletion => new TranslatableString(getKey(@"restart_and_re_open_required_for_completion"), @"To complete this operation, osu! will close. Please open it again to use the new data location.");
/// <summary>
/// "Import beatmaps from stable"
/// </summary>
public static LocalisableString ImportBeatmapsFromStable => new TranslatableString(getKey(@"import_beatmaps_from_stable"), @"Import beatmaps from stable");
/// <summary>
/// "Delete ALL beatmaps"
/// </summary>
@ -69,31 +64,16 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString DeleteAllBeatmapVideos => new TranslatableString(getKey(@"delete_all_beatmap_videos"), @"Delete ALL beatmap videos");
/// <summary>
/// "Import scores from stable"
/// </summary>
public static LocalisableString ImportScoresFromStable => new TranslatableString(getKey(@"import_scores_from_stable"), @"Import scores from stable");
/// <summary>
/// "Delete ALL scores"
/// </summary>
public static LocalisableString DeleteAllScores => new TranslatableString(getKey(@"delete_all_scores"), @"Delete ALL scores");
/// <summary>
/// "Import skins from stable"
/// </summary>
public static LocalisableString ImportSkinsFromStable => new TranslatableString(getKey(@"import_skins_from_stable"), @"Import skins from stable");
/// <summary>
/// "Delete ALL skins"
/// </summary>
public static LocalisableString DeleteAllSkins => new TranslatableString(getKey(@"delete_all_skins"), @"Delete ALL skins");
/// <summary>
/// "Import collections from stable"
/// </summary>
public static LocalisableString ImportCollectionsFromStable => new TranslatableString(getKey(@"import_collections_from_stable"), @"Import collections from stable");
/// <summary>
/// "Delete ALL collections"
/// </summary>

View File

@ -255,6 +255,9 @@ namespace osu.Game.Online.API.Requests.Responses
[CanBeNull]
public Dictionary<string, UserStatistics> RulesetsStatistics { get; set; }
[JsonProperty("groups")]
public APIUserGroup[] Groups;
public override string ToString() => Username;
/// <summary>

View File

@ -0,0 +1,37 @@
// 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;
namespace osu.Game.Online.API.Requests.Responses
{
public class APIUserGroup
{
[JsonProperty(@"colour")]
public string Colour { get; set; } = null!;
[JsonProperty(@"has_listing")]
public bool HasListings { get; set; }
[JsonProperty(@"has_playmodes")]
public bool HasPlaymodes { get; set; }
[JsonProperty(@"id")]
public int Id { get; set; }
[JsonProperty(@"identifier")]
public string Identifier { get; set; } = null!;
[JsonProperty(@"is_probationary")]
public bool IsProbationary { get; set; }
[JsonProperty(@"name")]
public string Name { get; set; } = null!;
[JsonProperty(@"short_name")]
public string ShortName { get; set; } = null!;
[JsonProperty(@"playmodes")]
public string[]? Playmodes { get; set; }
}
}

View File

@ -933,9 +933,9 @@ namespace osu.Game
loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true);
loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true);
var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true);
loadComponentSingleFile(channelManager = new ChannelManager(API), AddInternal, true);
loadComponentSingleFile(channelManager = new ChannelManager(API), Add, true);
loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true);
loadComponentSingleFile(new MessageNotifier(), AddInternal, true);
loadComponentSingleFile(new MessageNotifier(), Add, true);
loadComponentSingleFile(Settings = new SettingsOverlay(), leftFloatingOverlayContent.Add, true);
loadComponentSingleFile(changelogOverlay = new ChangelogOverlay(), overlayContent.Add, true);
loadComponentSingleFile(userProfile = new UserProfileOverlay(), overlayContent.Add, true);

View File

@ -296,7 +296,7 @@ namespace osu.Game
dependencies.Cache(ScoreDownloader = new ScoreModelDownloader(ScoreManager, API));
// Add after all the above cache operations as it depends on them.
AddInternal(difficultyCache);
base.Content.Add(difficultyCache);
// TODO: OsuGame or OsuGameBase?
dependencies.CacheAs(beatmapUpdater = new BeatmapUpdater(BeatmapManager, difficultyCache, API, Storage));
@ -305,19 +305,19 @@ namespace osu.Game
dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints));
dependencies.CacheAs(soloStatisticsWatcher = new SoloStatisticsWatcher());
AddInternal(new BeatmapOnlineChangeIngest(beatmapUpdater, realm, metadataClient));
base.Content.Add(new BeatmapOnlineChangeIngest(beatmapUpdater, realm, metadataClient));
BeatmapManager.ProcessBeatmap = args => beatmapUpdater.Process(args.beatmapSet, !args.isBatch);
dependencies.Cache(userCache = new UserLookupCache());
AddInternal(userCache);
base.Content.Add(userCache);
dependencies.Cache(beatmapCache = new BeatmapLookupCache());
AddInternal(beatmapCache);
base.Content.Add(beatmapCache);
var scorePerformanceManager = new ScorePerformanceCache();
dependencies.Cache(scorePerformanceManager);
AddInternal(scorePerformanceManager);
base.Content.Add(scorePerformanceManager);
dependencies.CacheAs<IRulesetConfigCache>(rulesetConfigCache = new RulesetConfigCache(realm, RulesetStore));
@ -344,14 +344,24 @@ namespace osu.Game
// add api components to hierarchy.
if (API is APIAccess apiAccess)
AddInternal(apiAccess);
base.Content.Add(apiAccess);
AddInternal(spectatorClient);
AddInternal(MultiplayerClient);
AddInternal(metadataClient);
AddInternal(soloStatisticsWatcher);
base.Content.Add(spectatorClient);
base.Content.Add(MultiplayerClient);
base.Content.Add(metadataClient);
base.Content.Add(soloStatisticsWatcher);
AddInternal(rulesetConfigCache);
base.Content.Add(rulesetConfigCache);
PreviewTrackManager previewTrackManager;
dependencies.Cache(previewTrackManager = new PreviewTrackManager(BeatmapManager.BeatmapTrackStore));
base.Content.Add(previewTrackManager);
base.Content.Add(MusicController = new MusicController());
dependencies.CacheAs(MusicController);
MusicController.TrackChanged += onTrackChanged;
base.Content.Add(beatmapClock);
GlobalActionContainer globalBindings;
@ -378,16 +388,6 @@ namespace osu.Game
dependencies.Cache(globalBindings);
PreviewTrackManager previewTrackManager;
dependencies.Cache(previewTrackManager = new PreviewTrackManager(BeatmapManager.BeatmapTrackStore));
Add(previewTrackManager);
AddInternal(MusicController = new MusicController());
dependencies.CacheAs(MusicController);
MusicController.TrackChanged += onTrackChanged;
AddInternal(beatmapClock);
Ruleset.BindValueChanged(onRulesetChanged);
Beatmap.BindValueChanged(onBeatmapChanged);
}

View File

@ -10,6 +10,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapSet;
@ -30,6 +31,13 @@ namespace osu.Game.Overlays
private readonly Bindable<APIBeatmapSet> beatmapSet = new Bindable<APIBeatmapSet>();
[Resolved]
private IAPIProvider api { get; set; }
private IBindable<APIUser> apiUser;
private (BeatmapSetLookupType type, int id)? lastLookup;
/// <remarks>
/// Isolates the beatmap set overlay from the game-wide selected mods bindable
/// to avoid affecting the beatmap details section (i.e. <see cref="AdvancedStats.StatisticRow"/>).
@ -72,6 +80,17 @@ namespace osu.Game.Overlays
};
}
[BackgroundDependencyLoader]
private void load()
{
apiUser = api.LocalUser.GetBoundCopy();
apiUser.BindValueChanged(_ => Schedule(() =>
{
if (api.IsLoggedIn)
performFetch();
}));
}
protected override BeatmapSetHeader CreateHeader() => new BeatmapSetHeader();
protected override Color4 BackgroundColour => ColourProvider.Background6;
@ -84,27 +103,20 @@ namespace osu.Game.Overlays
public void FetchAndShowBeatmap(int beatmapId)
{
lastLookup = (BeatmapSetLookupType.BeatmapId, beatmapId);
beatmapSet.Value = null;
var req = new GetBeatmapSetRequest(beatmapId, BeatmapSetLookupType.BeatmapId);
req.Success += res =>
{
beatmapSet.Value = res;
Header.HeaderContent.Picker.Beatmap.Value = Header.BeatmapSet.Value.Beatmaps.First(b => b.OnlineID == beatmapId);
};
API.Queue(req);
performFetch();
Show();
}
public void FetchAndShowBeatmapSet(int beatmapSetId)
{
lastLookup = (BeatmapSetLookupType.SetId, beatmapSetId);
beatmapSet.Value = null;
var req = new GetBeatmapSetRequest(beatmapSetId);
req.Success += res => beatmapSet.Value = res;
API.Queue(req);
performFetch();
Show();
}
@ -118,6 +130,24 @@ namespace osu.Game.Overlays
Show();
}
private void performFetch()
{
if (!api.IsLoggedIn)
return;
if (lastLookup == null)
return;
var req = new GetBeatmapSetRequest(lastLookup.Value.id, lastLookup.Value.type);
req.Success += res =>
{
beatmapSet.Value = res;
if (lastLookup.Value.type == BeatmapSetLookupType.BeatmapId)
Header.HeaderContent.Picker.Beatmap.Value = Header.BeatmapSet.Value.Beatmaps.First(b => b.OnlineID == lastLookup.Value.id);
};
API.Queue(req);
}
private partial class CommentsSection : BeatmapSetLayoutSection
{
public readonly Bindable<APIBeatmapSet> BeatmapSet = new Bindable<APIBeatmapSet>();

View File

@ -23,7 +23,7 @@ namespace osu.Game.Overlays.Profile.Header
{
public partial class BottomHeaderContainer : CompositeDrawable
{
public readonly Bindable<APIUser?> User = new Bindable<APIUser?>();
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
private LinkFlowContainer topLinkContainer = null!;
private LinkFlowContainer bottomLinkContainer = null!;
@ -73,7 +73,7 @@ namespace osu.Game.Overlays.Profile.Header
}
};
User.BindValueChanged(user => updateDisplay(user.NewValue));
User.BindValueChanged(user => updateDisplay(user.NewValue?.User));
}
private void updateDisplay(APIUser? user)

View File

@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Profile.Header
public partial class CentreHeaderContainer : CompositeDrawable
{
public readonly BindableBool DetailsVisible = new BindableBool(true);
public readonly Bindable<APIUser?> User = new Bindable<APIUser?>();
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
private OverlinedInfoContainer hiddenDetailGlobal = null!;
private OverlinedInfoContainer hiddenDetailCountry = null!;
@ -141,7 +141,7 @@ namespace osu.Game.Overlays.Profile.Header
expandedDetailContainer.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint);
});
User.BindValueChanged(user => updateDisplay(user.NewValue));
User.BindValueChanged(user => updateDisplay(user.NewValue?.User));
}
private void updateDisplay(APIUser? user)

View File

@ -5,14 +5,13 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Profile.Header.Components
{
public partial class FollowersButton : ProfileHeaderStatisticsButton
{
public readonly Bindable<APIUser?> User = new Bindable<APIUser?>();
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
public override LocalisableString TooltipText => FriendsStrings.ButtonsDisabled;
@ -22,7 +21,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
private void load()
{
// todo: when friending/unfriending is implemented, the APIAccess.Friends list should be updated accordingly.
User.BindValueChanged(user => SetValue(user.NewValue?.FollowerCount ?? 0), true);
User.BindValueChanged(user => SetValue(user.NewValue?.User.FollowerCount ?? 0), true);
}
}
}

View File

@ -0,0 +1,84 @@
// 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 System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osuTK;
namespace osu.Game.Overlays.Profile.Header.Components
{
public partial class GroupBadge : Container, IHasTooltip
{
public LocalisableString TooltipText { get; }
public int TextSize { get; set; } = 12;
private readonly APIUserGroup group;
public GroupBadge(APIUserGroup group)
{
this.group = group;
AutoSizeAxes = Axes.Both;
Masking = true;
CornerRadius = 8;
TooltipText = group.Name;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider? colourProvider, RulesetStore rulesets)
{
FillFlowContainer innerContainer;
AddRangeInternal(new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider?.Background6 ?? Colour4.Black
},
innerContainer = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Padding = new MarginPadding { Vertical = 2, Horizontal = 10 },
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Children = new[]
{
new OsuSpriteText
{
Text = group.ShortName,
Colour = Color4Extensions.FromHex(group.Colour),
Shadow = false,
Font = OsuFont.GetFont(size: TextSize, weight: FontWeight.Bold, italics: true)
}
}
}
});
if (group.Playmodes?.Length > 0)
{
innerContainer.AddRange(group.Playmodes.Select(p =>
(rulesets.GetRuleset(p)?.CreateInstance().CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle }).With(icon =>
{
icon.Size = new Vector2(TextSize - 1);
})).ToList()
);
}
}
}
}

View File

@ -0,0 +1,32 @@
// 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 System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osuTK;
namespace osu.Game.Overlays.Profile.Header.Components
{
public partial class GroupBadgeFlow : FillFlowContainer
{
public readonly Bindable<APIUser?> User = new Bindable<APIUser?>();
public GroupBadgeFlow()
{
AutoSizeAxes = Axes.Both;
Direction = FillDirection.Horizontal;
Spacing = new Vector2(2);
User.BindValueChanged(user =>
{
Clear(true);
if (user.NewValue?.Groups != null)
AddRange(user.NewValue.Groups.Select(g => new GroupBadge(g)));
});
}
}
}

View File

@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public partial class LevelBadge : CompositeDrawable, IHasTooltip
{
public readonly Bindable<APIUser?> User = new Bindable<APIUser?>();
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
public LocalisableString TooltipText { get; private set; }
@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
}
};
User.BindValueChanged(user => updateLevel(user.NewValue));
User.BindValueChanged(user => updateLevel(user.NewValue?.User));
}
private void updateLevel(APIUser? user)

View File

@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public partial class LevelProgressBar : CompositeDrawable, IHasTooltip
{
public readonly Bindable<APIUser?> User = new Bindable<APIUser?>();
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
public LocalisableString TooltipText { get; }
@ -56,7 +56,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
}
};
User.BindValueChanged(user => updateProgress(user.NewValue));
User.BindValueChanged(user => updateProgress(user.NewValue?.User));
}
private void updateProgress(APIUser? user)

View File

@ -5,14 +5,13 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Profile.Header.Components
{
public partial class MappingSubscribersButton : ProfileHeaderStatisticsButton
{
public readonly Bindable<APIUser?> User = new Bindable<APIUser?>();
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
public override LocalisableString TooltipText => FollowsStrings.MappingFollowers;
@ -21,7 +20,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
[BackgroundDependencyLoader]
private void load()
{
User.BindValueChanged(user => SetValue(user.NewValue?.MappingFollowerCount ?? 0), true);
User.BindValueChanged(user => SetValue(user.NewValue?.User.MappingFollowerCount ?? 0), true);
}
}
}

View File

@ -7,7 +7,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Resources.Localisation.Web;
using osuTK;
@ -16,7 +15,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
public partial class MessageUserButton : ProfileHeaderButton
{
public readonly Bindable<APIUser?> User = new Bindable<APIUser?>();
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
public override LocalisableString TooltipText => UsersStrings.CardSendMessage;
@ -49,12 +48,16 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
if (!Content.IsPresent) return;
channelManager?.OpenPrivateChannel(User.Value);
channelManager?.OpenPrivateChannel(User.Value?.User);
userOverlay?.Hide();
chatOverlay?.Show();
};
User.ValueChanged += e => Content.Alpha = e.NewValue != null && !e.NewValue.PMFriendsOnly && apiProvider.LocalUser.Value.Id != e.NewValue.Id ? 1 : 0;
User.ValueChanged += e =>
{
var user = e.NewValue?.User;
Content.Alpha = user != null && !user.PMFriendsOnly && apiProvider.LocalUser.Value.Id != user.Id ? 1 : 0;
};
}
}
}

View File

@ -7,14 +7,13 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Profile.Header.Components
{
public partial class OverlinedTotalPlayTime : CompositeDrawable, IHasTooltip
{
public readonly Bindable<APIUser?> User = new Bindable<APIUser?>();
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
public LocalisableString TooltipText { get; set; }
@ -39,10 +38,11 @@ namespace osu.Game.Overlays.Profile.Header.Components
User.BindValueChanged(updateTime, true);
}
private void updateTime(ValueChangedEvent<APIUser?> user)
private void updateTime(ValueChangedEvent<UserProfileData?> user)
{
TooltipText = (user.NewValue?.Statistics?.PlayTime ?? 0) / 3600 + " hours";
info.Content = formatTime(user.NewValue?.Statistics?.PlayTime);
int? playTime = user.NewValue?.User.Statistics?.PlayTime;
TooltipText = (playTime ?? 0) / 3600 + " hours";
info.Content = formatTime(playTime);
}
private string formatTime(int? secondsNull)

View File

@ -1,22 +1,39 @@
// 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 System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Extensions;
using osu.Game.Rulesets;
namespace osu.Game.Overlays.Profile.Header.Components
{
public partial class ProfileRulesetSelector : OverlayRulesetSelector
{
public readonly Bindable<APIUser?> User = new Bindable<APIUser?>();
[Resolved]
private UserProfileOverlay? profileOverlay { get; set; }
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
protected override void LoadComplete()
{
base.LoadComplete();
User.BindValueChanged(u => SetDefaultRuleset(Rulesets.GetRuleset(u.NewValue?.PlayMode ?? "osu").AsNonNull()), true);
User.BindValueChanged(user => updateState(user.NewValue), true);
Current.BindValueChanged(ruleset =>
{
if (User.Value != null && !ruleset.NewValue.Equals(User.Value.Ruleset))
profileOverlay?.ShowUser(User.Value.User, ruleset.NewValue);
});
}
private void updateState(UserProfileData? user)
{
Current.Value = Items.SingleOrDefault(ruleset => user?.Ruleset.MatchesOnlineID(ruleset) == true);
SetDefaultRuleset(Rulesets.GetRuleset(user?.User.PlayMode ?? @"osu").AsNonNull());
}
public void SetDefaultRuleset(RulesetInfo ruleset)

View File

@ -11,7 +11,6 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Resources.Localisation.Web;
@ -30,7 +29,7 @@ namespace osu.Game.Overlays.Profile.Header
private FillFlowContainer? fillFlow;
private RankGraph rankGraph = null!;
public readonly Bindable<APIUser?> User = new Bindable<APIUser?>();
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
private bool expanded = true;
@ -171,8 +170,10 @@ namespace osu.Game.Overlays.Profile.Header
};
}
private void updateDisplay(APIUser? user)
private void updateDisplay(UserProfileData? data)
{
var user = data?.User;
medalInfo.Content = user?.Achievements?.Length.ToString() ?? "0";
ppInfo.Content = user?.Statistics?.PP?.ToLocalisableString("#,##0") ?? (LocalisableString)"0";

View File

@ -20,14 +20,14 @@ namespace osu.Game.Overlays.Profile.Header
{
private FillFlowContainer badgeFlowContainer = null!;
public readonly Bindable<APIUser?> User = new Bindable<APIUser?>();
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Alpha = 0;
AutoSizeAxes = Axes.Y;
User.ValueChanged += e => updateDisplay(e.NewValue);
User.ValueChanged += e => updateDisplay(e.NewValue?.User);
InternalChildren = new Drawable[]
{

View File

@ -15,7 +15,6 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Users.Drawables;
@ -27,7 +26,7 @@ namespace osu.Game.Overlays.Profile.Header
{
private const float avatar_size = 110;
public readonly Bindable<APIUser?> User = new Bindable<APIUser?>();
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
[Resolved]
private IAPIProvider api { get; set; } = null!;
@ -40,6 +39,7 @@ namespace osu.Game.Overlays.Profile.Header
private UpdateableFlag userFlag = null!;
private OsuSpriteText userCountryText = null!;
private FillFlowContainer userStats = null!;
private GroupBadgeFlow groupBadgeFlow = null!;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
@ -90,6 +90,7 @@ namespace osu.Game.Overlays.Profile.Header
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Children = new Drawable[]
{
usernameText = new OsuSpriteText
@ -98,10 +99,14 @@ namespace osu.Game.Overlays.Profile.Header
},
openUserExternally = new ExternalLinkButton
{
Margin = new MarginPadding { Left = 5 },
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
groupBadgeFlow = new GroupBadgeFlow
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
}
}
},
titleText = new OsuSpriteText
@ -174,8 +179,10 @@ namespace osu.Game.Overlays.Profile.Header
User.BindValueChanged(user => updateUser(user.NewValue));
}
private void updateUser(APIUser? user)
private void updateUser(UserProfileData? data)
{
var user = data?.User;
avatar.User = user;
usernameText.Text = user?.Username ?? string.Empty;
openUserExternally.Link = $@"{api.WebsiteRootUrl}/users/{user?.Id ?? 0}";
@ -184,6 +191,7 @@ namespace osu.Game.Overlays.Profile.Header
supporterTag.SupportLevel = user?.SupportLevel ?? 0;
titleText.Text = user?.Title ?? string.Empty;
titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff");
groupBadgeFlow.User.Value = user;
userStats.Clear();

View File

@ -9,8 +9,8 @@ using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Profile.Header;
using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Users;
@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Profile
{
private UserCoverBackground coverContainer = null!;
public Bindable<APIUser?> User = new Bindable<APIUser?>();
public Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
private CentreHeaderContainer centreHeaderContainer;
private DetailHeaderContainer detailHeaderContainer;
@ -36,6 +36,13 @@ namespace osu.Game.Overlays.Profile
// todo: pending implementation.
// TabControl.AddItem(LayoutStrings.HeaderUsersModding);
TabControlContainer.Add(new ProfileRulesetSelector
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
User = { BindTarget = User }
});
// Haphazardly guaranteed by OverlayHeader constructor (see CreateBackground / CreateContent).
Debug.Assert(centreHeaderContainer != null);
Debug.Assert(detailHeaderContainer != null);
@ -100,7 +107,7 @@ namespace osu.Game.Overlays.Profile
protected override OverlayTitle CreateTitle() => new ProfileHeaderTitle();
private void updateDisplay(APIUser? user) => coverContainer.User = user;
private void updateDisplay(UserProfileData? user) => coverContainer.User = user?.User;
private partial class ProfileHeaderTitle : OverlayTitle
{

View File

@ -13,7 +13,6 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Overlays.Profile
{
@ -29,7 +28,7 @@ namespace osu.Game.Overlays.Profile
protected override Container<Drawable> Content => content;
public readonly Bindable<APIUser?> User = new Bindable<APIUser?>();
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
protected ProfileSection()
{

View File

@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps
protected override int InitialItemsCount => type == BeatmapSetType.Graveyard ? 2 : 6;
public PaginatedBeatmapContainer(BeatmapSetType type, Bindable<APIUser?> user, LocalisableString headerText)
public PaginatedBeatmapContainer(BeatmapSetType type, Bindable<UserProfileData?> user, LocalisableString headerText)
: base(user, headerText)
{
this.type = type;
@ -64,8 +64,8 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps
}
}
protected override APIRequest<List<APIBeatmapSet>> CreateRequest(APIUser user, PaginationParameters pagination) =>
new GetUserBeatmapsRequest(user.Id, type, pagination);
protected override APIRequest<List<APIBeatmapSet>> CreateRequest(UserProfileData user, PaginationParameters pagination) =>
new GetUserBeatmapsRequest(user.User.Id, type, pagination);
protected override Drawable? CreateDrawableItem(APIBeatmapSet model) => model.OnlineID > 0
? new BeatmapCardNormal(model)

View File

@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
/// </summary>
protected abstract LocalisableString GraphCounterName { get; }
protected ChartProfileSubsection(Bindable<APIUser?> user, LocalisableString headerText)
protected ChartProfileSubsection(Bindable<UserProfileData?> user, LocalisableString headerText)
: base(user, headerText)
{
}
@ -44,9 +44,9 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
User.BindValueChanged(onUserChanged, true);
}
private void onUserChanged(ValueChangedEvent<APIUser?> e)
private void onUserChanged(ValueChangedEvent<UserProfileData?> e)
{
var values = GetValues(e.NewValue);
var values = GetValues(e.NewValue?.User);
if (values == null || values.Length <= 1)
{

View File

@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
{
public partial class PaginatedMostPlayedBeatmapContainer : PaginatedProfileSubsection<APIUserMostPlayedBeatmap>
{
public PaginatedMostPlayedBeatmapContainer(Bindable<APIUser?> user)
public PaginatedMostPlayedBeatmapContainer(Bindable<UserProfileData?> user)
: base(user, UsersStrings.ShowExtraHistoricalMostPlayedTitle)
{
}
@ -29,8 +29,8 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
protected override int GetCount(APIUser user) => user.BeatmapPlayCountsCount;
protected override APIRequest<List<APIUserMostPlayedBeatmap>> CreateRequest(APIUser user, PaginationParameters pagination) =>
new GetUserMostPlayedBeatmapsRequest(user.Id, pagination);
protected override APIRequest<List<APIUserMostPlayedBeatmap>> CreateRequest(UserProfileData user, PaginationParameters pagination) =>
new GetUserMostPlayedBeatmapsRequest(user.User.Id, pagination);
protected override Drawable CreateDrawableItem(APIUserMostPlayedBeatmap mostPlayed) =>
new DrawableMostPlayedBeatmap(mostPlayed);

View File

@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
{
protected override LocalisableString GraphCounterName => UsersStrings.ShowExtraHistoricalMonthlyPlaycountsCountLabel;
public PlayHistorySubsection(Bindable<APIUser?> user)
public PlayHistorySubsection(Bindable<UserProfileData?> user)
: base(user, UsersStrings.ShowExtraHistoricalMonthlyPlaycountsTitle)
{
}

View File

@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
{
protected override LocalisableString GraphCounterName => UsersStrings.ShowExtraHistoricalReplaysWatchedCountsCountLabel;
public ReplaysSubsection(Bindable<APIUser?> user)
public ReplaysSubsection(Bindable<UserProfileData?> user)
: base(user, UsersStrings.ShowExtraHistoricalReplaysWatchedCountsTitle)
{
}

View File

@ -14,15 +14,14 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Game.Resources.Localisation.Web;
using osu.Framework.Localisation;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Overlays.Profile.Sections.Kudosu
{
public partial class KudosuInfo : Container
{
private readonly Bindable<APIUser?> user = new Bindable<APIUser?>();
private readonly Bindable<UserProfileData?> user = new Bindable<UserProfileData?>();
public KudosuInfo(Bindable<APIUser?> user)
public KudosuInfo(Bindable<UserProfileData?> user)
{
this.user.BindTo(user);
CountSection total;
@ -32,7 +31,7 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu
CornerRadius = 3;
Child = total = new CountTotal();
this.user.ValueChanged += u => total.Count = u.NewValue?.Kudosu.Total ?? 0;
this.user.ValueChanged += u => total.Count = u.NewValue?.User.Kudosu.Total ?? 0;
}
protected override bool OnClick(ClickEvent e) => true;

View File

@ -8,19 +8,18 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.API;
using System.Collections.Generic;
using osu.Game.Resources.Localisation.Web;
using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
namespace osu.Game.Overlays.Profile.Sections.Kudosu
{
public partial class PaginatedKudosuHistoryContainer : PaginatedProfileSubsection<APIKudosuHistory>
{
public PaginatedKudosuHistoryContainer(Bindable<APIUser?> user)
public PaginatedKudosuHistoryContainer(Bindable<UserProfileData?> user)
: base(user, missingText: UsersStrings.ShowExtraKudosuEntryEmpty)
{
}
protected override APIRequest<List<APIKudosuHistory>> CreateRequest(APIUser user, PaginationParameters pagination)
=> new GetUserKudosuHistoryRequest(user.Id, pagination);
protected override APIRequest<List<APIKudosuHistory>> CreateRequest(UserProfileData user, PaginationParameters pagination)
=> new GetUserKudosuHistoryRequest(user.User.Id, pagination);
protected override Drawable CreateDrawableItem(APIKudosuHistory item) => new DrawableKudosuHistoryItem(item);
}

View File

@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Profile.Sections
private OsuSpriteText missing = null!;
private readonly LocalisableString? missingText;
protected PaginatedProfileSubsection(Bindable<APIUser?> user, LocalisableString? headerText = null, LocalisableString? missingText = null)
protected PaginatedProfileSubsection(Bindable<UserProfileData?> user, LocalisableString? headerText = null, LocalisableString? missingText = null)
: base(user, headerText, CounterVisibilityState.AlwaysVisible)
{
this.missingText = missingText;
@ -92,7 +92,7 @@ namespace osu.Game.Overlays.Profile.Sections
User.BindValueChanged(onUserChanged, true);
}
private void onUserChanged(ValueChangedEvent<APIUser?> e)
private void onUserChanged(ValueChangedEvent<UserProfileData?> e)
{
loadCancellation?.Cancel();
retrievalRequest?.Cancel();
@ -100,10 +100,10 @@ namespace osu.Game.Overlays.Profile.Sections
CurrentPage = null;
ItemsContainer.Clear();
if (e.NewValue != null)
if (e.NewValue?.User != null)
{
showMore();
SetCount(GetCount(e.NewValue));
SetCount(GetCount(e.NewValue.User));
}
}
@ -154,7 +154,7 @@ namespace osu.Game.Overlays.Profile.Sections
{
}
protected abstract APIRequest<List<TModel>> CreateRequest(APIUser user, PaginationParameters pagination);
protected abstract APIRequest<List<TModel>> CreateRequest(UserProfileData user, PaginationParameters pagination);
protected abstract Drawable? CreateDrawableItem(TModel model);

View File

@ -6,20 +6,19 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Overlays.Profile.Sections
{
public abstract partial class ProfileSubsection : FillFlowContainer
{
protected readonly Bindable<APIUser?> User = new Bindable<APIUser?>();
protected readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
private readonly LocalisableString headerText;
private readonly CounterVisibilityState counterVisibilityState;
private ProfileSubsectionHeader header = null!;
protected ProfileSubsection(Bindable<APIUser?> user, LocalisableString? headerText = null, CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
protected ProfileSubsection(Bindable<UserProfileData?> user, LocalisableString? headerText = null, CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
{
this.headerText = headerText ?? string.Empty;
this.counterVisibilityState = counterVisibilityState;

View File

@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
{
private readonly ScoreType type;
public PaginatedScoreContainer(ScoreType type, Bindable<APIUser?> user, LocalisableString headerText)
public PaginatedScoreContainer(ScoreType type, Bindable<UserProfileData?> user, LocalisableString headerText)
: base(user, headerText)
{
this.type = type;
@ -60,8 +60,8 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
base.OnItemsReceived(items);
}
protected override APIRequest<List<SoloScoreInfo>> CreateRequest(APIUser user, PaginationParameters pagination) =>
new GetUserScoresRequest(user.Id, type, pagination);
protected override APIRequest<List<SoloScoreInfo>> CreateRequest(UserProfileData user, PaginationParameters pagination) =>
new GetUserScoresRequest(user.User.Id, type, pagination, user.Ruleset);
private int drawableItemIndex;

View File

@ -10,13 +10,12 @@ using System.Collections.Generic;
using osuTK;
using osu.Framework.Allocation;
using osu.Game.Resources.Localisation.Web;
using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
namespace osu.Game.Overlays.Profile.Sections.Recent
{
public partial class PaginatedRecentActivityContainer : PaginatedProfileSubsection<APIRecentActivity>
{
public PaginatedRecentActivityContainer(Bindable<APIUser?> user)
public PaginatedRecentActivityContainer(Bindable<UserProfileData?> user)
: base(user, missingText: EventsStrings.Empty)
{
}
@ -27,8 +26,8 @@ namespace osu.Game.Overlays.Profile.Sections.Recent
ItemsContainer.Spacing = new Vector2(0, 8);
}
protected override APIRequest<List<APIRecentActivity>> CreateRequest(APIUser user, PaginationParameters pagination) =>
new GetUserRecentActivitiesRequest(user.Id, pagination);
protected override APIRequest<List<APIRecentActivity>> CreateRequest(UserProfileData user, PaginationParameters pagination) =>
new GetUserRecentActivitiesRequest(user.User.Id, pagination);
protected override Drawable CreateDrawableItem(APIRecentActivity model) => new DrawableRecentActivity(model);
}

View File

@ -0,0 +1,30 @@
// 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.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
namespace osu.Game.Overlays.Profile
{
/// <summary>
/// Contains data about a profile presented on the <see cref="UserProfileOverlay"/>.
/// </summary>
public class UserProfileData
{
/// <summary>
/// The user whose profile is being presented.
/// </summary>
public APIUser User { get; }
/// <summary>
/// The ruleset that the user profile is being shown with.
/// </summary>
public RulesetInfo Ruleset { get; }
public UserProfileData(APIUser user, RulesetInfo ruleset)
{
User = user;
Ruleset = ruleset;
}
}
}

View File

@ -3,6 +3,7 @@
#nullable disable
using osu.Framework.Development;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
@ -22,11 +23,12 @@ namespace osu.Game.Overlays.Settings.Sections
public DebugSection()
{
Children = new Drawable[]
{
new GeneralSettings(),
new MemorySettings(),
};
Add(new GeneralSettings());
if (DebugUtils.IsDebugBuild)
Add(new BatchImportSettings());
Add(new MemorySettings());
}
}
}

View File

@ -0,0 +1,66 @@
// 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.Localisation;
using osu.Game.Database;
namespace osu.Game.Overlays.Settings.Sections.DebugSettings
{
public partial class BatchImportSettings : SettingsSubsection
{
protected override LocalisableString Header => @"Batch Import";
private SettingsButton importBeatmapsButton = null!;
private SettingsButton importCollectionsButton = null!;
private SettingsButton importScoresButton = null!;
private SettingsButton importSkinsButton = null!;
[BackgroundDependencyLoader]
private void load(LegacyImportManager? legacyImportManager)
{
if (legacyImportManager?.SupportsImportFromStable != true)
return;
AddRange(new[]
{
importBeatmapsButton = new SettingsButton
{
Text = @"Import beatmaps from stable",
Action = () =>
{
importBeatmapsButton.Enabled.Value = false;
legacyImportManager.ImportFromStableAsync(StableContent.Beatmaps).ContinueWith(_ => Schedule(() => importBeatmapsButton.Enabled.Value = true));
}
},
importSkinsButton = new SettingsButton
{
Text = @"Import skins from stable",
Action = () =>
{
importSkinsButton.Enabled.Value = false;
legacyImportManager.ImportFromStableAsync(StableContent.Skins).ContinueWith(_ => Schedule(() => importSkinsButton.Enabled.Value = true));
}
},
importCollectionsButton = new SettingsButton
{
Text = @"Import collections from stable",
Action = () =>
{
importCollectionsButton.Enabled.Value = false;
legacyImportManager.ImportFromStableAsync(StableContent.Collections).ContinueWith(_ => Schedule(() => importCollectionsButton.Enabled.Value = true));
}
},
importScoresButton = new SettingsButton
{
Text = @"Import scores from stable",
Action = () =>
{
importScoresButton.Enabled.Value = false;
legacyImportManager.ImportFromStableAsync(StableContent.Scores).ContinueWith(_ => Schedule(() => importScoresButton.Enabled.Value = true));
}
},
});
}
}
}

View File

@ -6,7 +6,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Settings.Sections.Maintenance
@ -15,28 +14,14 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
protected override LocalisableString Header => CommonStrings.Beatmaps;
private SettingsButton importBeatmapsButton = null!;
private SettingsButton deleteBeatmapsButton = null!;
private SettingsButton deleteBeatmapVideosButton = null!;
private SettingsButton restoreButton = null!;
private SettingsButton undeleteButton = null!;
[BackgroundDependencyLoader]
private void load(BeatmapManager beatmaps, LegacyImportManager? legacyImportManager, IDialogOverlay? dialogOverlay)
private void load(BeatmapManager beatmaps, IDialogOverlay? dialogOverlay)
{
if (legacyImportManager?.SupportsImportFromStable == true)
{
Add(importBeatmapsButton = new SettingsButton
{
Text = MaintenanceSettingsStrings.ImportBeatmapsFromStable,
Action = () =>
{
importBeatmapsButton.Enabled.Value = false;
legacyImportManager.ImportFromStableAsync(StableContent.Beatmaps).ContinueWith(_ => Schedule(() => importBeatmapsButton.Enabled.Value = true));
}
});
}
Add(deleteBeatmapsButton = new DangerousSettingsButton
{
Text = MaintenanceSettingsStrings.DeleteAllBeatmaps,

View File

@ -14,8 +14,6 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
protected override LocalisableString Header => CommonStrings.Collections;
private SettingsButton importCollectionsButton = null!;
[Resolved]
private RealmAccess realm { get; set; } = null!;
@ -23,21 +21,8 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
private INotificationOverlay? notificationOverlay { get; set; }
[BackgroundDependencyLoader]
private void load(LegacyImportManager? legacyImportManager, IDialogOverlay? dialogOverlay)
private void load(IDialogOverlay? dialogOverlay)
{
if (legacyImportManager?.SupportsImportFromStable == true)
{
Add(importCollectionsButton = new SettingsButton
{
Text = MaintenanceSettingsStrings.ImportCollectionsFromStable,
Action = () =>
{
importCollectionsButton.Enabled.Value = false;
legacyImportManager.ImportFromStableAsync(StableContent.Collections).ContinueWith(_ => Schedule(() => importCollectionsButton.Enabled.Value = true));
}
});
}
Add(new DangerousSettingsButton
{
Text = MaintenanceSettingsStrings.DeleteAllCollections,

View File

@ -4,7 +4,6 @@
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Localisation;
using osu.Game.Database;
using osu.Game.Localisation;
using osu.Game.Scoring;
@ -14,25 +13,11 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
protected override LocalisableString Header => CommonStrings.Scores;
private SettingsButton importScoresButton = null!;
private SettingsButton deleteScoresButton = null!;
[BackgroundDependencyLoader]
private void load(ScoreManager scores, LegacyImportManager? legacyImportManager, IDialogOverlay? dialogOverlay)
private void load(ScoreManager scores, IDialogOverlay? dialogOverlay)
{
if (legacyImportManager?.SupportsImportFromStable == true)
{
Add(importScoresButton = new SettingsButton
{
Text = MaintenanceSettingsStrings.ImportScoresFromStable,
Action = () =>
{
importScoresButton.Enabled.Value = false;
legacyImportManager.ImportFromStableAsync(StableContent.Scores).ContinueWith(_ => Schedule(() => importScoresButton.Enabled.Value = true));
}
});
}
Add(deleteScoresButton = new DangerousSettingsButton
{
Text = MaintenanceSettingsStrings.DeleteAllScores,

View File

@ -4,7 +4,6 @@
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Localisation;
using osu.Game.Database;
using osu.Game.Localisation;
using osu.Game.Skinning;
@ -14,25 +13,11 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{
protected override LocalisableString Header => CommonStrings.Skins;
private SettingsButton importSkinsButton = null!;
private SettingsButton deleteSkinsButton = null!;
[BackgroundDependencyLoader]
private void load(SkinManager skins, LegacyImportManager? legacyImportManager, IDialogOverlay? dialogOverlay)
private void load(SkinManager skins, IDialogOverlay? dialogOverlay)
{
if (legacyImportManager?.SupportsImportFromStable == true)
{
Add(importSkinsButton = new SettingsButton
{
Text = MaintenanceSettingsStrings.ImportSkinsFromStable,
Action = () =>
{
importSkinsButton.Enabled.Value = false;
legacyImportManager.ImportFromStableAsync(StableContent.Skins).ContinueWith(_ => Schedule(() => importSkinsButton.Enabled.Value = true));
}
});
}
Add(deleteSkinsButton = new DangerousSettingsButton
{
Text = MaintenanceSettingsStrings.DeleteAllSkins,

View File

@ -23,10 +23,10 @@ namespace osu.Game.Overlays
/// <typeparam name="T">The type of item to be represented by tabs.</typeparam>
public abstract partial class TabControlOverlayHeader<T> : OverlayHeader, IHasCurrentValue<T>
{
protected OsuTabControl<T> TabControl;
protected OsuTabControl<T> TabControl { get; }
protected Container TabControlContainer { get; }
private readonly Box controlBackground;
private readonly Container tabControlContainer;
private readonly BindableWithCurrent<T> current = new BindableWithCurrent<T>();
public Bindable<T> Current
@ -41,7 +41,7 @@ namespace osu.Game.Overlays
set
{
base.ContentSidePadding = value;
tabControlContainer.Padding = new MarginPadding { Horizontal = value };
TabControlContainer.Padding = new MarginPadding { Horizontal = value };
}
}
@ -57,7 +57,7 @@ namespace osu.Game.Overlays
{
RelativeSizeAxes = Axes.Both,
},
tabControlContainer = new Container
TabControlContainer = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,

View File

@ -5,16 +5,21 @@ using System;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Extensions;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Profile;
using osu.Game.Overlays.Profile.Sections;
using osu.Game.Rulesets;
using osu.Game.Users;
using osuTK;
using osuTK.Graphics;
@ -23,31 +28,47 @@ namespace osu.Game.Overlays
{
public partial class UserProfileOverlay : FullscreenOverlay<ProfileHeader>
{
protected override Container<Drawable> Content => onlineViewContainer;
private readonly OnlineViewContainer onlineViewContainer;
private readonly LoadingLayer loadingLayer;
private ProfileSection? lastSection;
private ProfileSection[]? sections;
private GetUserRequest? userReq;
private ProfileSectionsContainer? sectionsContainer;
private ProfileSectionTabControl? tabs;
[Resolved]
private RulesetStore rulesets { get; set; } = null!;
public const float CONTENT_X_MARGIN = 70;
public UserProfileOverlay()
: base(OverlayColourScheme.Pink)
{
base.Content.AddRange(new Drawable[]
{
onlineViewContainer = new OnlineViewContainer($"Sign in to view the {Header.Title.Title}")
{
RelativeSizeAxes = Axes.Both
},
loadingLayer = new LoadingLayer(true)
});
}
protected override ProfileHeader CreateHeader() => new ProfileHeader();
protected override Color4 BackgroundColour => ColourProvider.Background6;
public void ShowUser(IUser user)
public void ShowUser(IUser user, IRulesetInfo? ruleset = null)
{
if (user.OnlineID == APIUser.SYSTEM_USER_ID)
return;
Show();
if (user.OnlineID == Header?.User.Value?.Id)
if (user.OnlineID == Header.User.Value?.User.Id && ruleset?.MatchesOnlineID(Header.User.Value?.Ruleset) == true)
return;
if (sectionsContainer != null)
@ -116,25 +137,20 @@ namespace osu.Game.Overlays
sectionsContainer.ScrollToTop();
// Check arbitrarily whether this user has already been populated.
// This is only generally used by tests, but should be quite safe unless we want to force a refresh on loading a previous user in the future.
if (user is APIUser apiUser && apiUser.JoinDate != default)
{
userReq = null;
userLoadComplete(apiUser);
return;
}
userReq = user.OnlineID > 1 ? new GetUserRequest(user.OnlineID) : new GetUserRequest(user.Username);
userReq.Success += userLoadComplete;
userReq = user.OnlineID > 1 ? new GetUserRequest(user.OnlineID, ruleset) : new GetUserRequest(user.Username, ruleset);
userReq.Success += u => userLoadComplete(u, ruleset);
API.Queue(userReq);
loadingLayer.Show();
}
private void userLoadComplete(APIUser user)
private void userLoadComplete(APIUser user, IRulesetInfo? ruleset)
{
Debug.Assert(sections != null && sectionsContainer != null && tabs != null);
Header.User.Value = user;
var actualRuleset = rulesets.GetRuleset(ruleset?.ShortName ?? user.PlayMode).AsNonNull();
var userProfile = new UserProfileData(user, actualRuleset);
Header.User.Value = userProfile;
if (user.ProfileOrder != null)
{
@ -144,13 +160,15 @@ namespace osu.Game.Overlays
if (sec != null)
{
sec.User.Value = user;
sec.User.Value = userProfile;
sectionsContainer.Add(sec);
tabs.AddItem(sec);
}
}
}
loadingLayer.Hide();
}
private partial class ProfileSectionTabControl : OverlayTabControl<ProfileSection>

View File

@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Objects
private readonly List<Vector2> calculatedPath = new List<Vector2>();
private readonly List<double> cumulativeLength = new List<double>();
private readonly List<int> segmentEnds = new List<int>();
private readonly Cached pathCache = new Cached();
private double calculatedLength;
@ -196,6 +197,16 @@ namespace osu.Game.Rulesets.Objects
return pointsInCurrentSegment;
}
/// <summary>
/// Returns the progress values at which segments of the path end.
/// </summary>
public IEnumerable<double> GetSegmentEnds()
{
ensureValid();
return segmentEnds.Select(i => cumulativeLength[i] / calculatedLength);
}
private void invalidate()
{
pathCache.Invalidate();
@ -216,6 +227,7 @@ namespace osu.Game.Rulesets.Objects
private void calculatePath()
{
calculatedPath.Clear();
segmentEnds.Clear();
if (ControlPoints.Count == 0)
return;
@ -241,6 +253,9 @@ namespace osu.Game.Rulesets.Objects
calculatedPath.Add(t);
}
// Remember the index of the segment end
segmentEnds.Add(calculatedPath.Count - 1);
// Start the new segment at the current vertex
start = i;
}
@ -306,6 +321,10 @@ namespace osu.Game.Rulesets.Objects
{
cumulativeLength.RemoveAt(cumulativeLength.Count - 1);
calculatedPath.RemoveAt(pathEndIndex--);
// Shorten the last segment to the expected distance
if (segmentEnds.Count > 0)
segmentEnds[^1]--;
}
}

View File

@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Scoring
public DrainingHealthProcessor(double drainStartTime, double drainLenience = 0)
{
this.drainStartTime = drainStartTime;
this.drainLenience = drainLenience;
this.drainLenience = Math.Clamp(drainLenience, 0, 1);
}
protected override void Update()
@ -79,7 +79,8 @@ namespace osu.Game.Rulesets.Scoring
double lastGameplayTime = Math.Clamp(Time.Current - Time.Elapsed, drainStartTime, gameplayEndTime);
double currentGameplayTime = Math.Clamp(Time.Current, drainStartTime, gameplayEndTime);
Health.Value -= drainRate * (currentGameplayTime - lastGameplayTime);
if (drainLenience < 1)
Health.Value -= drainRate * (currentGameplayTime - lastGameplayTime);
}
public override void ApplyBeatmap(IBeatmap beatmap)

View File

@ -423,6 +423,8 @@ namespace osu.Game.Rulesets.Scoring
score.Accuracy = Accuracy.Value;
score.Rank = Rank.Value;
score.HitEvents = hitEvents;
score.Statistics.Clear();
score.MaximumStatistics.Clear();
foreach (var result in HitResultExtensions.ALL_TYPES)
score.Statistics[result] = scoreResultCounts.GetValueOrDefault(result);

View File

@ -46,10 +46,12 @@ namespace osu.Game.Scoring.Legacy
score.ScoreInfo = scoreInfo;
int version = sr.ReadInt32();
string beatmapHash = sr.ReadString();
workingBeatmap = GetBeatmap(beatmapHash);
workingBeatmap = GetBeatmap(sr.ReadString());
if (workingBeatmap is DummyWorkingBeatmap)
throw new BeatmapNotFoundException();
throw new BeatmapNotFoundException(beatmapHash);
scoreInfo.User = new APIUser { Username = sr.ReadString() };
@ -334,9 +336,11 @@ namespace osu.Game.Scoring.Legacy
public class BeatmapNotFoundException : Exception
{
public BeatmapNotFoundException()
: base("No corresponding beatmap for the score could be found.")
public string Hash { get; }
public BeatmapNotFoundException(string hash)
{
Hash = hash;
}
}
}

View File

@ -44,7 +44,9 @@ namespace osu.Game.Scoring
protected override ScoreInfo? CreateModel(ArchiveReader archive)
{
using (var stream = archive.GetStream(archive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase))))
string name = archive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase));
using (var stream = archive.GetStream(name))
{
try
{
@ -52,7 +54,7 @@ namespace osu.Game.Scoring
}
catch (LegacyScoreDecoder.BeatmapNotFoundException e)
{
Logger.Log(e.Message, LoggingTarget.Information, LogLevel.Error);
Logger.Log($@"Score '{name}' failed to import: no corresponding beatmap with the hash '{e.Hash}' could be found.", LoggingTarget.Database);
return null;
}
}

View File

@ -148,9 +148,14 @@ namespace osu.Game.Screens.OnlinePlay
public override bool OnExiting(ScreenExitEvent e)
{
var subScreen = screenStack.CurrentScreen as Drawable;
if (subScreen?.IsLoaded == true && screenStack.CurrentScreen.OnExiting(e))
return true;
while (screenStack.CurrentScreen != null && screenStack.CurrentScreen is not LoungeSubScreen)
{
var subScreen = (Screen)screenStack.CurrentScreen;
if (subScreen.IsLoaded && subScreen.OnExiting(e))
return true;
subScreen.Exit();
}
RoomManager.PartRoom();

View File

@ -15,11 +15,13 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Audio.Effects;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
@ -48,7 +50,7 @@ namespace osu.Game.Screens.Play
private const float duration = 2500;
private Sample? failSample;
private ISample? failSample;
[Resolved]
private OsuConfigManager config { get; set; } = null!;
@ -73,10 +75,10 @@ namespace osu.Game.Screens.Play
}
[BackgroundDependencyLoader]
private void load(AudioManager audio, IBindable<WorkingBeatmap> beatmap)
private void load(AudioManager audio, ISkinSource skin, IBindable<WorkingBeatmap> beatmap)
{
track = beatmap.Value.Track;
failSample = audio.Samples.Get(@"Gameplay/failsound");
failSample = skin.GetSample(new SampleInfo(@"Gameplay/failsound"));
AddRangeInternal(new Drawable[]
{

View File

@ -0,0 +1,98 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Screens.Play.HUD
{
public partial class BPMCounter : RollingCounter<double>, ISkinnableDrawable
{
protected override double RollingDuration => 750;
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
[Resolved]
private IGameplayClock gameplayClock { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(OsuColour colour)
{
Colour = colour.BlueLighter;
Current.Value = DisplayedCount = 0;
}
protected override void Update()
{
base.Update();
//We don't want it going to 0 when we pause. so we block the updates
if (gameplayClock.IsPaused.Value) return;
// We want to check Rate every update to cover windup/down
Current.Value = beatmap.Value.Beatmap.ControlPointInfo.TimingPointAt(gameplayClock.CurrentTime).BPM * gameplayClock.Rate;
}
protected override OsuSpriteText CreateSpriteText()
=> base.CreateSpriteText().With(s => s.Font = s.Font.With(size: 20f, fixedWidth: true));
protected override LocalisableString FormatCount(double count)
{
return $@"{count:0}";
}
protected override IHasText CreateText() => new TextComponent();
private partial class TextComponent : CompositeDrawable, IHasText
{
public LocalisableString Text
{
get => text.Text;
set => text.Text = value;
}
private readonly OsuSpriteText text;
public TextComponent()
{
AutoSizeAxes = Axes.Both;
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(2),
Children = new Drawable[]
{
text = new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.Numeric.With(size: 16, fixedWidth: true)
},
new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.Numeric.With(size: 8),
Text = @"BPM",
Padding = new MarginPadding { Bottom = 2f }, // align baseline better
}
}
};
}
}
public bool UsesFixedAnchor { get; set; }
}
}

View File

@ -33,6 +33,9 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
Precision = 0.1f,
};
[SettingSource("Show colour bars")]
public Bindable<bool> ColourBarVisibility { get; } = new Bindable<bool>(true);
[SettingSource("Show moving average arrow", "Whether an arrow should move beneath the bar showing the average error.")]
public Bindable<bool> ShowMovingAverage { get; } = new BindableBool(true);
@ -108,6 +111,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
Origin = Anchor.TopCentre,
Width = bar_width,
RelativeSizeAxes = Axes.Y,
Alpha = 0,
Height = 0.5f,
Scale = new Vector2(1, -1),
},
@ -115,6 +119,7 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
{
Anchor = Anchor.Centre,
Origin = Anchor.TopCentre,
Alpha = 0,
Width = bar_width,
RelativeSizeAxes = Axes.Y,
Height = 0.5f,
@ -178,6 +183,11 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
CentreMarkerStyle.BindValueChanged(style => recreateCentreMarker(style.NewValue), true);
LabelStyle.BindValueChanged(style => recreateLabels(style.NewValue), true);
ColourBarVisibility.BindValueChanged(visible =>
{
colourBarsEarly.FadeTo(visible.NewValue ? 1 : 0, 500, Easing.OutQuint);
colourBarsLate.FadeTo(visible.NewValue ? 1 : 0, 500, Easing.OutQuint);
}, true);
// delay the appearance animations for only the initial appearance.
using (arrowContainer.BeginDelayedSequence(450))

View File

@ -64,7 +64,7 @@ namespace osu.Game.Screens.Select
/// <summary>
/// A function to optionally decide on a recommended difficulty from a beatmap set.
/// </summary>
public Func<IEnumerable<BeatmapInfo>, BeatmapInfo>? GetRecommendedBeatmap;
public Func<IEnumerable<BeatmapInfo>, BeatmapInfo?>? GetRecommendedBeatmap;
private CarouselBeatmapSet? selectedBeatmapSet;
@ -119,7 +119,7 @@ namespace osu.Game.Screens.Select
{
CarouselRoot newRoot = new CarouselRoot(this);
newRoot.AddItems(beatmapSets.Select(s => createCarouselSet(s.Detach())).Where(g => g != null));
newRoot.AddItems(beatmapSets.Select(s => createCarouselSet(s.Detach())).OfType<CarouselBeatmapSet>());
root = newRoot;
@ -739,6 +739,8 @@ namespace osu.Game.Screens.Select
foreach (var panel in Scroll.Children)
{
Debug.Assert(panel.Item != null);
if (toDisplay.Remove(panel.Item))
{
// panel already displayed.
@ -770,6 +772,8 @@ namespace osu.Game.Screens.Select
{
updateItem(item);
Debug.Assert(item.Item != null);
if (item.Item.Visible)
{
bool isSelected = item.Item.State.Value == CarouselItemState.Selected;

View File

@ -1,8 +1,6 @@
// 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 disable
using System;
using System.Linq;
using osu.Game.Beatmaps;

View File

@ -1,8 +1,6 @@
// 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 disable
using System;
using System.Collections.Generic;
using System.Linq;
@ -33,7 +31,7 @@ namespace osu.Game.Screens.Select.Carousel
public BeatmapSetInfo BeatmapSet;
public Func<IEnumerable<BeatmapInfo>, BeatmapInfo> GetRecommendedBeatmap;
public Func<IEnumerable<BeatmapInfo>, BeatmapInfo?>? GetRecommendedBeatmap;
public CarouselBeatmapSet(BeatmapSetInfo beatmapSet)
{
@ -47,7 +45,7 @@ namespace osu.Game.Screens.Select.Carousel
.ForEach(AddItem);
}
protected override CarouselItem GetNextToSelect()
protected override CarouselItem? GetNextToSelect()
{
if (LastSelected == null || LastSelected.Filtered.Value)
{
@ -132,8 +130,8 @@ namespace osu.Game.Screens.Select.Carousel
bool filtered = Items.All(i => i.Filtered.Value);
filtered |= criteria.Sort == SortMode.DateRanked && BeatmapSet?.DateRanked == null;
filtered |= criteria.Sort == SortMode.DateSubmitted && BeatmapSet?.DateSubmitted == null;
filtered |= criteria.Sort == SortMode.DateRanked && BeatmapSet.DateRanked == null;
filtered |= criteria.Sort == SortMode.DateSubmitted && BeatmapSet.DateSubmitted == null;
Filtered.Value = filtered;
}

View File

@ -1,8 +1,6 @@
// 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 disable
using System;
using System.Collections.Generic;
using System.Linq;
@ -26,7 +24,7 @@ namespace osu.Game.Screens.Select.Carousel
/// <summary>
/// The last selected item.
/// </summary>
protected CarouselItem LastSelected { get; private set; }
protected CarouselItem? LastSelected { get; private set; }
/// <summary>
/// We need to keep track of the index for cases where the selection is removed but we want to select a new item based on its old location.
@ -112,7 +110,7 @@ namespace osu.Game.Screens.Select.Carousel
/// Finds the item this group would select next if it attempted selection
/// </summary>
/// <returns>An unfiltered item nearest to the last selected one or null if all items are filtered</returns>
protected virtual CarouselItem GetNextToSelect()
protected virtual CarouselItem? GetNextToSelect()
{
if (Items.Count == 0)
return null;
@ -141,7 +139,7 @@ namespace osu.Game.Screens.Select.Carousel
protected virtual void PerformSelection()
{
CarouselItem nextToSelect = GetNextToSelect();
CarouselItem? nextToSelect = GetNextToSelect();
if (nextToSelect != null)
nextToSelect.State.Value = CarouselItemState.Selected;
@ -149,7 +147,7 @@ namespace osu.Game.Screens.Select.Carousel
updateSelected(null);
}
private void updateSelected(CarouselItem newSelection)
private void updateSelected(CarouselItem? newSelection)
{
if (newSelection != null)
LastSelected = newSelection;

View File

@ -1,8 +1,6 @@
// 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 disable
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@ -95,9 +93,9 @@ namespace osu.Game.Screens.Select.Carousel
public partial class HoverLayer : HoverSampleDebounceComponent
{
private Sample sampleHover;
private Sample? sampleHover;
private Box box;
private Box box = null!;
public HoverLayer()
{

View File

@ -1,8 +1,6 @@
// 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 disable
using System;
using osu.Framework.Bindables;
@ -43,7 +41,7 @@ namespace osu.Game.Screens.Select.Carousel
/// <summary>
/// Create a fresh drawable version of this item.
/// </summary>
public abstract DrawableCarouselItem CreateDrawableRepresentation();
public abstract DrawableCarouselItem? CreateDrawableRepresentation();
public virtual void Filter(FilterCriteria criteria)
{
@ -51,7 +49,12 @@ namespace osu.Game.Screens.Select.Carousel
public virtual int CompareTo(FilterCriteria criteria, CarouselItem other) => ItemID.CompareTo(other.ItemID);
public int CompareTo(CarouselItem other) => CarouselYPosition.CompareTo(other.CarouselYPosition);
public int CompareTo(CarouselItem? other)
{
if (other == null) return 1;
return CarouselYPosition.CompareTo(other.CarouselYPosition);
}
}
public enum CarouselItemState

View File

@ -1,8 +1,6 @@
// 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 disable
using System;
using System.Collections.Generic;
using System.Linq;
@ -47,31 +45,31 @@ namespace osu.Game.Screens.Select.Carousel
private readonly BeatmapInfo beatmapInfo;
private Sprite background;
private Sprite background = null!;
private Action<BeatmapInfo> startRequested;
private Action<BeatmapInfo> editRequested;
private Action<BeatmapInfo> hideRequested;
private Action<BeatmapInfo>? startRequested;
private Action<BeatmapInfo>? editRequested;
private Action<BeatmapInfo>? hideRequested;
private Triangles triangles;
private Triangles triangles = null!;
private StarCounter starCounter;
private DifficultyIcon difficultyIcon;
[Resolved(CanBeNull = true)]
private BeatmapSetOverlay beatmapOverlay { get; set; }
private StarCounter starCounter = null!;
private DifficultyIcon difficultyIcon = null!;
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; }
[Resolved(CanBeNull = true)]
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
private BeatmapSetOverlay? beatmapOverlay { get; set; }
[Resolved]
private RealmAccess realm { get; set; }
private BeatmapDifficultyCache difficultyCache { get; set; } = null!;
private IBindable<StarDifficulty?> starDifficultyBindable;
private CancellationTokenSource starDifficultyCancellationSource;
[Resolved]
private ManageCollectionsDialog? manageCollectionsDialog { get; set; }
[Resolved]
private RealmAccess realm { get; set; } = null!;
private IBindable<StarDifficulty?> starDifficultyBindable = null!;
private CancellationTokenSource? starDifficultyCancellationSource;
public DrawableCarouselBeatmap(CarouselBeatmap panel)
{
@ -79,8 +77,8 @@ namespace osu.Game.Screens.Select.Carousel
Item = panel;
}
[BackgroundDependencyLoader(true)]
private void load(BeatmapManager manager, SongSelect songSelect)
[BackgroundDependencyLoader]
private void load(BeatmapManager? manager, SongSelect? songSelect)
{
Header.Height = height;
@ -194,7 +192,7 @@ namespace osu.Game.Screens.Select.Carousel
protected override bool OnClick(ClickEvent e)
{
if (Item.State.Value == CarouselItemState.Selected)
if (Item?.State.Value == CarouselItemState.Selected)
startRequested?.Invoke(beatmapInfo);
return base.OnClick(e);
@ -202,13 +200,13 @@ namespace osu.Game.Screens.Select.Carousel
protected override void ApplyState()
{
if (Item.State.Value != CarouselItemState.Collapsed && Alpha == 0)
if (Item?.State.Value != CarouselItemState.Collapsed && Alpha == 0)
starCounter.ReplayAnimation();
starDifficultyCancellationSource?.Cancel();
// Only compute difficulty when the item is visible.
if (Item.State.Value != CarouselItemState.Collapsed)
if (Item?.State.Value != CarouselItemState.Collapsed)
{
// We've potentially cancelled the computation above so a new bindable is required.
starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmapInfo, (starDifficultyCancellationSource = new CancellationTokenSource()).Token);

View File

@ -1,14 +1,11 @@
// 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 disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -27,30 +24,28 @@ namespace osu.Game.Screens.Select.Carousel
{
public const float HEIGHT = MAX_HEIGHT;
private Action<BeatmapSetInfo> restoreHiddenRequested;
private Action<int> viewDetails;
[Resolved(CanBeNull = true)]
private IDialogOverlay dialogOverlay { get; set; }
[Resolved(CanBeNull = true)]
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
private Action<BeatmapSetInfo> restoreHiddenRequested = null!;
private Action<int>? viewDetails;
[Resolved]
private RealmAccess realm { get; set; }
private IDialogOverlay? dialogOverlay { get; set; }
[Resolved]
private ManageCollectionsDialog? manageCollectionsDialog { get; set; }
[Resolved]
private RealmAccess realm { get; set; } = null!;
public IEnumerable<DrawableCarouselItem> DrawableBeatmaps => beatmapContainer?.IsLoaded != true ? Enumerable.Empty<DrawableCarouselItem>() : beatmapContainer.AliveChildren;
[CanBeNull]
private Container<DrawableCarouselItem> beatmapContainer;
private Container<DrawableCarouselItem>? beatmapContainer;
private BeatmapSetInfo beatmapSet;
private BeatmapSetInfo beatmapSet = null!;
[CanBeNull]
private Task beatmapsLoadTask;
private Task? beatmapsLoadTask;
[Resolved]
private BeatmapManager manager { get; set; }
private BeatmapManager manager { get; set; } = null!;
protected override void FreeAfterUse()
{
@ -61,8 +56,8 @@ namespace osu.Game.Screens.Select.Carousel
ClearTransforms();
}
[BackgroundDependencyLoader(true)]
private void load(BeatmapSetOverlay beatmapOverlay)
[BackgroundDependencyLoader]
private void load(BeatmapSetOverlay? beatmapOverlay)
{
restoreHiddenRequested = s =>
{
@ -78,10 +73,11 @@ namespace osu.Game.Screens.Select.Carousel
{
base.Update();
Debug.Assert(Item != null);
// position updates should not occur if the item is filtered away.
// this avoids panels flying across the screen only to be eventually off-screen or faded out.
if (!Item.Visible)
return;
if (!Item.Visible) return;
float targetY = Item.CarouselYPosition;
@ -151,6 +147,8 @@ namespace osu.Game.Screens.Select.Carousel
private void updateBeatmapDifficulties()
{
Debug.Assert(Item != null);
var carouselBeatmapSet = (CarouselBeatmapSet)Item;
var visibleBeatmaps = carouselBeatmapSet.Items.Where(c => c.Visible).ToArray();
@ -171,7 +169,7 @@ namespace osu.Game.Screens.Select.Carousel
{
X = 100,
RelativeSizeAxes = Axes.Both,
ChildrenEnumerable = visibleBeatmaps.Select(c => c.CreateDrawableRepresentation())
ChildrenEnumerable = visibleBeatmaps.Select(c => c.CreateDrawableRepresentation()!)
};
beatmapsLoadTask = LoadComponentAsync(beatmapContainer, loaded =>
@ -196,10 +194,12 @@ namespace osu.Game.Screens.Select.Carousel
float yPos = DrawableCarouselBeatmap.CAROUSEL_BEATMAP_SPACING;
bool isSelected = Item.State.Value == CarouselItemState.Selected;
bool isSelected = Item?.State.Value == CarouselItemState.Selected;
foreach (var panel in beatmapContainer.Children)
{
Debug.Assert(panel.Item != null);
if (isSelected)
{
panel.MoveToY(yPos, 800, Easing.OutQuint);
@ -218,7 +218,7 @@ namespace osu.Game.Screens.Select.Carousel
List<MenuItem> items = new List<MenuItem>();
if (Item.State.Value == CarouselItemState.NotSelected)
if (Item?.State.Value == CarouselItemState.NotSelected)
items.Add(new OsuMenuItem("Expand", MenuItemType.Highlighted, () => Item.State.Value = CarouselItemState.Selected));
if (beatmapSet.OnlineID > 0 && viewDetails != null)

View File

@ -1,8 +1,6 @@
// 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 disable
using System.Diagnostics;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -34,9 +32,9 @@ namespace osu.Game.Screens.Select.Carousel
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
Header.ReceivePositionalInputAt(screenSpacePos);
private CarouselItem item;
private CarouselItem? item;
public CarouselItem Item
public CarouselItem? Item
{
get => item;
set
@ -105,7 +103,7 @@ namespace osu.Game.Screens.Select.Carousel
protected virtual void UpdateItem()
{
if (item == null)
if (Item == null)
return;
Scheduler.AddOnce(ApplyState);
@ -128,12 +126,12 @@ namespace osu.Game.Screens.Select.Carousel
protected virtual void ApplyState()
{
Debug.Assert(Item != null);
// Use the fact that we know the precise height of the item from the model to avoid the need for AutoSize overhead.
// Additionally, AutoSize doesn't work well due to content starting off-screen and being masked away.
Height = Item.TotalHeight;
Debug.Assert(Item != null);
switch (Item.State.Value)
{
case CarouselItemState.NotSelected:
@ -162,6 +160,8 @@ namespace osu.Game.Screens.Select.Carousel
protected override bool OnClick(ClickEvent e)
{
Debug.Assert(Item != null);
Item.State.Value = CarouselItemState.Selected;
return true;
}

View File

@ -1,8 +1,6 @@
// 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 disable
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;

View File

@ -1,8 +1,6 @@
// 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 disable
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;

View File

@ -1,8 +1,6 @@
// 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 disable
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;

View File

@ -1,8 +1,6 @@
// 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 disable
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;

View File

@ -6,14 +6,21 @@ using osu.Framework.Audio.Track;
using osu.Framework.Graphics;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
using osuTK.Graphics;
namespace osu.Game.Skinning
{
public partial class LegacyKiaiFlashingDrawable : BeatSyncedContainer
{
public Color4 KiaiGlowColour
{
get => flashingDrawable.Colour;
set => flashingDrawable.Colour = value;
}
private readonly Drawable flashingDrawable;
private const float flash_opacity = 0.55f;
private const float flash_opacity = 0.3f;
public LegacyKiaiFlashingDrawable(Func<Drawable?> creationFunc)
{