1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 06:52:55 +08:00

Merge branch 'master' into comment-editor-1

This commit is contained in:
Bartłomiej Dach 2023-01-13 22:41:41 +01:00 committed by GitHub
commit ab3d39a940
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
142 changed files with 1986 additions and 765 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

@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
{
drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
drawableTaikoRuleset.LockPlayfieldAspect.Value = false;
drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false;
var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
playfield.ClassicHitTargetPosition.Value = true;

View File

@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.UI
{
public new BindableDouble TimeRange => base.TimeRange;
public readonly BindableBool LockPlayfieldAspect = new BindableBool(true);
public readonly BindableBool LockPlayfieldMaxAspect = new BindableBool(true);
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Taiko.UI
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer
{
LockPlayfieldAspect = { BindTarget = LockPlayfieldAspect }
LockPlayfieldMaxAspect = { BindTarget = LockPlayfieldMaxAspect }
};
protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.UI
private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
private const float default_aspect = 16f / 9f;
public readonly IBindable<bool> LockPlayfieldAspect = new BindableBool(true);
public readonly IBindable<bool> LockPlayfieldMaxAspect = new BindableBool(true);
protected override void Update()
{
@ -21,7 +21,12 @@ namespace osu.Game.Rulesets.Taiko.UI
float height = default_relative_height;
if (LockPlayfieldAspect.Value)
// Players coming from stable expect to be able to change the aspect ratio regardless of the window size.
// We originally wanted to limit this more, but there was considerable pushback from the community.
//
// As a middle-ground, the aspect ratio can still be adjusted in the downwards direction but has a maximum limit.
// This is still a bit weird, because readability changes with window size, but it is what it is.
if (LockPlayfieldMaxAspect.Value && Parent.ChildSize.X / Parent.ChildSize.Y > default_aspect)
height *= Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
Height = height;

View File

@ -75,6 +75,8 @@ namespace osu.Game.Tests.Chat
return false;
};
});
AddUntilStep("wait for notifications client", () => channelManager.NotificationsConnected);
}
[Test]

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

@ -181,7 +181,8 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
AddStep("exit", () => Player.Exit());
AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false);
AddUntilStep("wait for submission", () => Player.SubmittedScore != null);
AddAssert("ensure failing submission", () => Player.SubmittedScore.ScoreInfo.Passed == false);
}
[Test]
@ -209,7 +210,9 @@ namespace osu.Game.Tests.Visual.Gameplay
addFakeHit();
AddStep("exit", () => Player.Exit());
AddAssert("ensure failing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == false);
AddUntilStep("wait for submission", () => Player.SubmittedScore != null);
AddAssert("ensure failing submission", () => Player.SubmittedScore.ScoreInfo.Passed == false);
}
[Test]

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

@ -0,0 +1,154 @@
// 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 System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneSegmentedGraph : OsuTestScene
{
private readonly SegmentedGraph<int> graph;
public TestSceneSegmentedGraph()
{
Children = new Drawable[]
{
graph = new SegmentedGraph<int>(6)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(1, 0.5f),
},
};
graph.TierColours = new[]
{
Colour4.Red,
Colour4.OrangeRed,
Colour4.Orange,
Colour4.Yellow,
Colour4.YellowGreen,
Colour4.Green
};
AddStep("values from 1-10", () => graph.Values = Enumerable.Range(1, 10).ToArray());
AddStep("values from 1-100", () => graph.Values = Enumerable.Range(1, 100).ToArray());
AddStep("values from 1-500", () => graph.Values = Enumerable.Range(1, 500).ToArray());
AddStep("sin() function of size 100", () => sinFunction());
AddStep("sin() function of size 500", () => sinFunction(500));
AddStep("bumps of size 100", () => bumps());
AddStep("bumps of size 500", () => bumps(500));
AddStep("100 random values", () => randomValues());
AddStep("500 random values", () => randomValues(500));
AddStep("beatmap density with granularity of 200", () => beatmapDensity());
AddStep("beatmap density with granularity of 300", () => beatmapDensity(300));
AddStep("reversed values from 1-10", () => graph.Values = Enumerable.Range(1, 10).Reverse().ToArray());
AddStep("change colour", () =>
{
graph.TierColours = new[]
{
Colour4.White,
Colour4.LightBlue,
Colour4.Aqua,
Colour4.Blue
};
});
AddStep("reset colour", () =>
{
graph.TierColours = new[]
{
Colour4.Red,
Colour4.OrangeRed,
Colour4.Orange,
Colour4.Yellow,
Colour4.YellowGreen,
Colour4.Green
};
});
}
private void sinFunction(int size = 100)
{
const int max_value = 255;
graph.Values = new int[size];
float step = 2 * MathF.PI / size;
float x = 0;
for (int i = 0; i < size; i++)
{
graph.Values[i] = (int)(max_value * MathF.Sin(x));
x += step;
}
}
private void bumps(int size = 100)
{
const int max_value = 255;
graph.Values = new int[size];
float step = 2 * MathF.PI / size;
float x = 0;
for (int i = 0; i < size; i++)
{
graph.Values[i] = (int)(max_value * Math.Abs(MathF.Sin(x)));
x += step;
}
}
private void randomValues(int size = 100)
{
Random rng = new Random();
graph.Values = new int[size];
for (int i = 0; i < size; i++)
{
graph.Values[i] = rng.Next(255);
}
}
private void beatmapDensity(int granularity = 200)
{
var ruleset = new OsuRuleset();
var beatmap = CreateBeatmap(ruleset.RulesetInfo);
IEnumerable<HitObject> objects = beatmap.HitObjects;
// Taken from SongProgressGraph
graph.Values = new int[granularity];
if (!objects.Any())
return;
double firstHit = objects.First().StartTime;
double lastHit = objects.Max(o => o.GetEndTime());
if (lastHit == 0)
lastHit = objects.Last().StartTime;
double interval = (lastHit - firstHit + 1) / granularity;
foreach (var h in objects)
{
double endTime = h.GetEndTime();
Debug.Assert(endTime >= h.StartTime);
int startRange = (int)((h.StartTime - firstHit) / interval);
int endRange = (int)((endTime - firstHit) / interval);
for (int i = startRange; i <= endRange; i++)
graph.Values[i]++;
}
}
}
}

View File

@ -44,6 +44,16 @@ namespace osu.Game.Beatmaps
public Action<(BeatmapSetInfo beatmapSet, bool isBatch)>? ProcessBeatmap { private get; set; }
public override bool PauseImports
{
get => base.PauseImports;
set
{
base.PauseImports = value;
beatmapImporter.PauseImports = value;
}
}
public BeatmapManager(Storage storage, RealmAccess realm, IAPIProvider? api, AudioManager audioManager, IResourceStore<byte[]> gameResources, GameHost? host = null,
WorkingBeatmap? defaultBeatmap = null, BeatmapDifficultyCache? difficultyCache = null, bool performOnlineLookups = false)
: base(storage, realm)
@ -458,7 +468,8 @@ namespace osu.Game.Beatmaps
public Task Import(ImportTask[] tasks, ImportParameters parameters = default) => beatmapImporter.Import(tasks, parameters);
public Task<IEnumerable<Live<BeatmapSetInfo>>> Import(ProgressNotification notification, ImportTask[] tasks, ImportParameters parameters = default) => beatmapImporter.Import(notification, tasks, parameters);
public Task<IEnumerable<Live<BeatmapSetInfo>>> Import(ProgressNotification notification, ImportTask[] tasks, ImportParameters parameters = default) =>
beatmapImporter.Import(notification, tasks, parameters);
public Task<Live<BeatmapSetInfo>?> Import(ImportTask task, ImportParameters parameters = default, CancellationToken cancellationToken = default) =>
beatmapImporter.Import(task, parameters, cancellationToken);

View File

@ -5,16 +5,19 @@ using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Localisation;
namespace osu.Game.Beatmaps.Drawables.Cards
{
public abstract partial class BeatmapCard : OsuClickableContainer
public abstract partial class BeatmapCard : OsuClickableContainer, IHasContextMenu
{
public const float TRANSITION_DURATION = 400;
public const float CORNER_RADIUS = 10;
@ -96,5 +99,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards
throw new ArgumentOutOfRangeException(nameof(size), size, @"Unsupported card size");
}
}
public MenuItem[] ContextMenuItems => new MenuItem[]
{
new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, Action),
};
}
}

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

@ -54,14 +54,14 @@ namespace osu.Game.Database
public void UpdateStorage(string stablePath) => cachedStorage = new StableStorage(stablePath, gameHost as DesktopGameHost);
public bool CheckHardLinkAvailability()
public bool CheckSongsFolderHardLinkAvailability()
{
var stableStorage = GetCurrentStableStorage();
if (stableStorage == null || gameHost is not DesktopGameHost desktopGameHost)
return false;
string testExistingPath = stableStorage.GetFullPath(string.Empty);
string testExistingPath = stableStorage.GetSongStorage().GetFullPath(string.Empty);
string testDestinationPath = desktopGameHost.Storage.GetFullPath(string.Empty);
return HardLinkHelper.CheckAvailability(testDestinationPath, testExistingPath);

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

@ -18,6 +18,11 @@ namespace osu.Game.Database
public class ModelManager<TModel> : IModelManager<TModel>, IModelFileManager<TModel, RealmNamedFileUsage>
where TModel : RealmObject, IHasRealmFiles, IHasGuidPrimaryKey, ISoftDelete
{
/// <summary>
/// Temporarily pause imports to avoid performance overheads affecting gameplay scenarios.
/// </summary>
public virtual bool PauseImports { get; set; }
protected RealmAccess Realm { get; }
private readonly RealmFileStore realmFileStore;

View File

@ -56,6 +56,11 @@ namespace osu.Game.Database
/// </summary>
private static readonly ThreadedTaskScheduler import_scheduler_batch = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(RealmArchiveModelImporter<TModel>));
/// <summary>
/// Temporarily pause imports to avoid performance overheads affecting gameplay scenarios.
/// </summary>
public bool PauseImports { get; set; }
public abstract IEnumerable<string> HandledExtensions { get; }
protected readonly RealmFileStore Files;
@ -149,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)
{
@ -253,7 +261,7 @@ namespace osu.Game.Database
/// <param name="cancellationToken">An optional cancellation token.</param>
public virtual Live<TModel>? ImportModel(TModel item, ArchiveReader? archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default) => Realm.Run(realm =>
{
cancellationToken.ThrowIfCancellationRequested();
pauseIfNecessary(cancellationToken);
TModel? existing;
@ -551,6 +559,23 @@ namespace osu.Game.Database
/// <returns>Whether to perform deletion.</returns>
protected virtual bool ShouldDeleteArchive(string path) => false;
private void pauseIfNecessary(CancellationToken cancellationToken)
{
if (!PauseImports)
return;
Logger.Log($@"{GetType().Name} is being paused.");
while (PauseImports)
{
cancellationToken.ThrowIfCancellationRequested();
Thread.Sleep(500);
}
cancellationToken.ThrowIfCancellationRequested();
Logger.Log($@"{GetType().Name} is being resumed.");
}
private IEnumerable<string> getIDs(IEnumerable<INamedFile> files)
{
foreach (var f in files.OrderBy(f => f.Filename))

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

@ -4,6 +4,7 @@
#nullable disable
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
@ -117,11 +118,11 @@ namespace osu.Game.Graphics
host.GetClipboard()?.SetImage(image);
string filename = getFilename();
(string filename, var stream) = getWritableStream();
if (filename == null) return;
using (var stream = storage.CreateFileSafely(filename))
using (stream)
{
switch (screenshotFormat.Value)
{
@ -142,7 +143,7 @@ namespace osu.Game.Graphics
notificationOverlay.Post(new SimpleNotification
{
Text = $"{filename} saved!",
Text = $"Screenshot {filename} saved!",
Activated = () =>
{
storage.PresentFileExternally(filename);
@ -152,23 +153,28 @@ namespace osu.Game.Graphics
}
});
private string getFilename()
private static readonly object filename_reservation_lock = new object();
private (string filename, Stream stream) getWritableStream()
{
var dt = DateTime.Now;
string fileExt = screenshotFormat.ToString().ToLowerInvariant();
string withoutIndex = $"osu_{dt:yyyy-MM-dd_HH-mm-ss}.{fileExt}";
if (!storage.Exists(withoutIndex))
return withoutIndex;
for (ulong i = 1; i < ulong.MaxValue; i++)
lock (filename_reservation_lock)
{
string indexedName = $"osu_{dt:yyyy-MM-dd_HH-mm-ss}-{i}.{fileExt}";
if (!storage.Exists(indexedName))
return indexedName;
}
var dt = DateTime.Now;
string fileExt = screenshotFormat.ToString().ToLowerInvariant();
return null;
string withoutIndex = $"osu_{dt:yyyy-MM-dd_HH-mm-ss}.{fileExt}";
if (!storage.Exists(withoutIndex))
return (withoutIndex, storage.GetStream(withoutIndex, FileAccess.Write, FileMode.Create));
for (ulong i = 1; i < ulong.MaxValue; i++)
{
string indexedName = $"osu_{dt:yyyy-MM-dd_HH-mm-ss}-{i}.{fileExt}";
if (!storage.Exists(indexedName))
return (indexedName, storage.GetStream(indexedName, FileAccess.Write, FileMode.Create));
}
return (null, null);
}
}
}
}

View File

@ -0,0 +1,337 @@
// 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 System.Collections;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Textures;
using osuTK;
namespace osu.Game.Graphics.UserInterface
{
public partial class SegmentedGraph<T> : Drawable
where T : struct, IComparable<T>, IConvertible, IEquatable<T>
{
private bool graphNeedsUpdate;
private T[]? values;
private int[] tiers = Array.Empty<int>();
private readonly SegmentManager segments;
private int tierCount;
public SegmentedGraph(int tierCount = 1)
{
this.tierCount = tierCount;
tierColours = new[]
{
new Colour4(0, 0, 0, 0)
};
segments = new SegmentManager(tierCount);
}
public T[] Values
{
get => values ?? Array.Empty<T>();
set
{
if (value == values) return;
values = value;
graphNeedsUpdate = true;
}
}
private Colour4[] tierColours;
public Colour4[] TierColours
{
get => tierColours;
set
{
if (value.Length == 0 || value == tierColours)
return;
tierCount = value.Length;
tierColours = value;
graphNeedsUpdate = true;
}
}
private Texture texture = null!;
private IShader shader = null!;
[BackgroundDependencyLoader]
private void load(IRenderer renderer, ShaderManager shaders)
{
texture = renderer.WhitePixel;
shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
}
protected override void Update()
{
base.Update();
if (graphNeedsUpdate)
{
recalculateTiers(values);
recalculateSegments();
Invalidate(Invalidation.DrawNode);
graphNeedsUpdate = false;
}
}
private void recalculateTiers(T[]? arr)
{
if (arr == null || arr.Length == 0)
{
tiers = Array.Empty<int>();
return;
}
float[] floatValues = arr.Select(v => Convert.ToSingle(v)).ToArray();
// Shift values to eliminate negative ones
float min = floatValues.Min();
if (min < 0)
{
for (int i = 0; i < floatValues.Length; i++)
floatValues[i] += Math.Abs(min);
}
// Normalize values
float max = floatValues.Max();
for (int i = 0; i < floatValues.Length; i++)
floatValues[i] /= max;
// Deduce tiers from values
tiers = floatValues.Select(v => (int)Math.Floor(v * tierCount)).ToArray();
}
private void recalculateSegments()
{
segments.Clear();
if (tiers.Length == 0)
{
segments.Add(0, 0, 1);
return;
}
for (int i = 0; i < tiers.Length; i++)
{
for (int tier = 0; tier < tierCount; tier++)
{
if (tier < 0)
continue;
// One tier covers itself and all tiers above it.
// By layering multiple transparent boxes, higher tiers will be brighter.
// If using opaque colors, higher tiers will be on front, covering lower tiers.
if (tiers[i] >= tier)
{
if (!segments.IsTierStarted(tier))
segments.StartSegment(tier, i * 1f / tiers.Length);
}
else
{
if (segments.IsTierStarted(tier))
segments.EndSegment(tier, i * 1f / tiers.Length);
}
}
}
segments.EndAllPendingSegments();
segments.Sort();
}
private Colour4 getTierColour(int tier) => tier >= 0 ? tierColours[tier] : new Colour4(0, 0, 0, 0);
protected override DrawNode CreateDrawNode() => new SegmentedGraphDrawNode(this);
protected struct SegmentInfo
{
/// <summary>
/// The tier this segment is at.
/// </summary>
public int Tier;
/// <summary>
/// The progress at which this segment starts.
/// </summary>
/// <remarks>
/// The value is a normalized float (from 0 to 1).
/// </remarks>
public float Start;
/// <summary>
/// The progress at which this segment ends.
/// </summary>
/// <remarks>
/// The value is a normalized float (from 0 to 1).
/// </remarks>
public float End;
/// <summary>
/// The length of this segment.
/// </summary>
/// <remarks>
/// The value is a normalized float (from 0 to 1).
/// </remarks>
public float Length => End - Start;
public override string ToString()
{
return $"({Tier}, {Start * 100}%, {End * 100}%)";
}
}
private class SegmentedGraphDrawNode : DrawNode
{
public new SegmentedGraph<T> Source => (SegmentedGraph<T>)base.Source;
private Texture texture = null!;
private IShader shader = null!;
private readonly List<SegmentInfo> segments = new List<SegmentInfo>();
private Vector2 drawSize;
public SegmentedGraphDrawNode(SegmentedGraph<T> source)
: base(source)
{
}
public override void ApplyState()
{
base.ApplyState();
texture = Source.texture;
shader = Source.shader;
drawSize = Source.DrawSize;
segments.Clear();
segments.AddRange(Source.segments.Where(s => s.Length * drawSize.X > 1));
}
public override void Draw(IRenderer renderer)
{
base.Draw(renderer);
shader.Bind();
foreach (SegmentInfo segment in segments)
{
Vector2 topLeft = new Vector2(segment.Start * drawSize.X, 0);
Vector2 topRight = new Vector2(segment.End * drawSize.X, 0);
Vector2 bottomLeft = new Vector2(segment.Start * drawSize.X, drawSize.Y);
Vector2 bottomRight = new Vector2(segment.End * drawSize.X, drawSize.Y);
renderer.DrawQuad(
texture,
new Quad(
Vector2Extensions.Transform(topLeft, DrawInfo.Matrix),
Vector2Extensions.Transform(topRight, DrawInfo.Matrix),
Vector2Extensions.Transform(bottomLeft, DrawInfo.Matrix),
Vector2Extensions.Transform(bottomRight, DrawInfo.Matrix)),
Source.getTierColour(segment.Tier));
}
shader.Unbind();
}
}
protected class SegmentManager : IEnumerable<SegmentInfo>
{
private readonly List<SegmentInfo> segments = new List<SegmentInfo>();
private readonly SegmentInfo?[] pendingSegments;
public SegmentManager(int tierCount)
{
pendingSegments = new SegmentInfo?[tierCount];
}
public void StartSegment(int tier, float start)
{
if (pendingSegments[tier] != null)
throw new InvalidOperationException($"Another {nameof(SegmentInfo)} of tier {tier.ToString()} has already been started.");
pendingSegments[tier] = new SegmentInfo
{
Tier = tier,
Start = Math.Clamp(start, 0, 1)
};
}
public void EndSegment(int tier, float end)
{
SegmentInfo? pendingSegment = pendingSegments[tier];
if (pendingSegment == null)
throw new InvalidOperationException($"Cannot end {nameof(SegmentInfo)} of tier {tier.ToString()} that has not been started.");
SegmentInfo segment = pendingSegment.Value;
segment.End = Math.Clamp(end, 0, 1);
segments.Add(segment);
pendingSegments[tier] = null;
}
public void EndAllPendingSegments()
{
foreach (SegmentInfo? pendingSegment in pendingSegments)
{
if (pendingSegment == null)
continue;
SegmentInfo finalizedSegment = pendingSegment.Value;
finalizedSegment.End = 1;
segments.Add(finalizedSegment);
}
}
public void Sort() =>
segments.Sort((a, b) =>
a.Tier != b.Tier
? a.Tier.CompareTo(b.Tier)
: a.Start.CompareTo(b.Start));
public void Add(SegmentInfo segment) => segments.Add(segment);
public void Clear()
{
segments.Clear();
for (int i = 0; i < pendingSegments.Length; i++)
pendingSegments[i] = null;
}
public int Count => segments.Count;
public void Add(int tier, float start, float end)
{
SegmentInfo segment = new SegmentInfo
{
Tier = tier,
Start = Math.Clamp(start, 0, 1),
End = Math.Clamp(end, 0, 1)
};
if (segment.Start > segment.End)
throw new InvalidOperationException("Segment start cannot be after segment end.");
Add(segment);
}
public bool IsTierStarted(int tier) => tier >= 0 && pendingSegments[tier].HasValue;
public IEnumerator<SegmentInfo> GetEnumerator() => segments.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
}

View File

@ -0,0 +1,24 @@
// 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.Localisation;
namespace osu.Game.Localisation
{
public static class ContextMenuStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.ContextMenu";
/// <summary>
/// "View profile"
/// </summary>
public static LocalisableString ViewProfile => new TranslatableString(getKey(@"view_profile"), @"View profile");
/// <summary>
/// "View beatmap"
/// </summary>
public static LocalisableString ViewBeatmap => new TranslatableString(getKey(@"view_beatmap"), @"View beatmap");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

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

@ -329,12 +329,35 @@ namespace osu.Game.Online.API
{
try
{
return JObject.Parse(req.GetResponseString().AsNonNull()).SelectToken("form_error", true).AsNonNull().ToObject<RegistrationRequest.RegistrationRequestErrors>();
return JObject.Parse(req.GetResponseString().AsNonNull()).SelectToken(@"form_error", true).AsNonNull().ToObject<RegistrationRequest.RegistrationRequestErrors>();
}
catch
{
// if we couldn't deserialize the error message let's throw the original exception outwards.
e.Rethrow();
try
{
// attempt to parse a non-form error message
var response = JObject.Parse(req.GetResponseString().AsNonNull());
string redirect = (string)response.SelectToken(@"url", true);
string message = (string)response.SelectToken(@"error", false);
if (!string.IsNullOrEmpty(redirect))
{
return new RegistrationRequest.RegistrationRequestErrors
{
Redirect = redirect,
Message = message,
};
}
// if we couldn't deserialize the error message let's throw the original exception outwards.
e.Rethrow();
}
catch
{
// if we couldn't deserialize the error message let's throw the original exception outwards.
e.Rethrow();
}
}
}

View File

@ -1,17 +1,16 @@
// 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 Newtonsoft.Json;
namespace osu.Game.Online.API
{
public class RegistrationRequest : OsuWebRequest
{
internal string Username;
internal string Email;
internal string Password;
internal string Username = string.Empty;
internal string Email = string.Empty;
internal string Password = string.Empty;
protected override void PrePerform()
{
@ -24,18 +23,28 @@ namespace osu.Game.Online.API
public class RegistrationRequestErrors
{
public UserErrors User;
/// <summary>
/// An optional error message.
/// </summary>
public string? Message;
/// <summary>
/// An optional URL which the user should be directed towards to complete registration.
/// </summary>
public string? Redirect;
public UserErrors? User;
public class UserErrors
{
[JsonProperty("username")]
public string[] Username;
public string[] Username = Array.Empty<string>();
[JsonProperty("user_email")]
public string[] Email;
public string[] Email = Array.Empty<string>();
[JsonProperty("password")]
public string[] Password;
public string[] Password = Array.Empty<string>();
}
}
}

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

@ -64,6 +64,11 @@ namespace osu.Game.Online.Chat
/// </summary>
public IBindableList<Channel> AvailableChannels => availableChannels;
/// <summary>
/// Whether the client responsible for channel notifications is connected.
/// </summary>
public bool NotificationsConnected => connector.IsConnected.Value;
private readonly IAPIProvider api;
private readonly NotificationsClientConnector connector;
@ -71,7 +76,6 @@ namespace osu.Game.Online.Chat
private UserLookupCache users { get; set; }
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
private bool channelsInitialised;
private ScheduledDelegate scheduledAck;
private long? lastSilenceMessageId;
@ -95,15 +99,7 @@ namespace osu.Game.Online.Chat
connector.NewMessages += msgs => Schedule(() => addMessages(msgs));
connector.PresenceReceived += () => Schedule(() =>
{
if (!channelsInitialised)
{
channelsInitialised = true;
// we want this to run after the first presence so we can see if the user is in any channels already.
initializeChannels();
}
});
connector.PresenceReceived += () => Schedule(initializeChannels);
connector.Start();
@ -335,6 +331,11 @@ namespace osu.Game.Online.Chat
private void initializeChannels()
{
// This request is self-retrying until it succeeds.
// To avoid requests piling up when not logged in (ie. API is unavailable) exit early.
if (!api.IsLoggedIn)
return;
var req = new ListChannelsRequest();
bool joinDefaults = JoinedChannels.Count == 0;
@ -350,10 +351,11 @@ namespace osu.Game.Online.Chat
joinChannel(ch);
}
};
req.Failure += error =>
{
Logger.Error(error, "Fetching channel list failed");
initializeChannels();
Scheduler.AddDelayed(initializeChannels, 60000);
};
api.Queue(req);

View File

@ -307,6 +307,13 @@ namespace osu.Game
// Transfer any runtime changes back to configuration file.
SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID.ToString();
LocalUserPlaying.BindValueChanged(p =>
{
BeatmapManager.PauseImports = p.NewValue;
SkinManager.PauseImports = p.NewValue;
ScoreManager.PauseImports = p.NewValue;
}, true);
IsActive.BindValueChanged(active => updateActiveState(active.NewValue), true);
Audio.AddAdjustment(AdjustableProperty.Volume, inactiveVolumeFade);
@ -926,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

@ -47,6 +47,9 @@ namespace osu.Game.Overlays.AccountCreation
[Resolved]
private GameHost host { get; set; }
[Resolved]
private OsuGame game { get; set; }
[BackgroundDependencyLoader]
private void load()
{
@ -194,9 +197,20 @@ namespace osu.Game.Overlays.AccountCreation
{
if (errors != null)
{
usernameDescription.AddErrors(errors.User.Username);
emailAddressDescription.AddErrors(errors.User.Email);
passwordDescription.AddErrors(errors.User.Password);
if (errors.User != null)
{
usernameDescription.AddErrors(errors.User.Username);
emailAddressDescription.AddErrors(errors.User.Email);
passwordDescription.AddErrors(errors.User.Password);
}
if (!string.IsNullOrEmpty(errors.Redirect))
{
if (!string.IsNullOrEmpty(errors.Message))
passwordDescription.AddErrors(new[] { errors.Message });
game.OpenUrlExternally($"{errors.Redirect}?username={usernameTextBox.Text}&email={emailTextBox.Text}");
}
}
else
{

View File

@ -6,6 +6,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Extensions;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
@ -28,7 +29,13 @@ namespace osu.Game.Overlays.BeatmapListing
AddTabItem(new RulesetFilterTabItemAny());
foreach (var r in rulesets.AvailableRulesets)
{
// Don't display non-legacy rulesets
if (!r.IsLegacyRuleset())
continue;
AddItem(r);
}
}
}

View File

@ -10,7 +10,6 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Game.Graphics.Cursor;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
@ -91,79 +90,74 @@ namespace osu.Game.Overlays.BeatmapSet
},
},
},
new OsuContextMenuContainer
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = new Container
Padding = new MarginPadding
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding
Vertical = BeatmapSetOverlay.Y_PADDING,
Left = BeatmapSetOverlay.X_PADDING,
Right = BeatmapSetOverlay.X_PADDING + BeatmapSetOverlay.RIGHT_WIDTH,
},
Children = new Drawable[]
{
fadeContent = new FillFlowContainer
{
Vertical = BeatmapSetOverlay.Y_PADDING,
Left = BeatmapSetOverlay.X_PADDING,
Right = BeatmapSetOverlay.X_PADDING + BeatmapSetOverlay.RIGHT_WIDTH,
},
Children = new Drawable[]
{
fadeContent = new FillFlowContainer
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
new Container
{
new Container
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = Picker = new BeatmapPicker(),
},
title = new MetadataFlowContainer(s =>
{
s.Font = OsuFont.GetFont(size: 30, weight: FontWeight.SemiBold, italics: true);
})
{
Margin = new MarginPadding { Top = 15 },
},
artist = new MetadataFlowContainer(s =>
{
s.Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, italics: true);
})
{
Margin = new MarginPadding { Bottom = 20 },
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = author = new AuthorInfo(),
},
beatmapAvailability = new BeatmapAvailability(),
new Container
{
RelativeSizeAxes = Axes.X,
Height = buttons_height,
Margin = new MarginPadding { Top = 10 },
Children = new Drawable[]
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = Picker = new BeatmapPicker(),
},
title = new MetadataFlowContainer(s =>
{
s.Font = OsuFont.GetFont(size: 30, weight: FontWeight.SemiBold, italics: true);
})
{
Margin = new MarginPadding { Top = 15 },
},
artist = new MetadataFlowContainer(s =>
{
s.Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, italics: true);
})
{
Margin = new MarginPadding { Bottom = 20 },
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = author = new AuthorInfo(),
},
beatmapAvailability = new BeatmapAvailability(),
new Container
{
RelativeSizeAxes = Axes.X,
Height = buttons_height,
Margin = new MarginPadding { Top = 10 },
Children = new Drawable[]
favouriteButton = new FavouriteButton
{
favouriteButton = new FavouriteButton
{
BeatmapSet = { BindTarget = BeatmapSet }
},
downloadButtonsContainer = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = buttons_height + buttons_spacing },
Spacing = new Vector2(buttons_spacing),
},
BeatmapSet = { BindTarget = BeatmapSet }
},
},
downloadButtonsContainer = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = buttons_height + buttons_spacing },
Spacing = new Vector2(buttons_spacing),
},
}
},
},
}
},
},
}
},
loading = new LoadingSpinner
{

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

@ -17,9 +17,11 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.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;
using osuTK.Graphics;
@ -148,11 +150,11 @@ namespace osu.Game.Overlays.Chat
List<MenuItem> items = new List<MenuItem>
{
new OsuMenuItem("View Profile", MenuItemType.Highlighted, openUserProfile)
new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, openUserProfile)
};
if (!user.Equals(api.LocalUser.Value))
items.Add(new OsuMenuItem("Start Chat", MenuItemType.Standard, openUserChannel));
items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, openUserChannel));
return items.ToArray();
}

View File

@ -122,8 +122,8 @@ namespace osu.Game.Overlays.FirstRunSetup
stableLocatorTextBox.Current.Value = storage.GetFullPath(string.Empty);
importButton.Enabled.Value = true;
bool available = legacyImportManager.CheckHardLinkAvailability();
Logger.Log($"Hard link support is {available}");
bool available = legacyImportManager.CheckSongsFolderHardLinkAvailability();
Logger.Log($"Hard link support for beatmaps is {available}");
if (available)
{

View File

@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
@ -38,20 +39,30 @@ namespace osu.Game.Overlays
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = new FillFlowContainer
Child = new OsuContextMenuContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Children = new Drawable[]
AutoSizeAxes = Axes.Y,
Child = new PopoverContainer
{
Header.With(h => h.Depth = float.MinValue),
content = new PopoverContainer
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
Header.With(h => h.Depth = float.MinValue),
content = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
}
}
}
},
}
},
Loading = new LoadingLayer(true)

View File

@ -13,6 +13,9 @@ namespace osu.Game.Overlays
{
public partial class OverlayRulesetSelector : RulesetSelector
{
// Since this component is used in online overlays and currently web-side doesn't support non-legacy rulesets, let's disable them for now.
protected override bool LegacyOnly => true;
public OverlayRulesetSelector()
{
AutoSizeAxes = Axes.Both;

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

@ -27,7 +27,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
new SettingsEnumDropdown<FrameSync>
{
LabelText = GraphicsSettingsStrings.FrameLimiter,
Current = config.GetBindable<FrameSync>(FrameworkSetting.FrameSync)
Current = config.GetBindable<FrameSync>(FrameworkSetting.FrameSync),
Keywords = new[] { @"fps" },
},
new SettingsEnumDropdown<ExecutionMode>
{

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

@ -88,5 +88,13 @@ namespace osu.Game.Overlays.Volume
{
Content.TransformTo<Container<Drawable>, ColourInfo>("BorderColour", unhoveredColour, 500, Easing.OutQuint);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
base.OnMouseDown(e);
// Block mouse down to avoid dismissing overlays sitting behind the mute button
return true;
}
}
}

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

@ -6,6 +6,7 @@
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Game.Extensions;
namespace osu.Game.Rulesets
{
@ -16,11 +17,16 @@ namespace osu.Game.Rulesets
protected override Dropdown<RulesetInfo> CreateDropdown() => null;
protected virtual bool LegacyOnly => false;
[BackgroundDependencyLoader]
private void load()
{
foreach (var ruleset in Rulesets.AvailableRulesets)
{
if (!ruleset.IsLegacyRuleset() && LegacyOnly)
continue;
try
{
AddItem(ruleset);

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

@ -28,6 +28,16 @@ namespace osu.Game.Scoring
private readonly OsuConfigManager configManager;
private readonly ScoreImporter scoreImporter;
public override bool PauseImports
{
get => base.PauseImports;
set
{
base.PauseImports = value;
scoreImporter.PauseImports = value;
}
}
public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, RealmAccess realm, IAPIProvider api,
OsuConfigManager configManager = null)
: base(storage, realm)

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.Track;
using osu.Framework.Bindables;

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.Graphics;
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 osu.Framework.Allocation;
using osu.Framework.Graphics;
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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
@ -38,7 +36,7 @@ namespace osu.Game.Screens.Edit.Components.Menus
});
}
protected override Dropdown<EditorScreenMode> CreateDropdown() => null;
protected override Dropdown<EditorScreenMode> CreateDropdown() => null!;
protected override TabItem<EditorScreenMode> CreateTabItem(EditorScreenMode value) => new TabItem(value);

Some files were not shown because too many files have changed in this diff Show More