mirror of
https://github.com/ppy/osu.git
synced 2024-11-06 09:07:25 +08:00
Merge branch 'master' into comment-editor-3
This commit is contained in:
commit
e9995410e8
156
CONTRIBUTING.md
156
CONTRIBUTING.md
@ -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.
|
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
|
## Table of contents
|
||||||
|
|
||||||
1. [I would like to submit an issue!](#i-would-like-to-submit-an-issue)
|
1. [Reporting bugs](#reporting-bugs)
|
||||||
2. [I would like to submit a pull request!](#i-would-like-to-submit-a-pull-request)
|
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:
|
## Providing general feedback
|
||||||
* `%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.
|
|
||||||
|
|
||||||
* **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.
|
- [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
|
||||||
* **Make sure to submit pull requests off of a topic branch.**
|
- [`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!
|
||||||
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.
|
|
||||||
|
@ -101,9 +101,7 @@ JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it
|
|||||||
|
|
||||||
## Contributing
|
## 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.
|
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.
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
If you wish to help with localisation efforts, head over to [crowdin](https://crowdin.com/project/osu-web).
|
If you wish to help with localisation efforts, head over to [crowdin](https://crowdin.com/project/osu-web).
|
||||||
|
|
||||||
|
@ -18,6 +18,36 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
|
|||||||
{
|
{
|
||||||
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
|
protected override Ruleset CreatePlayerRuleset() => new CatchRuleset();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAlwaysHidden()
|
||||||
|
{
|
||||||
|
CreateModTest(new ModTestData
|
||||||
|
{
|
||||||
|
Mod = new CatchModNoScope
|
||||||
|
{
|
||||||
|
HiddenComboCount = { Value = 0 },
|
||||||
|
},
|
||||||
|
Autoplay = true,
|
||||||
|
PassCondition = () => Player.ScoreProcessor.Combo.Value == 2,
|
||||||
|
Beatmap = new Beatmap
|
||||||
|
{
|
||||||
|
HitObjects = new List<HitObject>
|
||||||
|
{
|
||||||
|
new Fruit
|
||||||
|
{
|
||||||
|
X = CatchPlayfield.CENTER_X * 0.5f,
|
||||||
|
StartTime = 1000,
|
||||||
|
},
|
||||||
|
new Fruit
|
||||||
|
{
|
||||||
|
X = CatchPlayfield.CENTER_X * 1.5f,
|
||||||
|
StartTime = 2000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestVisibleDuringBreak()
|
public void TestVisibleDuringBreak()
|
||||||
{
|
{
|
||||||
|
@ -26,6 +26,9 @@ namespace osu.Game.Rulesets.Catch.Mods
|
|||||||
var catchPlayfield = (CatchPlayfield)playfield;
|
var catchPlayfield = (CatchPlayfield)playfield;
|
||||||
bool shouldAlwaysShowCatcher = IsBreakTime.Value;
|
bool shouldAlwaysShowCatcher = IsBreakTime.Value;
|
||||||
float targetAlpha = shouldAlwaysShowCatcher ? 1 : ComboBasedAlpha;
|
float targetAlpha = shouldAlwaysShowCatcher ? 1 : ComboBasedAlpha;
|
||||||
|
|
||||||
|
// AlwaysPresent required for catcher to still act on input when fully hidden.
|
||||||
|
catchPlayfield.CatcherArea.AlwaysPresent = true;
|
||||||
catchPlayfield.CatcherArea.Alpha = (float)Interpolation.Lerp(catchPlayfield.CatcherArea.Alpha, targetAlpha, Math.Clamp(catchPlayfield.Time.Elapsed / TRANSITION_DURATION, 0, 1));
|
catchPlayfield.CatcherArea.Alpha = (float)Interpolation.Lerp(catchPlayfield.CatcherArea.Alpha, targetAlpha, Math.Clamp(catchPlayfield.Time.Elapsed / TRANSITION_DURATION, 0, 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,6 @@ using osu.Framework.Input.Bindings;
|
|||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Input;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Catch.UI
|
namespace osu.Game.Rulesets.Catch.UI
|
||||||
{
|
{
|
||||||
@ -106,41 +105,17 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e)
|
|
||||||
{
|
|
||||||
return updateAction(e.Button, getTouchCatchActionFromInput(e.ScreenSpaceMousePosition));
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnTouchDown(TouchDownEvent e)
|
protected override bool OnTouchDown(TouchDownEvent e)
|
||||||
{
|
{
|
||||||
return updateAction(e.Touch.Source, getTouchCatchActionFromInput(e.ScreenSpaceTouch.Position));
|
return updateAction(e.Touch.Source, getTouchCatchActionFromInput(e.ScreenSpaceTouch.Position));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
|
||||||
{
|
|
||||||
Show();
|
|
||||||
|
|
||||||
TouchCatchAction? action = getTouchCatchActionFromInput(e.ScreenSpaceMousePosition);
|
|
||||||
|
|
||||||
// multiple mouse buttons may be pressed and handling the same action.
|
|
||||||
foreach (MouseButton button in e.PressedButtons)
|
|
||||||
updateAction(button, action);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnTouchMove(TouchMoveEvent e)
|
protected override void OnTouchMove(TouchMoveEvent e)
|
||||||
{
|
{
|
||||||
updateAction(e.Touch.Source, getTouchCatchActionFromInput(e.ScreenSpaceTouch.Position));
|
updateAction(e.Touch.Source, getTouchCatchActionFromInput(e.ScreenSpaceTouch.Position));
|
||||||
base.OnTouchMove(e);
|
base.OnTouchMove(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnMouseUp(MouseUpEvent e)
|
|
||||||
{
|
|
||||||
updateAction(e.Button, null);
|
|
||||||
base.OnMouseUp(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnTouchUp(TouchUpEvent e)
|
protected override void OnTouchUp(TouchUpEvent e)
|
||||||
{
|
{
|
||||||
updateAction(e.Touch.Source, null);
|
updateAction(e.Touch.Source, null);
|
||||||
|
@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
|
|
||||||
public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor();
|
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);
|
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this);
|
||||||
|
|
||||||
|
@ -11,8 +11,8 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
|||||||
public partial class ManiaHealthProcessor : DrainingHealthProcessor
|
public partial class ManiaHealthProcessor : DrainingHealthProcessor
|
||||||
{
|
{
|
||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public ManiaHealthProcessor(double drainStartTime, double drainLenience = 0)
|
public ManiaHealthProcessor(double drainStartTime)
|
||||||
: base(drainStartTime, drainLenience)
|
: base(drainStartTime, 1.0)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -209,18 +209,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
keyBindingContainer = maniaInputManager?.KeyBindingContainer;
|
keyBindingContainer = maniaInputManager?.KeyBindingContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e)
|
|
||||||
{
|
|
||||||
keyBindingContainer?.TriggerPressed(column.Action.Value);
|
|
||||||
return base.OnMouseDown(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnMouseUp(MouseUpEvent e)
|
|
||||||
{
|
|
||||||
keyBindingContainer?.TriggerReleased(column.Action.Value);
|
|
||||||
base.OnMouseUp(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnTouchDown(TouchDownEvent e)
|
protected override bool OnTouchDown(TouchDownEvent e)
|
||||||
{
|
{
|
||||||
keyBindingContainer?.TriggerPressed(column.Action.Value);
|
keyBindingContainer?.TriggerPressed(column.Action.Value);
|
||||||
|
@ -1,35 +1,64 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Colour;
|
using osu.Framework.Graphics.Colour;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Rulesets.Objects.Drawables;
|
||||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||||
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||||
{
|
{
|
||||||
public partial class ArgonFollowCircle : FollowCircle
|
public partial class ArgonFollowCircle : FollowCircle
|
||||||
{
|
{
|
||||||
|
private readonly CircularContainer circleContainer;
|
||||||
|
private readonly Box circleFill;
|
||||||
|
|
||||||
|
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
|
||||||
|
|
||||||
|
[Resolved(canBeNull: true)]
|
||||||
|
private DrawableHitObject? parentObject { get; set; }
|
||||||
|
|
||||||
public ArgonFollowCircle()
|
public ArgonFollowCircle()
|
||||||
{
|
{
|
||||||
InternalChild = new CircularContainer
|
InternalChild = circleContainer = new CircularContainer
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Masking = true,
|
Masking = true,
|
||||||
BorderThickness = 4,
|
BorderThickness = 4,
|
||||||
BorderColour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
|
|
||||||
Blending = BlendingParameters.Additive,
|
Blending = BlendingParameters.Additive,
|
||||||
Child = new Box
|
Child = circleFill = new Box
|
||||||
{
|
{
|
||||||
Colour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Alpha = 0.3f,
|
Alpha = 0.3f,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
if (parentObject != null)
|
||||||
|
accentColour.BindTo(parentObject.AccentColour);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
accentColour.BindValueChanged(colour =>
|
||||||
|
{
|
||||||
|
circleContainer.BorderColour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.5f));
|
||||||
|
circleFill.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.5f));
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnSliderPress()
|
protected override void OnSliderPress()
|
||||||
{
|
{
|
||||||
const float duration = 300f;
|
const float duration = 300f;
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Colour;
|
using osu.Framework.Graphics.Colour;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
@ -21,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
|||||||
|
|
||||||
private readonly Vector2 defaultIconScale = new Vector2(0.6f, 0.8f);
|
private readonly Vector2 defaultIconScale = new Vector2(0.6f, 0.8f);
|
||||||
|
|
||||||
|
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
|
||||||
|
|
||||||
[Resolved(canBeNull: true)]
|
[Resolved(canBeNull: true)]
|
||||||
private DrawableHitObject? parentObject { get; set; }
|
private DrawableHitObject? parentObject { get; set; }
|
||||||
|
|
||||||
@ -37,7 +41,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
|||||||
{
|
{
|
||||||
fill = new Box
|
fill = new Box
|
||||||
{
|
{
|
||||||
Colour = ColourInfo.GradientVertical(Colour4.FromHex("FC618F"), Colour4.FromHex("BB1A41")),
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
@ -53,10 +56,22 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
if (parentObject != null)
|
||||||
|
accentColour.BindTo(parentObject.AccentColour);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
|
accentColour.BindValueChanged(colour =>
|
||||||
|
{
|
||||||
|
fill.Colour = ColourInfo.GradientVertical(colour.NewValue, colour.NewValue.Darken(0.5f));
|
||||||
|
}, true);
|
||||||
|
|
||||||
if (parentObject != null)
|
if (parentObject != null)
|
||||||
{
|
{
|
||||||
parentObject.ApplyCustomUpdateState += updateStateTransforms;
|
parentObject.ApplyCustomUpdateState += updateStateTransforms;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
@ -29,8 +30,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
|
|
||||||
private readonly bool hasNumber;
|
private readonly bool hasNumber;
|
||||||
|
|
||||||
protected Drawable CircleSprite = null!;
|
protected LegacyKiaiFlashingDrawable CircleSprite = null!;
|
||||||
protected Drawable OverlaySprite = null!;
|
protected LegacyKiaiFlashingDrawable OverlaySprite = null!;
|
||||||
|
|
||||||
protected Container OverlayLayer { get; private set; } = 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.
|
// 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.
|
// 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.
|
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png.
|
||||||
|
|
||||||
InternalChildren = new[]
|
InternalChildren = new[]
|
||||||
{
|
{
|
||||||
CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) })
|
CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName) })
|
||||||
@ -114,7 +114,21 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
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)
|
if (hasNumber)
|
||||||
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
|
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
|||||||
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
|
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
|
||||||
{
|
{
|
||||||
drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
|
drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
|
||||||
drawableTaikoRuleset.LockPlayfieldAspect.Value = false;
|
drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false;
|
||||||
|
|
||||||
var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
|
var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
|
||||||
playfield.ClassicHitTargetPosition.Value = true;
|
playfield.ClassicHitTargetPosition.Value = true;
|
||||||
|
@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
{
|
{
|
||||||
public new BindableDouble TimeRange => base.TimeRange;
|
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;
|
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
|
||||||
|
|
||||||
@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
|
|
||||||
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer
|
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer
|
||||||
{
|
{
|
||||||
LockPlayfieldAspect = { BindTarget = LockPlayfieldAspect }
|
LockPlayfieldMaxAspect = { BindTarget = LockPlayfieldMaxAspect }
|
||||||
};
|
};
|
||||||
|
|
||||||
protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);
|
protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);
|
||||||
|
@ -107,24 +107,6 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e)
|
|
||||||
{
|
|
||||||
if (!validMouse(e))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
handleDown(e.Button, e.ScreenSpaceMousePosition);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnMouseUp(MouseUpEvent e)
|
|
||||||
{
|
|
||||||
if (!validMouse(e))
|
|
||||||
return;
|
|
||||||
|
|
||||||
handleUp(e.Button);
|
|
||||||
base.OnMouseUp(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override bool OnTouchDown(TouchDownEvent e)
|
protected override bool OnTouchDown(TouchDownEvent e)
|
||||||
{
|
{
|
||||||
handleDown(e.Touch.Source, e.ScreenSpaceTouchDownPosition);
|
handleDown(e.Touch.Source, e.ScreenSpaceTouchDownPosition);
|
||||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
|
private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
|
||||||
private const float default_aspect = 16f / 9f;
|
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()
|
protected override void Update()
|
||||||
{
|
{
|
||||||
@ -21,7 +21,12 @@ namespace osu.Game.Rulesets.Taiko.UI
|
|||||||
|
|
||||||
float height = default_relative_height;
|
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 *= Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
|
||||||
|
|
||||||
Height = height;
|
Height = height;
|
||||||
|
@ -75,6 +75,8 @@ namespace osu.Game.Tests.Chat
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AddUntilStep("wait for notifications client", () => channelManager.NotificationsConnected);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
|
@ -8,6 +8,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Online.Spectator;
|
using osu.Game.Online.Spectator;
|
||||||
using osu.Game.Rulesets.Judgements;
|
using osu.Game.Rulesets.Judgements;
|
||||||
@ -139,6 +140,29 @@ namespace osu.Game.Tests.Gameplay
|
|||||||
Assert.That(score.MaximumStatistics[HitResult.LargeBonus], Is.EqualTo(1));
|
Assert.That(score.MaximumStatistics[HitResult.LargeBonus], Is.EqualTo(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAccuracyModes()
|
||||||
|
{
|
||||||
|
var beatmap = new Beatmap<HitObject>
|
||||||
|
{
|
||||||
|
HitObjects = Enumerable.Range(0, 4).Select(_ => new TestHitObject(HitResult.Great)).ToList<HitObject>()
|
||||||
|
};
|
||||||
|
|
||||||
|
var scoreProcessor = new ScoreProcessor(new OsuRuleset());
|
||||||
|
scoreProcessor.ApplyBeatmap(beatmap);
|
||||||
|
|
||||||
|
Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo(1));
|
||||||
|
Assert.That(scoreProcessor.MinimumAccuracy.Value, Is.EqualTo(0));
|
||||||
|
Assert.That(scoreProcessor.MaximumAccuracy.Value, Is.EqualTo(1));
|
||||||
|
|
||||||
|
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], beatmap.HitObjects[0].CreateJudgement()) { Type = HitResult.Ok });
|
||||||
|
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], beatmap.HitObjects[1].CreateJudgement()) { Type = HitResult.Great });
|
||||||
|
|
||||||
|
Assert.That(scoreProcessor.Accuracy.Value, Is.EqualTo((double)(100 + 300) / (2 * 300)).Within(Precision.DOUBLE_EPSILON));
|
||||||
|
Assert.That(scoreProcessor.MinimumAccuracy.Value, Is.EqualTo((double)(100 + 300) / (4 * 300)).Within(Precision.DOUBLE_EPSILON));
|
||||||
|
Assert.That(scoreProcessor.MaximumAccuracy.Value, Is.EqualTo((double)(100 + 3 * 300) / (4 * 300)).Within(Precision.DOUBLE_EPSILON));
|
||||||
|
}
|
||||||
|
|
||||||
private class TestJudgement : Judgement
|
private class TestJudgement : Judgement
|
||||||
{
|
{
|
||||||
public override HitResult MaxResult { get; }
|
public override HitResult MaxResult { get; }
|
||||||
|
BIN
osu.Game.Tests/Resources/Archives/modified-default-20221205.osk
Normal file
BIN
osu.Game.Tests/Resources/Archives/modified-default-20221205.osk
Normal file
Binary file not shown.
@ -42,7 +42,9 @@ namespace osu.Game.Tests.Skins
|
|||||||
// Covers longest combo counter
|
// Covers longest combo counter
|
||||||
"Archives/modified-default-20221012.osk",
|
"Archives/modified-default-20221012.osk",
|
||||||
// Covers TextElement and BeatmapInfoDrawable
|
// Covers TextElement and BeatmapInfoDrawable
|
||||||
"Archives/modified-default-20221102.osk"
|
"Archives/modified-default-20221102.osk",
|
||||||
|
// Covers BPM counter.
|
||||||
|
"Archives/modified-default-20221205.osk"
|
||||||
};
|
};
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.Components.Timelines.Summary;
|
using osu.Game.Screens.Edit.Components.Timelines.Summary;
|
||||||
@ -21,7 +22,13 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
public TestSceneEditorSummaryTimeline()
|
public TestSceneEditorSummaryTimeline()
|
||||||
{
|
{
|
||||||
editorBeatmap = new EditorBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo));
|
var beatmap = CreateBeatmap(new OsuRuleset().RulesetInfo);
|
||||||
|
|
||||||
|
beatmap.ControlPointInfo.Add(100000, new TimingControlPoint { BeatLength = 100 });
|
||||||
|
beatmap.ControlPointInfo.Add(50000, new DifficultyControlPoint { SliderVelocity = 2 });
|
||||||
|
beatmap.BeatmapInfo.Bookmarks = new[] { 75000, 125000 };
|
||||||
|
|
||||||
|
editorBeatmap = new EditorBeatmap(beatmap);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
|
35
osu.Game.Tests/Visual/Editing/TestScenePreviewTime.cs
Normal file
35
osu.Game.Tests/Visual/Editing/TestScenePreviewTime.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// 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 NUnit.Framework;
|
||||||
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Editing
|
||||||
|
{
|
||||||
|
public partial class TestScenePreviewTime : EditorTestScene
|
||||||
|
{
|
||||||
|
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSceneSetPreviewTimingPoint()
|
||||||
|
{
|
||||||
|
AddStep("seek to 1000", () => EditorClock.Seek(1000));
|
||||||
|
AddAssert("time is 1000", () => EditorClock.CurrentTime == 1000);
|
||||||
|
AddStep("set current time as preview point", () => Editor.SetPreviewPointToCurrentTime());
|
||||||
|
AddAssert("preview time is 1000", () => EditorBeatmap.PreviewTime.Value == 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestScenePreviewTimeline()
|
||||||
|
{
|
||||||
|
AddStep("set preview time to -1", () => EditorBeatmap.PreviewTime.Value = -1);
|
||||||
|
AddAssert("preview time line should not show", () => !Editor.ChildrenOfType<PreviewTimePart>().Single().Children.Any());
|
||||||
|
AddStep("set preview time to 1000", () => EditorBeatmap.PreviewTime.Value = 1000);
|
||||||
|
AddAssert("preview time line should show", () => Editor.ChildrenOfType<PreviewTimePart>().Single().Children.Single().Alpha == 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -181,7 +181,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
|
AddUntilStep("wait for fail", () => Player.GameplayState.HasFailed);
|
||||||
AddStep("exit", () => Player.Exit());
|
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]
|
[Test]
|
||||||
@ -209,7 +210,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
addFakeHit();
|
addFakeHit();
|
||||||
|
|
||||||
AddStep("exit", () => Player.Exit());
|
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]
|
[Test]
|
||||||
|
@ -201,6 +201,23 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
AddAssert("volume not changed", () => Audio.Volume.Value == 0.5);
|
AddAssert("volume not changed", () => Audio.Volume.Value == 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestRulesetSelectorOverflow()
|
||||||
|
{
|
||||||
|
AddStep("set toolbar width", () =>
|
||||||
|
{
|
||||||
|
toolbar.RelativeSizeAxes = Axes.None;
|
||||||
|
toolbar.Width = 400;
|
||||||
|
});
|
||||||
|
AddStep("move mouse over news toggle button", () =>
|
||||||
|
{
|
||||||
|
var button = toolbar.ChildrenOfType<ToolbarNewsButton>().Single();
|
||||||
|
InputManager.MoveMouseTo(button);
|
||||||
|
});
|
||||||
|
AddAssert("no ruleset toggle buttons hovered", () => !toolbar.ChildrenOfType<ToolbarRulesetTabButton>().Any(button => button.IsHovered));
|
||||||
|
AddUntilStep("toolbar gradient visible", () => toolbar.ChildrenOfType<Toolbar.ToolbarBackground>().Single().Children.All(d => d.Alpha > 0));
|
||||||
|
}
|
||||||
|
|
||||||
public partial class TestToolbar : Toolbar
|
public partial class TestToolbar : Toolbar
|
||||||
{
|
{
|
||||||
public new Bindable<OverlayActivation> OverlayActivationMode => base.OverlayActivationMode as Bindable<OverlayActivation>;
|
public new Bindable<OverlayActivation> OverlayActivationMode => base.OverlayActivationMode as Bindable<OverlayActivation>;
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
@ -46,10 +47,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
AddAssert("item removed", () => !playlist.Items.Contains(selectedItem));
|
AddAssert("item removed", () => !playlist.Items.Contains(selectedItem));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[TestCase(true)]
|
||||||
public void TestNextItemSelectedAfterDeletion()
|
[TestCase(false)]
|
||||||
|
public void TestNextItemSelectedAfterDeletion(bool allowSelection)
|
||||||
{
|
{
|
||||||
createPlaylist();
|
createPlaylist(p =>
|
||||||
|
{
|
||||||
|
p.AllowSelection = allowSelection;
|
||||||
|
});
|
||||||
|
|
||||||
moveToItem(0);
|
moveToItem(0);
|
||||||
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
AddStep("click", () => InputManager.Click(MouseButton.Left));
|
||||||
@ -57,7 +62,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
moveToDeleteButton(0);
|
moveToDeleteButton(0);
|
||||||
AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
|
AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
|
||||||
|
|
||||||
AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
|
AddAssert("item 0 is " + (allowSelection ? "selected" : "not selected"), () => playlist.SelectedItem.Value == (allowSelection ? playlist.Items[0] : null));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -117,7 +122,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
InputManager.MoveMouseTo(item.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistRemoveButton>().ElementAt(0), offset);
|
InputManager.MoveMouseTo(item.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistRemoveButton>().ElementAt(0), offset);
|
||||||
});
|
});
|
||||||
|
|
||||||
private void createPlaylist()
|
private void createPlaylist(Action<TestPlaylist> setupPlaylist = null)
|
||||||
{
|
{
|
||||||
AddStep("create playlist", () =>
|
AddStep("create playlist", () =>
|
||||||
{
|
{
|
||||||
@ -154,6 +159,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setupPlaylist?.Invoke(playlist);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
|
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
|
||||||
|
@ -27,6 +27,7 @@ using osu.Game.Rulesets.Osu.Mods;
|
|||||||
using osu.Game.Scoring;
|
using osu.Game.Scoring;
|
||||||
using osu.Game.Screens.Menu;
|
using osu.Game.Screens.Menu;
|
||||||
using osu.Game.Screens.OnlinePlay.Lounge;
|
using osu.Game.Screens.OnlinePlay.Lounge;
|
||||||
|
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||||
using osu.Game.Screens.OnlinePlay.Playlists;
|
using osu.Game.Screens.OnlinePlay.Playlists;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Screens.Ranking;
|
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);
|
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();
|
||||||
|
pushEscape();
|
||||||
|
|
||||||
AddAssert("confirmation dialog shown", () => Game.ChildrenOfType<DialogOverlay>().Single().CurrentDialog is not null);
|
AddAssert("confirmation dialog shown", () => Game.ChildrenOfType<DialogOverlay>().Single().CurrentDialog is not null);
|
||||||
|
|
||||||
AddStep("confirm exit", () => InputManager.Key(Key.Enter));
|
AddStep("confirm exit", () => InputManager.Key(Key.Enter));
|
||||||
|
@ -14,6 +14,8 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Game.Beatmaps.Drawables;
|
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.Online.API.Requests.Responses;
|
||||||
using osu.Game.Overlays.BeatmapSet.Scores;
|
using osu.Game.Overlays.BeatmapSet.Scores;
|
||||||
using osu.Game.Resources.Localisation.Web;
|
using osu.Game.Resources.Localisation.Web;
|
||||||
@ -54,6 +56,8 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
{
|
{
|
||||||
overlay.ShowBeatmapSet(new APIBeatmapSet
|
overlay.ShowBeatmapSet(new APIBeatmapSet
|
||||||
{
|
{
|
||||||
|
Genre = new BeatmapSetOnlineGenre { Id = 15, Name = "Future genre" },
|
||||||
|
Language = new BeatmapSetOnlineLanguage { Id = 15, Name = "Future language" },
|
||||||
OnlineID = 1235,
|
OnlineID = 1235,
|
||||||
Title = @"an awesome beatmap",
|
Title = @"an awesome beatmap",
|
||||||
Artist = @"naru narusegawa",
|
Artist = @"naru narusegawa",
|
||||||
@ -239,6 +243,44 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
AddStep(@"show without reload", overlay.Show);
|
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()
|
private APIBeatmapSet createManyDifficultiesBeatmapSet()
|
||||||
{
|
{
|
||||||
var set = getBeatmapSet();
|
var set = getBeatmapSet();
|
||||||
|
@ -530,6 +530,52 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTextBoxSavePerChannel()
|
||||||
|
{
|
||||||
|
var testPMChannel = new Channel(testUser);
|
||||||
|
|
||||||
|
AddStep("show overlay", () => chatOverlay.Show());
|
||||||
|
joinTestChannel(0);
|
||||||
|
joinChannel(testPMChannel);
|
||||||
|
|
||||||
|
AddAssert("listing is visible", () => listingIsVisible);
|
||||||
|
AddStep("search for 'number 2'", () => chatOverlayTextBox.Text = "number 2");
|
||||||
|
AddAssert("'number 2' saved to selector", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == "number 2");
|
||||||
|
|
||||||
|
AddStep("select normal channel", () => clickDrawable(getChannelListItem(testChannel1)));
|
||||||
|
AddAssert("text box cleared on normal channel", () => chatOverlayTextBox.Text == string.Empty);
|
||||||
|
AddAssert("nothing saved on normal channel", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == string.Empty);
|
||||||
|
AddStep("type '727'", () => chatOverlayTextBox.Text = "727");
|
||||||
|
AddAssert("'727' saved to normal channel", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == "727");
|
||||||
|
|
||||||
|
AddStep("select PM channel", () => clickDrawable(getChannelListItem(testPMChannel)));
|
||||||
|
AddAssert("text box cleared on PM channel", () => chatOverlayTextBox.Text == string.Empty);
|
||||||
|
AddAssert("nothing saved on PM channel", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == string.Empty);
|
||||||
|
AddStep("type 'hello'", () => chatOverlayTextBox.Text = "hello");
|
||||||
|
AddAssert("'hello' saved to PM channel", () => channelManager.CurrentChannel.Value.TextBoxMessage.Value == "hello");
|
||||||
|
|
||||||
|
AddStep("select normal channel", () => clickDrawable(getChannelListItem(testChannel1)));
|
||||||
|
AddAssert("text box contains '727'", () => chatOverlayTextBox.Text == "727");
|
||||||
|
|
||||||
|
AddStep("select PM channel", () => clickDrawable(getChannelListItem(testPMChannel)));
|
||||||
|
AddAssert("text box contains 'hello'", () => chatOverlayTextBox.Text == "hello");
|
||||||
|
AddStep("click close button", () =>
|
||||||
|
{
|
||||||
|
ChannelListItemCloseButton closeButton = getChannelListItem(testPMChannel).ChildrenOfType<ChannelListItemCloseButton>().Single();
|
||||||
|
clickDrawable(closeButton);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("listing is visible", () => listingIsVisible);
|
||||||
|
AddAssert("text box contains 'channel 2'", () => chatOverlayTextBox.Text == "number 2");
|
||||||
|
AddUntilStep("only channel 2 visible", () =>
|
||||||
|
{
|
||||||
|
IEnumerable<ChannelListingItem> listingItems = chatOverlay.ChildrenOfType<ChannelListingItem>()
|
||||||
|
.Where(item => item.IsPresent);
|
||||||
|
return listingItems.Count() == 1 && listingItems.Single().Channel == testChannel2;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void joinTestChannel(int i)
|
private void joinTestChannel(int i)
|
||||||
{
|
{
|
||||||
AddStep($"Join test channel {i}", () => channelManager.JoinChannel(testChannels[i]));
|
AddStep($"Join test channel {i}", () => channelManager.JoinChannel(testChannels[i]));
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -11,7 +9,9 @@ using osu.Game.Graphics;
|
|||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.Profile;
|
||||||
using osu.Game.Overlays.Profile.Sections;
|
using osu.Game.Overlays.Profile.Sections;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Online
|
namespace osu.Game.Tests.Visual.Online
|
||||||
{
|
{
|
||||||
@ -39,8 +39,8 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
Child = section = new HistoricalSection(),
|
Child = section = new HistoricalSection(),
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("Show peppy", () => section.User.Value = new APIUser { Id = 2 });
|
AddStep("Show peppy", () => section.User.Value = new UserProfileData(new APIUser { Id = 2 }, new OsuRuleset().RulesetInfo));
|
||||||
AddStep("Show WubWoofWolf", () => section.User.Value = new APIUser { Id = 39828 });
|
AddStep("Show WubWoofWolf", () => section.User.Value = new UserProfileData(new APIUser { Id = 39828 }, new OsuRuleset().RulesetInfo));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Game.Overlays.Profile.Sections.Kudosu;
|
using osu.Game.Overlays.Profile.Sections.Kudosu;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System;
|
using System;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Game.Overlays.Profile.Sections.Historical;
|
using osu.Game.Overlays.Profile.Sections.Historical;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -14,6 +12,8 @@ using System.Linq;
|
|||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Overlays.Profile;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Online
|
namespace osu.Game.Tests.Visual.Online
|
||||||
{
|
{
|
||||||
@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[Cached]
|
[Cached]
|
||||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Red);
|
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;
|
private readonly PlayHistorySubsection section;
|
||||||
|
|
||||||
public TestScenePlayHistorySubsection()
|
public TestScenePlayHistorySubsection()
|
||||||
@ -45,49 +45,49 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestNullValues()
|
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);
|
AddAssert("Section is hidden", () => section.Alpha == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestEmptyValues()
|
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);
|
AddAssert("Section is hidden", () => section.Alpha == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestOneValue()
|
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);
|
AddAssert("Section is hidden", () => section.Alpha == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestTwoValues()
|
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);
|
AddAssert("Section is visible", () => section.Alpha == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestConstantValues()
|
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);
|
AddAssert("Section is visible", () => section.Alpha == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestConstantZeroValues()
|
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);
|
AddAssert("Section is visible", () => section.Alpha == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestFilledValues()
|
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("Section is visible", () => section.Alpha == 1);
|
||||||
AddAssert("Array length is the same", () => user_with_filled_values.MonthlyPlayCounts.Length == getChartValuesLength());
|
AddAssert("Array length is the same", () => user_with_filled_values.MonthlyPlayCounts.Length == getChartValuesLength());
|
||||||
}
|
}
|
||||||
@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestMissingValues()
|
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("Section is visible", () => section.Alpha == 1);
|
||||||
AddAssert("Array length is 7", () => getChartValuesLength() == 7);
|
AddAssert("Array length is 7", () => getChartValuesLength() == 7);
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Overlays.Profile.Header.Components;
|
using osu.Game.Overlays.Profile.Header.Components;
|
||||||
using osu.Game.Rulesets.Catch;
|
using osu.Game.Rulesets.Catch;
|
||||||
@ -13,6 +11,7 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Overlays.Profile;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Online
|
namespace osu.Game.Tests.Visual.Online
|
||||||
{
|
{
|
||||||
@ -23,25 +22,24 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
|
|
||||||
public TestSceneProfileRulesetSelector()
|
public TestSceneProfileRulesetSelector()
|
||||||
{
|
{
|
||||||
ProfileRulesetSelector selector;
|
var user = new Bindable<UserProfileData?>();
|
||||||
var user = new Bindable<APIUser>();
|
|
||||||
|
|
||||||
Child = selector = new ProfileRulesetSelector
|
Child = new ProfileRulesetSelector
|
||||||
{
|
{
|
||||||
Anchor = Anchor.Centre,
|
Anchor = Anchor.Centre,
|
||||||
Origin = Anchor.Centre,
|
Origin = Anchor.Centre,
|
||||||
User = { BindTarget = user }
|
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("User with osu as default", () => user.Value = new UserProfileData(new APIUser { Id = 0, PlayMode = "osu" }, new OsuRuleset().RulesetInfo));
|
||||||
AddStep("set taiko as default", () => selector.SetDefaultRuleset(new TaikoRuleset().RulesetInfo));
|
AddStep("User with taiko as default", () => user.Value = new UserProfileData(new APIUser { Id = 1, PlayMode = "taiko" }, new OsuRuleset().RulesetInfo));
|
||||||
AddStep("set catch as default", () => selector.SetDefaultRuleset(new CatchRuleset().RulesetInfo));
|
AddStep("User with catch as default", () => user.Value = new UserProfileData(new APIUser { Id = 2, PlayMode = "fruits" }, new OsuRuleset().RulesetInfo));
|
||||||
AddStep("set mania as default", () => selector.SetDefaultRuleset(new ManiaRuleset().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);
|
AddStep("null user", () => user.Value = null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -35,6 +35,8 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
private Action<GetUsersRequest>? handleGetUsersRequest;
|
private Action<GetUsersRequest>? handleGetUsersRequest;
|
||||||
private Action<GetUserRequest>? handleGetUserRequest;
|
private Action<GetUserRequest>? handleGetUserRequest;
|
||||||
|
|
||||||
|
private IDisposable? subscription;
|
||||||
|
|
||||||
private readonly Dictionary<(int userId, string rulesetName), UserStatistics> serverSideStatistics = new Dictionary<(int userId, string rulesetName), UserStatistics>();
|
private readonly Dictionary<(int userId, string rulesetName), UserStatistics> serverSideStatistics = new Dictionary<(int userId, string rulesetName), UserStatistics>();
|
||||||
|
|
||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
@ -246,6 +248,26 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
AddAssert("values after are correct", () => update!.After.TotalScore, () => Is.EqualTo(6_000_000));
|
AddAssert("values after are correct", () => update!.After.TotalScore, () => Is.EqualTo(6_000_000));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestStatisticsUpdateNotFiredAfterSubscriptionDisposal()
|
||||||
|
{
|
||||||
|
int userId = getUserId();
|
||||||
|
setUpUser(userId);
|
||||||
|
|
||||||
|
long scoreId = getScoreId();
|
||||||
|
var ruleset = new OsuRuleset().RulesetInfo;
|
||||||
|
|
||||||
|
SoloStatisticsUpdate? update = null;
|
||||||
|
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
|
||||||
|
AddStep("unsubscribe", () => subscription!.Dispose());
|
||||||
|
|
||||||
|
feignScoreProcessing(userId, ruleset, 5_000_000);
|
||||||
|
|
||||||
|
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
|
||||||
|
AddWaitStep("wait a bit", 5);
|
||||||
|
AddAssert("update not received", () => update == null);
|
||||||
|
}
|
||||||
|
|
||||||
private int nextUserId = 2000;
|
private int nextUserId = 2000;
|
||||||
private long nextScoreId = 50000;
|
private long nextScoreId = 50000;
|
||||||
|
|
||||||
@ -266,7 +288,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action<SoloStatisticsUpdate> onUpdateReady) =>
|
private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action<SoloStatisticsUpdate> onUpdateReady) =>
|
||||||
AddStep("register for updates", () => watcher.RegisterForStatisticsUpdateAfter(
|
AddStep("register for updates", () => subscription = watcher.RegisterForStatisticsUpdateAfter(
|
||||||
new ScoreInfo(Beatmap.Value.BeatmapInfo, new OsuRuleset().RulesetInfo, new RealmUser())
|
new ScoreInfo(Beatmap.Value.BeatmapInfo, new OsuRuleset().RulesetInfo, new RealmUser())
|
||||||
{
|
{
|
||||||
Ruleset = rulesetInfo,
|
Ruleset = rulesetInfo,
|
||||||
|
@ -50,6 +50,8 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
private ChannelManager channelManager;
|
private ChannelManager channelManager;
|
||||||
|
|
||||||
private TestStandAloneChatDisplay chatDisplay;
|
private TestStandAloneChatDisplay chatDisplay;
|
||||||
|
private TestStandAloneChatDisplay chatWithTextBox;
|
||||||
|
private TestStandAloneChatDisplay chatWithTextBox2;
|
||||||
private int messageIdSequence;
|
private int messageIdSequence;
|
||||||
|
|
||||||
private Channel testChannel;
|
private Channel testChannel;
|
||||||
@ -78,7 +80,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
|
|
||||||
private void reinitialiseDrawableDisplay()
|
private void reinitialiseDrawableDisplay()
|
||||||
{
|
{
|
||||||
Children = new[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
chatDisplay = new TestStandAloneChatDisplay
|
chatDisplay = new TestStandAloneChatDisplay
|
||||||
{
|
{
|
||||||
@ -88,13 +90,28 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
Size = new Vector2(400, 80),
|
Size = new Vector2(400, 80),
|
||||||
Channel = { Value = testChannel },
|
Channel = { Value = testChannel },
|
||||||
},
|
},
|
||||||
new TestStandAloneChatDisplay(true)
|
new FillFlowContainer
|
||||||
{
|
{
|
||||||
Anchor = Anchor.CentreRight,
|
Anchor = Anchor.CentreRight,
|
||||||
Origin = Anchor.CentreRight,
|
Origin = Anchor.CentreRight,
|
||||||
|
AutoSizeAxes = Axes.Both,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
Margin = new MarginPadding(20),
|
Margin = new MarginPadding(20),
|
||||||
Size = new Vector2(400, 150),
|
Children = new[]
|
||||||
Channel = { Value = testChannel },
|
{
|
||||||
|
chatWithTextBox = new TestStandAloneChatDisplay(true)
|
||||||
|
{
|
||||||
|
Margin = new MarginPadding(20),
|
||||||
|
Size = new Vector2(400, 150),
|
||||||
|
Channel = { Value = testChannel },
|
||||||
|
},
|
||||||
|
chatWithTextBox2 = new TestStandAloneChatDisplay(true)
|
||||||
|
{
|
||||||
|
Margin = new MarginPadding(20),
|
||||||
|
Size = new Vector2(400, 150),
|
||||||
|
Channel = { Value = testChannel },
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -351,6 +368,13 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
checkScrolledToBottom();
|
checkScrolledToBottom();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestTextBoxSync()
|
||||||
|
{
|
||||||
|
AddStep("type 'hello' to text box 1", () => chatWithTextBox.ChildrenOfType<StandAloneChatDisplay.ChatTextBox>().Single().Text = "hello");
|
||||||
|
AddAssert("text box 2 contains 'hello'", () => chatWithTextBox2.ChildrenOfType<StandAloneChatDisplay.ChatTextBox>().Single().Text == "hello");
|
||||||
|
}
|
||||||
|
|
||||||
private void fillChat(int count = 10)
|
private void fillChat(int count = 10)
|
||||||
{
|
{
|
||||||
AddStep("fill chat", () =>
|
AddStep("fill chat", () =>
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
@ -11,6 +9,7 @@ using osu.Framework.Testing;
|
|||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Profile;
|
using osu.Game.Overlays.Profile;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Online
|
namespace osu.Game.Tests.Visual.Online
|
||||||
@ -20,7 +19,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[Cached]
|
[Cached]
|
||||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
|
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
|
||||||
|
|
||||||
private ProfileHeader header;
|
private ProfileHeader header = null!;
|
||||||
|
|
||||||
[SetUpSteps]
|
[SetUpSteps]
|
||||||
public void SetUpSteps()
|
public void SetUpSteps()
|
||||||
@ -31,36 +30,37 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestBasic()
|
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]
|
[Test]
|
||||||
public void TestOnlineState()
|
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,
|
Id = 1001,
|
||||||
Username = "IAmOnline",
|
Username = "IAmOnline",
|
||||||
LastVisit = DateTimeOffset.Now,
|
LastVisit = DateTimeOffset.Now,
|
||||||
IsOnline = true,
|
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,
|
Id = 1002,
|
||||||
Username = "IAmOffline",
|
Username = "IAmOffline",
|
||||||
LastVisit = DateTimeOffset.Now.AddDays(-10),
|
LastVisit = DateTimeOffset.Now.AddDays(-10),
|
||||||
IsOnline = false,
|
IsOnline = false,
|
||||||
});
|
}, new OsuRuleset().RulesetInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestRankedState()
|
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,
|
Id = 2001,
|
||||||
Username = "RankedUser",
|
Username = "RankedUser",
|
||||||
|
Groups = new[] { new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" } },
|
||||||
Statistics = new UserStatistics
|
Statistics = new UserStatistics
|
||||||
{
|
{
|
||||||
IsRanked = true,
|
IsRanked = true,
|
||||||
@ -72,9 +72,9 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
Data = Enumerable.Range(2345, 45).Concat(Enumerable.Range(2109, 40)).ToArray()
|
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,
|
Id = 2002,
|
||||||
Username = "UnrankedUser",
|
Username = "UnrankedUser",
|
||||||
@ -88,7 +88,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
Data = Enumerable.Range(2345, 85).ToArray()
|
Data = Enumerable.Range(2345, 85).ToArray()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
});
|
}, new OsuRuleset().RulesetInfo));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,15 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
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.Online.API.Requests.Responses;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Profile;
|
|
||||||
using osu.Game.Users;
|
using osu.Game.Users;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Online
|
namespace osu.Game.Tests.Visual.Online
|
||||||
@ -16,9 +17,66 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public partial class TestSceneUserProfileOverlay : OsuTestScene
|
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
|
public static readonly APIUser TEST_USER = new APIUser
|
||||||
{
|
{
|
||||||
@ -29,6 +87,11 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
JoinDate = DateTimeOffset.Now.AddDays(-1),
|
JoinDate = DateTimeOffset.Now.AddDays(-1),
|
||||||
LastVisit = DateTimeOffset.Now,
|
LastVisit = DateTimeOffset.Now,
|
||||||
ProfileOrder = new[] { "me" },
|
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
|
Statistics = new UserStatistics
|
||||||
{
|
{
|
||||||
IsRanked = true,
|
IsRanked = true,
|
||||||
@ -65,61 +128,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
Title = "osu!volunteer",
|
Title = "osu!volunteer",
|
||||||
Colour = "ff0000",
|
Colour = "ff0000",
|
||||||
Achievements = Array.Empty<APIUserAchievement>(),
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -14,7 +12,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public partial class TestSceneUserProfilePreviousUsernames : OsuTestScene
|
public partial class TestSceneUserProfilePreviousUsernames : OsuTestScene
|
||||||
{
|
{
|
||||||
private PreviousUsernames container;
|
private PreviousUsernames container = null!;
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
public void SetUp() => Schedule(() =>
|
public void SetUp() => Schedule(() =>
|
||||||
@ -50,7 +48,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
AddUntilStep("Is hidden", () => container.Alpha == 0);
|
AddUntilStep("Is hidden", () => container.Alpha == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static readonly APIUser[] users =
|
private static readonly APIUser?[] users =
|
||||||
{
|
{
|
||||||
new APIUser { Id = 1, PreviousUsernames = new[] { "username1" } },
|
new APIUser { Id = 1, PreviousUsernames = new[] { "username1" } },
|
||||||
new APIUser { Id = 2, PreviousUsernames = new[] { "longusername", "longerusername" } },
|
new APIUser { Id = 2, PreviousUsernames = new[] { "longusername", "longerusername" } },
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -12,7 +10,9 @@ using osu.Game.Graphics;
|
|||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.Profile;
|
||||||
using osu.Game.Overlays.Profile.Sections;
|
using osu.Game.Overlays.Profile.Sections;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Online
|
namespace osu.Game.Tests.Visual.Online
|
||||||
{
|
{
|
||||||
@ -46,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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
var visibleBeatmapPanels = carousel.Items.OfType<DrawableCarouselBeatmap>().Where(p => p.IsPresent).ToArray();
|
var visibleBeatmapPanels = carousel.Items.OfType<DrawableCarouselBeatmap>().Where(p => p.IsPresent).ToArray();
|
||||||
|
|
||||||
return visibleBeatmapPanels.Length == 1
|
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
|
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();
|
var visibleBeatmapPanels = carousel.Items.OfType<DrawableCarouselBeatmap>().Where(p => p.IsPresent).ToArray();
|
||||||
|
|
||||||
return visibleBeatmapPanels.Length == 2
|
return visibleBeatmapPanels.Length == 2
|
||||||
&& visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 0) == 1
|
&& 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 == 1) == 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("filter to ruleset 2", () => carousel.Filter(new FilterCriteria
|
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();
|
var visibleBeatmapPanels = carousel.Items.OfType<DrawableCarouselBeatmap>().Where(p => p.IsPresent).ToArray();
|
||||||
|
|
||||||
return visibleBeatmapPanels.Length == 2
|
return visibleBeatmapPanels.Length == 2
|
||||||
&& visibleBeatmapPanels.Count(p => ((CarouselBeatmap)p.Item).BeatmapInfo.Ruleset.OnlineID == 0) == 1
|
&& 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 == 2) == 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1069,7 +1069,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
return Precision.AlmostEquals(
|
return Precision.AlmostEquals(
|
||||||
carousel.ScreenSpaceDrawQuad.Centre,
|
carousel.ScreenSpaceDrawQuad.Centre,
|
||||||
carousel.Items
|
carousel.Items
|
||||||
.First(i => i.Item.State.Value == CarouselItemState.Selected)
|
.First(i => i.Item?.State.Value == CarouselItemState.Selected)
|
||||||
.ScreenSpaceDrawQuad.Centre, 100);
|
.ScreenSpaceDrawQuad.Centre, 100);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -1103,7 +1103,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
if (currentlySelected == null)
|
if (currentlySelected == null)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return currentlySelected.Item.Visible;
|
return currentlySelected.Item!.Visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkInvisibleDifficultiesUnselectable()
|
private void checkInvisibleDifficultiesUnselectable()
|
||||||
|
@ -208,7 +208,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
AddStep("select next and enter", () =>
|
AddStep("select next and enter", () =>
|
||||||
{
|
{
|
||||||
InputManager.MoveMouseTo(songSelect!.Carousel.ChildrenOfType<DrawableCarouselBeatmap>()
|
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);
|
InputManager.Click(MouseButton.Left);
|
||||||
|
|
||||||
@ -235,7 +235,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
AddStep("select next and enter", () =>
|
AddStep("select next and enter", () =>
|
||||||
{
|
{
|
||||||
InputManager.MoveMouseTo(songSelect!.Carousel.ChildrenOfType<DrawableCarouselBeatmap>()
|
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);
|
InputManager.PressButton(MouseButton.Left);
|
||||||
|
|
||||||
@ -614,7 +614,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
AddAssert("selected only shows expected ruleset (plus converts)", () =>
|
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.
|
// special case for converts checked here.
|
||||||
return selectedPanel.ChildrenOfType<FilterableDifficultyIcon>().All(i =>
|
return selectedPanel.ChildrenOfType<FilterableDifficultyIcon>().All(i =>
|
||||||
|
@ -7,7 +7,6 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Sprites;
|
|
||||||
using osu.Framework.Graphics.UserInterface;
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
@ -56,9 +55,9 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
|
|
||||||
AddStep("press Enter", () => InputManager.Key(Key.Enter));
|
AddStep("press Enter", () => InputManager.Key(Key.Enter));
|
||||||
|
|
||||||
AddUntilStep("button is loading", () => commentEditor.ButtonLoading);
|
AddUntilStep("button is loading", () => commentEditor.IsSpinnerShown);
|
||||||
AddAssert("text committed", () => commentEditor.CommittedText == "text");
|
AddAssert("text committed", () => commentEditor.CommittedText == "text");
|
||||||
AddUntilStep("button is not loading", () => !commentEditor.ButtonLoading);
|
AddUntilStep("button is not loading", () => !commentEditor.IsSpinnerShown);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -72,7 +71,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
|
|
||||||
AddStep("press Enter", () => InputManager.Key(Key.Enter));
|
AddStep("press Enter", () => InputManager.Key(Key.Enter));
|
||||||
|
|
||||||
AddAssert("button is not loading", () => !commentEditor.ButtonLoading);
|
AddAssert("button is not loading", () => !commentEditor.IsSpinnerShown);
|
||||||
AddAssert("no text committed", () => commentEditor.CommittedText.Length == 0);
|
AddAssert("no text committed", () => commentEditor.CommittedText.Length == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,9 +91,9 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
InputManager.Click(MouseButton.Left);
|
InputManager.Click(MouseButton.Left);
|
||||||
});
|
});
|
||||||
|
|
||||||
AddUntilStep("button is loading", () => commentEditor.ButtonLoading);
|
AddUntilStep("button is loading", () => commentEditor.IsSpinnerShown);
|
||||||
AddAssert("text committed", () => commentEditor.CommittedText == "some other text");
|
AddAssert("text committed", () => commentEditor.CommittedText == "some other text");
|
||||||
AddUntilStep("button is not loading", () => !commentEditor.ButtonLoading);
|
AddUntilStep("button is not loading", () => !commentEditor.IsSpinnerShown);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -116,13 +115,13 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
|
|
||||||
public string CommittedText { get; private set; } = string.Empty;
|
public string CommittedText { get; private set; } = string.Empty;
|
||||||
|
|
||||||
public bool ButtonLoading => CommitButton.ChildrenOfType<LoadingSpinner>().Single().IsPresent && !CommitButton.ChildrenOfType<SpriteText>().Single().IsPresent;
|
public bool IsSpinnerShown => this.ChildrenOfType<LoadingSpinner>().Single().IsPresent;
|
||||||
|
|
||||||
protected override void OnCommit(string value)
|
protected override void OnCommit(string value)
|
||||||
{
|
{
|
||||||
CommitButton.IsLoadingSpinnerShown = true;
|
ShowLoadingSpinner = true;
|
||||||
CommittedText = value;
|
CommittedText = value;
|
||||||
Scheduler.AddDelayed(() => CommitButton.IsLoadingSpinnerShown = false, 1000);
|
Scheduler.AddDelayed(() => ShowLoadingSpinner = false, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override LocalisableString FooterText => @"Footer text. And it is pretty long. Cool.";
|
protected override LocalisableString FooterText => @"Footer text. And it is pretty long. Cool.";
|
||||||
|
106
osu.Game.Tests/Visual/UserInterface/TestSceneOverlayContainer.cs
Normal file
106
osu.Game.Tests/Visual/UserInterface/TestSceneOverlayContainer.cs
Normal 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Game.Overlays.Profile.Sections;
|
using osu.Game.Overlays.Profile.Sections;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
@ -18,7 +16,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
[Cached]
|
[Cached]
|
||||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
|
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
|
||||||
|
|
||||||
private ProfileSubsectionHeader header;
|
private ProfileSubsectionHeader header = null!;
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestHiddenCounter()
|
public void TestHiddenCounter()
|
||||||
|
154
osu.Game.Tests/Visual/UserInterface/TestSceneSegmentedGraph.cs
Normal file
154
osu.Game.Tests/Visual/UserInterface/TestSceneSegmentedGraph.cs
Normal 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]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -55,6 +55,16 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestDisplay()
|
||||||
|
{
|
||||||
|
AddRepeatStep("toggle expanded state", () =>
|
||||||
|
{
|
||||||
|
InputManager.MoveMouseTo(group.ChildrenOfType<IconButton>().Single());
|
||||||
|
InputManager.Click(MouseButton.Left);
|
||||||
|
}, 5);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestClickExpandButtonMultipleTimes()
|
public void TestClickExpandButtonMultipleTimes()
|
||||||
{
|
{
|
||||||
|
@ -44,6 +44,16 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
public Action<(BeatmapSetInfo beatmapSet, bool isBatch)>? ProcessBeatmap { private get; set; }
|
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,
|
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)
|
WorkingBeatmap? defaultBeatmap = null, BeatmapDifficultyCache? difficultyCache = null, bool performOnlineLookups = false)
|
||||||
: base(storage, realm)
|
: 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 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) =>
|
public Task<Live<BeatmapSetInfo>?> Import(ImportTask task, ImportParameters parameters = default, CancellationToken cancellationToken = default) =>
|
||||||
beatmapImporter.Import(task, parameters, cancellationToken);
|
beatmapImporter.Import(task, parameters, cancellationToken);
|
||||||
|
22
osu.Game/Beatmaps/BeatmapSetOnlineNomination.cs
Normal file
22
osu.Game/Beatmaps/BeatmapSetOnlineNomination.cs
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
// 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.Beatmaps
|
||||||
|
{
|
||||||
|
public struct BeatmapSetOnlineNomination
|
||||||
|
{
|
||||||
|
[JsonProperty(@"beatmapset_id")]
|
||||||
|
public int BeatmapsetId { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(@"reset")]
|
||||||
|
public bool Reset { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(@"rulesets")]
|
||||||
|
public string[]? Rulesets { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(@"user_id")]
|
||||||
|
public int UserId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
@ -5,16 +5,19 @@ using System;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Cursor;
|
||||||
|
using osu.Framework.Graphics.UserInterface;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Online;
|
using osu.Game.Online;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Localisation;
|
||||||
|
|
||||||
namespace osu.Game.Beatmaps.Drawables.Cards
|
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 TRANSITION_DURATION = 400;
|
||||||
public const float CORNER_RADIUS = 10;
|
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");
|
throw new ArgumentOutOfRangeException(nameof(size), size, @"Unsupported card size");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MenuItem[] ContextMenuItems => new MenuItem[]
|
||||||
|
{
|
||||||
|
new OsuMenuItem(ContextMenuStrings.ViewBeatmap, MenuItemType.Highlighted, Action),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -138,9 +138,18 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
|||||||
// This avoids depth issues where a hovered (scaled) card to the right of another card would be beneath the card to the left.
|
// This avoids depth issues where a hovered (scaled) card to the right of another card would be beneath the card to the left.
|
||||||
this.ScaleTo(Expanded.Value ? 1.03f : 1, 500, Easing.OutQuint);
|
this.ScaleTo(Expanded.Value ? 1.03f : 1, 500, Easing.OutQuint);
|
||||||
|
|
||||||
background.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
|
if (Expanded.Value)
|
||||||
dropdownContent.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
|
{
|
||||||
borderContainer.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
|
background.FadeIn(BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
|
||||||
|
dropdownContent.FadeIn(BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
|
||||||
|
borderContainer.FadeIn(BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
background.FadeOut(BeatmapCard.TRANSITION_DURATION / 3f, Easing.OutQuint);
|
||||||
|
dropdownContent.FadeOut(BeatmapCard.TRANSITION_DURATION / 3f, Easing.OutQuint);
|
||||||
|
borderContainer.FadeOut(BeatmapCard.TRANSITION_DURATION / 3f, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
|
||||||
content.TweenEdgeEffectTo(new EdgeEffectParameters
|
content.TweenEdgeEffectTo(new EdgeEffectParameters
|
||||||
{
|
{
|
||||||
|
@ -300,7 +300,7 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
{
|
{
|
||||||
var comboColour = colours[i];
|
var comboColour = colours[i];
|
||||||
|
|
||||||
writer.Write(FormattableString.Invariant($"Combo{i}: "));
|
writer.Write(FormattableString.Invariant($"Combo{1 + i}: "));
|
||||||
writer.Write(FormattableString.Invariant($"{(byte)(comboColour.R * byte.MaxValue)},"));
|
writer.Write(FormattableString.Invariant($"{(byte)(comboColour.R * byte.MaxValue)},"));
|
||||||
writer.Write(FormattableString.Invariant($"{(byte)(comboColour.G * byte.MaxValue)},"));
|
writer.Write(FormattableString.Invariant($"{(byte)(comboColour.G * byte.MaxValue)},"));
|
||||||
writer.Write(FormattableString.Invariant($"{(byte)(comboColour.B * byte.MaxValue)},"));
|
writer.Write(FormattableString.Invariant($"{(byte)(comboColour.B * byte.MaxValue)},"));
|
||||||
|
@ -34,8 +34,6 @@ namespace osu.Game.Beatmaps
|
|||||||
// TODO: remove once the fallback lookup is not required (and access via `working.BeatmapInfo.Metadata` directly).
|
// TODO: remove once the fallback lookup is not required (and access via `working.BeatmapInfo.Metadata` directly).
|
||||||
public BeatmapMetadata Metadata => BeatmapInfo.Metadata;
|
public BeatmapMetadata Metadata => BeatmapInfo.Metadata;
|
||||||
|
|
||||||
public Waveform Waveform => waveform.Value;
|
|
||||||
|
|
||||||
public Storyboard Storyboard => storyboard.Value;
|
public Storyboard Storyboard => storyboard.Value;
|
||||||
|
|
||||||
public Texture Background => GetBackground(); // Texture uses ref counting, so we want to return a new instance every usage.
|
public Texture Background => GetBackground(); // Texture uses ref counting, so we want to return a new instance every usage.
|
||||||
@ -48,10 +46,11 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
private readonly object beatmapFetchLock = new object();
|
private readonly object beatmapFetchLock = new object();
|
||||||
|
|
||||||
private readonly Lazy<Waveform> waveform;
|
|
||||||
private readonly Lazy<Storyboard> storyboard;
|
private readonly Lazy<Storyboard> storyboard;
|
||||||
private readonly Lazy<ISkin> skin;
|
private readonly Lazy<ISkin> skin;
|
||||||
|
|
||||||
private Track track; // track is not Lazy as we allow transferring and loading multiple times.
|
private Track track; // track is not Lazy as we allow transferring and loading multiple times.
|
||||||
|
private Waveform waveform; // waveform is also not Lazy as the track may change.
|
||||||
|
|
||||||
protected WorkingBeatmap(BeatmapInfo beatmapInfo, AudioManager audioManager)
|
protected WorkingBeatmap(BeatmapInfo beatmapInfo, AudioManager audioManager)
|
||||||
{
|
{
|
||||||
@ -60,7 +59,6 @@ namespace osu.Game.Beatmaps
|
|||||||
BeatmapInfo = beatmapInfo;
|
BeatmapInfo = beatmapInfo;
|
||||||
BeatmapSetInfo = beatmapInfo.BeatmapSet ?? new BeatmapSetInfo();
|
BeatmapSetInfo = beatmapInfo.BeatmapSet ?? new BeatmapSetInfo();
|
||||||
|
|
||||||
waveform = new Lazy<Waveform>(GetWaveform);
|
|
||||||
storyboard = new Lazy<Storyboard>(GetStoryboard);
|
storyboard = new Lazy<Storyboard>(GetStoryboard);
|
||||||
skin = new Lazy<ISkin>(GetSkin);
|
skin = new Lazy<ISkin>(GetSkin);
|
||||||
}
|
}
|
||||||
@ -108,7 +106,16 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
public virtual bool TrackLoaded => track != null;
|
public virtual bool TrackLoaded => track != null;
|
||||||
|
|
||||||
public Track LoadTrack() => track = GetBeatmapTrack() ?? GetVirtualTrack(1000);
|
public Track LoadTrack()
|
||||||
|
{
|
||||||
|
track = GetBeatmapTrack() ?? GetVirtualTrack(1000);
|
||||||
|
|
||||||
|
// the track may have changed, recycle the current waveform.
|
||||||
|
waveform?.Dispose();
|
||||||
|
waveform = null;
|
||||||
|
|
||||||
|
return track;
|
||||||
|
}
|
||||||
|
|
||||||
public void PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint = 0)
|
public void PrepareTrackForPreview(bool looping, double offsetFromPreviewPoint = 0)
|
||||||
{
|
{
|
||||||
@ -171,6 +178,12 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region Waveform
|
||||||
|
|
||||||
|
public Waveform Waveform => waveform ??= GetWaveform();
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region Beatmap
|
#region Beatmap
|
||||||
|
|
||||||
public virtual bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false;
|
public virtual bool BeatmapLoaded => beatmapLoadTask?.IsCompleted ?? false;
|
||||||
|
@ -33,13 +33,15 @@ namespace osu.Game.Database
|
|||||||
UserFileStorage = storage.GetStorageForDirectory(@"files");
|
UserFileStorage = storage.GetStorageForDirectory(@"files");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual string GetFilename(TModel item) => item.GetDisplayString();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Exports an item to a legacy (.zip based) package.
|
/// Exports an item to a legacy (.zip based) package.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="item">The item to export.</param>
|
/// <param name="item">The item to export.</param>
|
||||||
public void Export(TModel item)
|
public void Export(TModel item)
|
||||||
{
|
{
|
||||||
string itemFilename = item.GetDisplayString().GetValidFilename();
|
string itemFilename = GetFilename(item).GetValidFilename();
|
||||||
|
|
||||||
IEnumerable<string> existingExports = exportStorage.GetFiles("", $"{itemFilename}*{FileExtension}");
|
IEnumerable<string> existingExports = exportStorage.GetFiles("", $"{itemFilename}*{FileExtension}");
|
||||||
|
|
||||||
|
@ -54,14 +54,14 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
public void UpdateStorage(string stablePath) => cachedStorage = new StableStorage(stablePath, gameHost as DesktopGameHost);
|
public void UpdateStorage(string stablePath) => cachedStorage = new StableStorage(stablePath, gameHost as DesktopGameHost);
|
||||||
|
|
||||||
public bool CheckHardLinkAvailability()
|
public bool CheckSongsFolderHardLinkAvailability()
|
||||||
{
|
{
|
||||||
var stableStorage = GetCurrentStableStorage();
|
var stableStorage = GetCurrentStableStorage();
|
||||||
|
|
||||||
if (stableStorage == null || gameHost is not DesktopGameHost desktopGameHost)
|
if (stableStorage == null || gameHost is not DesktopGameHost desktopGameHost)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
string testExistingPath = stableStorage.GetFullPath(string.Empty);
|
string testExistingPath = stableStorage.GetSongStorage().GetFullPath(string.Empty);
|
||||||
string testDestinationPath = desktopGameHost.Storage.GetFullPath(string.Empty);
|
string testDestinationPath = desktopGameHost.Storage.GetFullPath(string.Empty);
|
||||||
|
|
||||||
return HardLinkHelper.CheckAvailability(testDestinationPath, testExistingPath);
|
return HardLinkHelper.CheckAvailability(testDestinationPath, testExistingPath);
|
||||||
|
@ -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)
|
public override void ExportModelTo(ScoreInfo model, Stream outputStream)
|
||||||
{
|
{
|
||||||
var file = model.Files.SingleOrDefault();
|
var file = model.Files.SingleOrDefault();
|
||||||
|
@ -18,6 +18,11 @@ namespace osu.Game.Database
|
|||||||
public class ModelManager<TModel> : IModelManager<TModel>, IModelFileManager<TModel, RealmNamedFileUsage>
|
public class ModelManager<TModel> : IModelManager<TModel>, IModelFileManager<TModel, RealmNamedFileUsage>
|
||||||
where TModel : RealmObject, IHasRealmFiles, IHasGuidPrimaryKey, ISoftDelete
|
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; }
|
protected RealmAccess Realm { get; }
|
||||||
|
|
||||||
private readonly RealmFileStore realmFileStore;
|
private readonly RealmFileStore realmFileStore;
|
||||||
|
@ -56,6 +56,11 @@ namespace osu.Game.Database
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private static readonly ThreadedTaskScheduler import_scheduler_batch = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(RealmArchiveModelImporter<TModel>));
|
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; }
|
public abstract IEnumerable<string> HandledExtensions { get; }
|
||||||
|
|
||||||
protected readonly RealmFileStore Files;
|
protected readonly RealmFileStore Files;
|
||||||
@ -149,9 +154,12 @@ namespace osu.Game.Database
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
notification.CompletionText = imported.Count == 1
|
if (tasks.Length > imported.Count)
|
||||||
? $"Imported {imported.First().GetDisplayString()}!"
|
notification.CompletionText = $"Imported {imported.Count} of {tasks.Length} {HumanisedModelName}s.";
|
||||||
: $"Imported {imported.Count} {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)
|
if (imported.Count > 0 && PresentImport != null)
|
||||||
{
|
{
|
||||||
@ -253,7 +261,7 @@ namespace osu.Game.Database
|
|||||||
/// <param name="cancellationToken">An optional cancellation token.</param>
|
/// <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 =>
|
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;
|
TModel? existing;
|
||||||
|
|
||||||
@ -551,6 +559,23 @@ namespace osu.Game.Database
|
|||||||
/// <returns>Whether to perform deletion.</returns>
|
/// <returns>Whether to perform deletion.</returns>
|
||||||
protected virtual bool ShouldDeleteArchive(string path) => false;
|
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)
|
private IEnumerable<string> getIDs(IEnumerable<INamedFile> files)
|
||||||
{
|
{
|
||||||
foreach (var f in files.OrderBy(f => f.Filename))
|
foreach (var f in files.OrderBy(f => f.Filename))
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Framework;
|
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.IO.Stores;
|
using osu.Framework.IO.Stores;
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
@ -63,10 +62,10 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
private void copyToStore(RealmFile file, Stream data, bool preferHardLinks)
|
private void copyToStore(RealmFile file, Stream data, bool preferHardLinks)
|
||||||
{
|
{
|
||||||
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows && data is FileStream fs && preferHardLinks)
|
if (data is FileStream fs && preferHardLinks)
|
||||||
{
|
{
|
||||||
// attempt to do a fast hard link rather than copy.
|
// attempt to do a fast hard link rather than copy.
|
||||||
if (HardLinkHelper.CreateHardLink(Storage.GetFullPath(file.GetStoragePath(), true), fs.Name, IntPtr.Zero))
|
if (HardLinkHelper.TryCreateHardLink(Storage.GetFullPath(file.GetStoragePath(), true), fs.Name))
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,8 +25,6 @@ namespace osu.Game.Graphics.Containers
|
|||||||
protected virtual string PopInSampleName => "UI/overlay-pop-in";
|
protected virtual string PopInSampleName => "UI/overlay-pop-in";
|
||||||
protected virtual string PopOutSampleName => "UI/overlay-pop-out";
|
protected virtual string PopOutSampleName => "UI/overlay-pop-out";
|
||||||
|
|
||||||
protected override bool BlockScrollInput => false;
|
|
||||||
|
|
||||||
protected override bool BlockNonPositionalInput => true;
|
protected override bool BlockNonPositionalInput => true;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -90,6 +88,15 @@ namespace osu.Game.Graphics.Containers
|
|||||||
base.OnMouseUp(e);
|
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)
|
public virtual bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||||
{
|
{
|
||||||
if (e.Repeat)
|
if (e.Repeat)
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
@ -117,11 +118,11 @@ namespace osu.Game.Graphics
|
|||||||
|
|
||||||
host.GetClipboard()?.SetImage(image);
|
host.GetClipboard()?.SetImage(image);
|
||||||
|
|
||||||
string filename = getFilename();
|
(string filename, var stream) = getWritableStream();
|
||||||
|
|
||||||
if (filename == null) return;
|
if (filename == null) return;
|
||||||
|
|
||||||
using (var stream = storage.CreateFileSafely(filename))
|
using (stream)
|
||||||
{
|
{
|
||||||
switch (screenshotFormat.Value)
|
switch (screenshotFormat.Value)
|
||||||
{
|
{
|
||||||
@ -142,7 +143,7 @@ namespace osu.Game.Graphics
|
|||||||
|
|
||||||
notificationOverlay.Post(new SimpleNotification
|
notificationOverlay.Post(new SimpleNotification
|
||||||
{
|
{
|
||||||
Text = $"{filename} saved!",
|
Text = $"Screenshot {filename} saved!",
|
||||||
Activated = () =>
|
Activated = () =>
|
||||||
{
|
{
|
||||||
storage.PresentFileExternally(filename);
|
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;
|
lock (filename_reservation_lock)
|
||||||
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++)
|
|
||||||
{
|
{
|
||||||
string indexedName = $"osu_{dt:yyyy-MM-dd_HH-mm-ss}-{i}.{fileExt}";
|
var dt = DateTime.Now;
|
||||||
if (!storage.Exists(indexedName))
|
string fileExt = screenshotFormat.ToString().ToLowerInvariant();
|
||||||
return indexedName;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,6 +45,9 @@ namespace osu.Game.Graphics.UserInterface
|
|||||||
|
|
||||||
protected override bool OnKeyDown(KeyDownEvent e)
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
{
|
{
|
||||||
|
if (e.ControlPressed || e.AltPressed || e.SuperPressed || e.ShiftPressed)
|
||||||
|
return false;
|
||||||
|
|
||||||
switch (e.Key)
|
switch (e.Key)
|
||||||
{
|
{
|
||||||
case Key.Up:
|
case Key.Up:
|
||||||
|
337
osu.Game/Graphics/UserInterface/SegmentedGraph.cs
Normal file
337
osu.Game/Graphics/UserInterface/SegmentedGraph.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -14,9 +14,8 @@ namespace osu.Game.IO
|
|||||||
{
|
{
|
||||||
public static bool CheckAvailability(string testDestinationPath, string testSourcePath)
|
public static bool CheckAvailability(string testDestinationPath, string testSourcePath)
|
||||||
{
|
{
|
||||||
// We can support other operating systems quite easily in the future.
|
// For simplicity, only support desktop operating systems for now.
|
||||||
// Let's handle the most common one for now, though.
|
if (!RuntimeInfo.IsDesktop)
|
||||||
if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows)
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const string test_filename = "_hard_link_test";
|
const string test_filename = "_hard_link_test";
|
||||||
@ -31,7 +30,7 @@ namespace osu.Game.IO
|
|||||||
File.WriteAllText(testSourcePath, string.Empty);
|
File.WriteAllText(testSourcePath, string.Empty);
|
||||||
|
|
||||||
// Test availability by creating an arbitrary hard link between the source and destination paths.
|
// Test availability by creating an arbitrary hard link between the source and destination paths.
|
||||||
return CreateHardLink(testDestinationPath, testSourcePath, IntPtr.Zero);
|
return TryCreateHardLink(testDestinationPath, testSourcePath);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@ -55,21 +54,60 @@ namespace osu.Game.IO
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to create a hard link from <paramref name="sourcePath"/> to <paramref name="destinationPath"/>,
|
||||||
|
/// using platform-specific native methods.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Hard links are only available on desktop platforms.
|
||||||
|
/// </remarks>
|
||||||
|
/// <returns>Whether the hard link was successfully created.</returns>
|
||||||
|
public static bool TryCreateHardLink(string destinationPath, string sourcePath)
|
||||||
|
{
|
||||||
|
switch (RuntimeInfo.OS)
|
||||||
|
{
|
||||||
|
case RuntimeInfo.Platform.Windows:
|
||||||
|
return CreateHardLink(destinationPath, sourcePath, IntPtr.Zero);
|
||||||
|
|
||||||
|
case RuntimeInfo.Platform.Linux:
|
||||||
|
case RuntimeInfo.Platform.macOS:
|
||||||
|
return link(sourcePath, destinationPath) == 0;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// For future use (to detect if a file is a hard link with other references existing on disk).
|
// For future use (to detect if a file is a hard link with other references existing on disk).
|
||||||
public static int GetFileLinkCount(string filePath)
|
public static int GetFileLinkCount(string filePath)
|
||||||
{
|
{
|
||||||
int result = 0;
|
int result = 0;
|
||||||
SafeFileHandle handle = CreateFile(filePath, FileAccess.Read, FileShare.Read, IntPtr.Zero, FileMode.Open, FileAttributes.Archive, IntPtr.Zero);
|
|
||||||
|
|
||||||
ByHandleFileInformation fileInfo;
|
switch (RuntimeInfo.OS)
|
||||||
|
{
|
||||||
|
case RuntimeInfo.Platform.Windows:
|
||||||
|
SafeFileHandle handle = CreateFile(filePath, FileAccess.Read, FileShare.Read, IntPtr.Zero, FileMode.Open, FileAttributes.Archive, IntPtr.Zero);
|
||||||
|
|
||||||
if (GetFileInformationByHandle(handle, out fileInfo))
|
ByHandleFileInformation fileInfo;
|
||||||
result = (int)fileInfo.NumberOfLinks;
|
|
||||||
CloseHandle(handle);
|
if (GetFileInformationByHandle(handle, out fileInfo))
|
||||||
|
result = (int)fileInfo.NumberOfLinks;
|
||||||
|
CloseHandle(handle);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case RuntimeInfo.Platform.Linux:
|
||||||
|
case RuntimeInfo.Platform.macOS:
|
||||||
|
if (stat(filePath, out var statbuf) == 0)
|
||||||
|
result = (int)statbuf.st_nlink;
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#region Windows native methods
|
||||||
|
|
||||||
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
public static extern bool CreateHardLink(string lpFileName, string lpExistingFileName, IntPtr lpSecurityAttributes);
|
public static extern bool CreateHardLink(string lpFileName, string lpExistingFileName, IntPtr lpSecurityAttributes);
|
||||||
|
|
||||||
@ -104,5 +142,49 @@ namespace osu.Game.IO
|
|||||||
public readonly uint FileIndexHigh;
|
public readonly uint FileIndexHigh;
|
||||||
public readonly uint FileIndexLow;
|
public readonly uint FileIndexLow;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Linux native methods
|
||||||
|
|
||||||
|
#pragma warning disable IDE1006 // Naming rule violation
|
||||||
|
|
||||||
|
[DllImport("libc", SetLastError = true)]
|
||||||
|
public static extern int link(string oldpath, string newpath);
|
||||||
|
|
||||||
|
[DllImport("libc", SetLastError = true)]
|
||||||
|
private static extern int stat(string pathname, out struct_stat statbuf);
|
||||||
|
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
// Struct layout is likely non-portable across unices. Tread with caution.
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct struct_stat
|
||||||
|
{
|
||||||
|
public readonly long st_dev;
|
||||||
|
public readonly long st_ino;
|
||||||
|
public readonly long st_nlink;
|
||||||
|
public readonly int st_mode;
|
||||||
|
public readonly int st_uid;
|
||||||
|
public readonly int st_gid;
|
||||||
|
public readonly long st_rdev;
|
||||||
|
public readonly long st_size;
|
||||||
|
public readonly long st_blksize;
|
||||||
|
public readonly long st_blocks;
|
||||||
|
public readonly timespec st_atim;
|
||||||
|
public readonly timespec st_mtim;
|
||||||
|
public readonly timespec st_ctim;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReSharper disable once InconsistentNaming
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct timespec
|
||||||
|
{
|
||||||
|
public readonly long tv_sec;
|
||||||
|
public readonly long tv_nsec;
|
||||||
|
}
|
||||||
|
|
||||||
|
#pragma warning restore IDE1006
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
24
osu.Game/Localisation/ContextMenuStrings.cs
Normal file
24
osu.Game/Localisation/ContextMenuStrings.cs
Normal 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}";
|
||||||
|
}
|
||||||
|
}
|
@ -54,11 +54,6 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </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.");
|
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>
|
/// <summary>
|
||||||
/// "Delete ALL beatmaps"
|
/// "Delete ALL beatmaps"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -69,31 +64,16 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString DeleteAllBeatmapVideos => new TranslatableString(getKey(@"delete_all_beatmap_videos"), @"Delete ALL beatmap videos");
|
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>
|
/// <summary>
|
||||||
/// "Delete ALL scores"
|
/// "Delete ALL scores"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString DeleteAllScores => new TranslatableString(getKey(@"delete_all_scores"), @"Delete ALL scores");
|
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>
|
/// <summary>
|
||||||
/// "Delete ALL skins"
|
/// "Delete ALL skins"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString DeleteAllSkins => new TranslatableString(getKey(@"delete_all_skins"), @"Delete ALL skins");
|
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>
|
/// <summary>
|
||||||
/// "Delete ALL collections"
|
/// "Delete ALL collections"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -259,7 +259,11 @@ namespace osu.Game.Online.API
|
|||||||
|
|
||||||
var friendsReq = new GetFriendsRequest();
|
var friendsReq = new GetFriendsRequest();
|
||||||
friendsReq.Failure += _ => state.Value = APIState.Failing;
|
friendsReq.Failure += _ => state.Value = APIState.Failing;
|
||||||
friendsReq.Success += res => friends.AddRange(res);
|
friendsReq.Success += res =>
|
||||||
|
{
|
||||||
|
friends.Clear();
|
||||||
|
friends.AddRange(res);
|
||||||
|
};
|
||||||
|
|
||||||
if (!handleRequest(friendsReq))
|
if (!handleRequest(friendsReq))
|
||||||
{
|
{
|
||||||
@ -325,12 +329,35 @@ namespace osu.Game.Online.API
|
|||||||
{
|
{
|
||||||
try
|
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
|
catch
|
||||||
{
|
{
|
||||||
// if we couldn't deserialize the error message let's throw the original exception outwards.
|
try
|
||||||
e.Rethrow();
|
{
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
using System;
|
||||||
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace osu.Game.Online.API
|
namespace osu.Game.Online.API
|
||||||
{
|
{
|
||||||
public class RegistrationRequest : OsuWebRequest
|
public class RegistrationRequest : OsuWebRequest
|
||||||
{
|
{
|
||||||
internal string Username;
|
internal string Username = string.Empty;
|
||||||
internal string Email;
|
internal string Email = string.Empty;
|
||||||
internal string Password;
|
internal string Password = string.Empty;
|
||||||
|
|
||||||
protected override void PrePerform()
|
protected override void PrePerform()
|
||||||
{
|
{
|
||||||
@ -24,18 +23,28 @@ namespace osu.Game.Online.API
|
|||||||
|
|
||||||
public class RegistrationRequestErrors
|
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
|
public class UserErrors
|
||||||
{
|
{
|
||||||
[JsonProperty("username")]
|
[JsonProperty("username")]
|
||||||
public string[] Username;
|
public string[] Username = Array.Empty<string>();
|
||||||
|
|
||||||
[JsonProperty("user_email")]
|
[JsonProperty("user_email")]
|
||||||
public string[] Email;
|
public string[] Email = Array.Empty<string>();
|
||||||
|
|
||||||
[JsonProperty("password")]
|
[JsonProperty("password")]
|
||||||
public string[] Password;
|
public string[] Password = Array.Empty<string>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
@ -11,10 +9,11 @@ using osu.Game.Online.API.Requests.Responses;
|
|||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace osu.Game.Online.API.Requests
|
namespace osu.Game.Online.API.Requests
|
||||||
{
|
{
|
||||||
public class GetScoresRequest : APIRequest<APIScoresCollection>
|
public class GetScoresRequest : APIRequest<APIScoresCollection>, IEquatable<GetScoresRequest>
|
||||||
{
|
{
|
||||||
public const int MAX_SCORES_PER_REQUEST = 50;
|
public const int MAX_SCORES_PER_REQUEST = 50;
|
||||||
|
|
||||||
@ -23,7 +22,7 @@ namespace osu.Game.Online.API.Requests
|
|||||||
private readonly IRulesetInfo ruleset;
|
private readonly IRulesetInfo ruleset;
|
||||||
private readonly IEnumerable<IMod> mods;
|
private readonly IEnumerable<IMod> mods;
|
||||||
|
|
||||||
public GetScoresRequest(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset, BeatmapLeaderboardScope scope = BeatmapLeaderboardScope.Global, IEnumerable<IMod> mods = null)
|
public GetScoresRequest(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset, BeatmapLeaderboardScope scope = BeatmapLeaderboardScope.Global, IEnumerable<IMod>? mods = null)
|
||||||
{
|
{
|
||||||
if (beatmapInfo.OnlineID <= 0)
|
if (beatmapInfo.OnlineID <= 0)
|
||||||
throw new InvalidOperationException($"Cannot lookup a beatmap's scores without having a populated {nameof(IBeatmapInfo.OnlineID)}.");
|
throw new InvalidOperationException($"Cannot lookup a beatmap's scores without having a populated {nameof(IBeatmapInfo.OnlineID)}.");
|
||||||
@ -51,5 +50,16 @@ namespace osu.Game.Online.API.Requests
|
|||||||
|
|
||||||
return query.ToString();
|
return query.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool Equals(GetScoresRequest? other)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(null, other)) return false;
|
||||||
|
if (ReferenceEquals(this, other)) return true;
|
||||||
|
|
||||||
|
return beatmapInfo.Equals(other.beatmapInfo)
|
||||||
|
&& scope == other.scope
|
||||||
|
&& ruleset.Equals(other.ruleset)
|
||||||
|
&& mods.SequenceEqual(other.mods);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,6 +111,12 @@ namespace osu.Game.Online.API.Requests.Responses
|
|||||||
[JsonProperty(@"language")]
|
[JsonProperty(@"language")]
|
||||||
public BeatmapSetOnlineLanguage Language { get; set; }
|
public BeatmapSetOnlineLanguage Language { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(@"current_nominations")]
|
||||||
|
public BeatmapSetOnlineNomination[]? CurrentNominations { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty(@"related_users")]
|
||||||
|
public APIUser[]? RelatedUsers { get; set; }
|
||||||
|
|
||||||
public string Source { get; set; } = string.Empty;
|
public string Source { get; set; } = string.Empty;
|
||||||
|
|
||||||
[JsonProperty(@"tags")]
|
[JsonProperty(@"tags")]
|
||||||
|
@ -255,6 +255,9 @@ namespace osu.Game.Online.API.Requests.Responses
|
|||||||
[CanBeNull]
|
[CanBeNull]
|
||||||
public Dictionary<string, UserStatistics> RulesetsStatistics { get; set; }
|
public Dictionary<string, UserStatistics> RulesetsStatistics { get; set; }
|
||||||
|
|
||||||
|
[JsonProperty("groups")]
|
||||||
|
public APIUserGroup[] Groups;
|
||||||
|
|
||||||
public override string ToString() => Username;
|
public override string ToString() => Username;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
37
osu.Game/Online/API/Requests/Responses/APIUserGroup.cs
Normal file
37
osu.Game/Online/API/Requests/Responses/APIUserGroup.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
@ -98,6 +98,11 @@ namespace osu.Game.Online.Chat
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public Bindable<Message> HighlightedMessage = new Bindable<Message>();
|
public Bindable<Message> HighlightedMessage = new Bindable<Message>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The current text box message while in this <see cref="Channel"/>.
|
||||||
|
/// </summary>
|
||||||
|
public Bindable<string> TextBoxMessage = new Bindable<string>(string.Empty);
|
||||||
|
|
||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
public Channel()
|
public Channel()
|
||||||
{
|
{
|
||||||
|
@ -64,6 +64,11 @@ namespace osu.Game.Online.Chat
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public IBindableList<Channel> AvailableChannels => availableChannels;
|
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 IAPIProvider api;
|
||||||
private readonly NotificationsClientConnector connector;
|
private readonly NotificationsClientConnector connector;
|
||||||
|
|
||||||
@ -71,7 +76,6 @@ namespace osu.Game.Online.Chat
|
|||||||
private UserLookupCache users { get; set; }
|
private UserLookupCache users { get; set; }
|
||||||
|
|
||||||
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
|
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
|
||||||
private bool channelsInitialised;
|
|
||||||
private ScheduledDelegate scheduledAck;
|
private ScheduledDelegate scheduledAck;
|
||||||
|
|
||||||
private long? lastSilenceMessageId;
|
private long? lastSilenceMessageId;
|
||||||
@ -95,15 +99,7 @@ namespace osu.Game.Online.Chat
|
|||||||
|
|
||||||
connector.NewMessages += msgs => Schedule(() => addMessages(msgs));
|
connector.NewMessages += msgs => Schedule(() => addMessages(msgs));
|
||||||
|
|
||||||
connector.PresenceReceived += () => Schedule(() =>
|
connector.PresenceReceived += () => Schedule(initializeChannels);
|
||||||
{
|
|
||||||
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.Start();
|
connector.Start();
|
||||||
|
|
||||||
@ -335,6 +331,11 @@ namespace osu.Game.Online.Chat
|
|||||||
|
|
||||||
private void initializeChannels()
|
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();
|
var req = new ListChannelsRequest();
|
||||||
|
|
||||||
bool joinDefaults = JoinedChannels.Count == 0;
|
bool joinDefaults = JoinedChannels.Count == 0;
|
||||||
@ -350,10 +351,11 @@ namespace osu.Game.Online.Chat
|
|||||||
joinChannel(ch);
|
joinChannel(ch);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
req.Failure += error =>
|
req.Failure += error =>
|
||||||
{
|
{
|
||||||
Logger.Error(error, "Fetching channel list failed");
|
Logger.Error(error, "Fetching channel list failed");
|
||||||
initializeChannels();
|
Scheduler.AddDelayed(initializeChannels, 60000);
|
||||||
};
|
};
|
||||||
|
|
||||||
api.Queue(req);
|
api.Queue(req);
|
||||||
|
@ -341,6 +341,8 @@ namespace osu.Game.Online.Chat
|
|||||||
OpenWiki,
|
OpenWiki,
|
||||||
Custom,
|
Custom,
|
||||||
OpenChangelog,
|
OpenChangelog,
|
||||||
|
FilterBeatmapSetGenre,
|
||||||
|
FilterBeatmapSetLanguage,
|
||||||
}
|
}
|
||||||
|
|
||||||
public class Link : IComparable<Link>
|
public class Link : IComparable<Link>
|
||||||
|
@ -111,8 +111,13 @@ namespace osu.Game.Online.Chat
|
|||||||
{
|
{
|
||||||
drawableChannel?.Expire();
|
drawableChannel?.Expire();
|
||||||
|
|
||||||
|
if (e.OldValue != null)
|
||||||
|
TextBox?.Current.UnbindFrom(e.OldValue.TextBoxMessage);
|
||||||
|
|
||||||
if (e.NewValue == null) return;
|
if (e.NewValue == null) return;
|
||||||
|
|
||||||
|
TextBox?.Current.BindTo(e.NewValue.TextBoxMessage);
|
||||||
|
|
||||||
drawableChannel = CreateDrawableChannel(e.NewValue);
|
drawableChannel = CreateDrawableChannel(e.NewValue);
|
||||||
drawableChannel.CreateChatLineAction = CreateMessage;
|
drawableChannel.CreateChatLineAction = CreateMessage;
|
||||||
drawableChannel.Padding = new MarginPadding { Bottom = postingTextBox ? text_box_height : 0 };
|
drawableChannel.Padding = new MarginPadding { Bottom = postingTextBox ? text_box_height : 0 };
|
||||||
|
@ -46,24 +46,30 @@ namespace osu.Game.Online.Solo
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="score">The score to listen for the statistics update for.</param>
|
/// <param name="score">The score to listen for the statistics update for.</param>
|
||||||
/// <param name="onUpdateReady">The callback to be invoked once the statistics update has been prepared.</param>
|
/// <param name="onUpdateReady">The callback to be invoked once the statistics update has been prepared.</param>
|
||||||
public void RegisterForStatisticsUpdateAfter(ScoreInfo score, Action<SoloStatisticsUpdate> onUpdateReady) => Schedule(() =>
|
/// <returns>An <see cref="IDisposable"/> representing the subscription. Disposing it is equivalent to unsubscribing from future notifications.</returns>
|
||||||
|
public IDisposable RegisterForStatisticsUpdateAfter(ScoreInfo score, Action<SoloStatisticsUpdate> onUpdateReady)
|
||||||
{
|
{
|
||||||
if (!api.IsLoggedIn)
|
Schedule(() =>
|
||||||
return;
|
|
||||||
|
|
||||||
if (!score.Ruleset.IsLegacyRuleset())
|
|
||||||
return;
|
|
||||||
|
|
||||||
var callback = new StatisticsUpdateCallback(score, onUpdateReady);
|
|
||||||
|
|
||||||
if (lastProcessedScoreId == score.OnlineID)
|
|
||||||
{
|
{
|
||||||
requestStatisticsUpdate(api.LocalUser.Value.Id, callback);
|
if (!api.IsLoggedIn)
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
callbacks.Add(score.OnlineID, callback);
|
if (!score.Ruleset.IsLegacyRuleset() || score.OnlineID <= 0)
|
||||||
});
|
return;
|
||||||
|
|
||||||
|
var callback = new StatisticsUpdateCallback(score, onUpdateReady);
|
||||||
|
|
||||||
|
if (lastProcessedScoreId == score.OnlineID)
|
||||||
|
{
|
||||||
|
requestStatisticsUpdate(api.LocalUser.Value.Id, callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
callbacks.Add(score.OnlineID, callback);
|
||||||
|
});
|
||||||
|
|
||||||
|
return new InvokeOnDisposal(() => Schedule(() => callbacks.Remove(score.OnlineID)));
|
||||||
|
}
|
||||||
|
|
||||||
private void onUserChanged(APIUser? localUser) => Schedule(() =>
|
private void onUserChanged(APIUser? localUser) => Schedule(() =>
|
||||||
{
|
{
|
||||||
@ -75,15 +81,27 @@ namespace osu.Game.Online.Solo
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
var userRequest = new GetUsersRequest(new[] { localUser.OnlineID });
|
var userRequest = new GetUsersRequest(new[] { localUser.OnlineID });
|
||||||
userRequest.Success += response => Schedule(() =>
|
userRequest.Success += initialiseUserStatistics;
|
||||||
{
|
|
||||||
latestStatistics = new Dictionary<string, UserStatistics>();
|
|
||||||
foreach (var rulesetStats in response.Users.Single().RulesetsStatistics)
|
|
||||||
latestStatistics.Add(rulesetStats.Key, rulesetStats.Value);
|
|
||||||
});
|
|
||||||
api.Queue(userRequest);
|
api.Queue(userRequest);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
private void initialiseUserStatistics(GetUsersResponse response) => Schedule(() =>
|
||||||
|
{
|
||||||
|
var user = response.Users.SingleOrDefault();
|
||||||
|
|
||||||
|
// possible if the user is restricted or similar.
|
||||||
|
if (user == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
latestStatistics = new Dictionary<string, UserStatistics>();
|
||||||
|
|
||||||
|
if (user.RulesetsStatistics != null)
|
||||||
|
{
|
||||||
|
foreach (var rulesetStats in user.RulesetsStatistics)
|
||||||
|
latestStatistics.Add(rulesetStats.Key, rulesetStats.Value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
private void userScoreProcessed(int userId, long scoreId)
|
private void userScoreProcessed(int userId, long scoreId)
|
||||||
{
|
{
|
||||||
if (userId != api.LocalUser.Value?.OnlineID)
|
if (userId != api.LocalUser.Value?.OnlineID)
|
||||||
|
@ -46,6 +46,7 @@ using osu.Game.Online;
|
|||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Online.Chat;
|
using osu.Game.Online.Chat;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
|
using osu.Game.Overlays.BeatmapListing;
|
||||||
using osu.Game.Overlays.Music;
|
using osu.Game.Overlays.Music;
|
||||||
using osu.Game.Overlays.Notifications;
|
using osu.Game.Overlays.Notifications;
|
||||||
using osu.Game.Overlays.Toolbar;
|
using osu.Game.Overlays.Toolbar;
|
||||||
@ -306,6 +307,13 @@ namespace osu.Game
|
|||||||
// Transfer any runtime changes back to configuration file.
|
// Transfer any runtime changes back to configuration file.
|
||||||
SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID.ToString();
|
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);
|
IsActive.BindValueChanged(active => updateActiveState(active.NewValue), true);
|
||||||
|
|
||||||
Audio.AddAdjustment(AdjustableProperty.Volume, inactiveVolumeFade);
|
Audio.AddAdjustment(AdjustableProperty.Volume, inactiveVolumeFade);
|
||||||
@ -359,6 +367,14 @@ namespace osu.Game
|
|||||||
SearchBeatmapSet(argString);
|
SearchBeatmapSet(argString);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case LinkAction.FilterBeatmapSetGenre:
|
||||||
|
FilterBeatmapSetGenre((SearchGenre)link.Argument);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case LinkAction.FilterBeatmapSetLanguage:
|
||||||
|
FilterBeatmapSetLanguage((SearchLanguage)link.Argument);
|
||||||
|
break;
|
||||||
|
|
||||||
case LinkAction.OpenEditorTimestamp:
|
case LinkAction.OpenEditorTimestamp:
|
||||||
case LinkAction.JoinMultiplayerMatch:
|
case LinkAction.JoinMultiplayerMatch:
|
||||||
case LinkAction.Spectate:
|
case LinkAction.Spectate:
|
||||||
@ -463,6 +479,10 @@ namespace osu.Game
|
|||||||
/// <param name="query">The query to search for.</param>
|
/// <param name="query">The query to search for.</param>
|
||||||
public void SearchBeatmapSet(string query) => waitForReady(() => beatmapListing, _ => beatmapListing.ShowWithSearch(query));
|
public void SearchBeatmapSet(string query) => waitForReady(() => beatmapListing, _ => beatmapListing.ShowWithSearch(query));
|
||||||
|
|
||||||
|
public void FilterBeatmapSetGenre(SearchGenre genre) => waitForReady(() => beatmapListing, _ => beatmapListing.ShowWithGenreFilter(genre));
|
||||||
|
|
||||||
|
public void FilterBeatmapSetLanguage(SearchLanguage language) => waitForReady(() => beatmapListing, _ => beatmapListing.ShowWithLanguageFilter(language));
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Show a wiki's page as an overlay
|
/// Show a wiki's page as an overlay
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -913,9 +933,9 @@ namespace osu.Game
|
|||||||
loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true);
|
loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true);
|
||||||
loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true);
|
loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true);
|
||||||
var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), 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(chatOverlay = new ChatOverlay(), overlayContent.Add, true);
|
||||||
loadComponentSingleFile(new MessageNotifier(), AddInternal, true);
|
loadComponentSingleFile(new MessageNotifier(), Add, true);
|
||||||
loadComponentSingleFile(Settings = new SettingsOverlay(), leftFloatingOverlayContent.Add, true);
|
loadComponentSingleFile(Settings = new SettingsOverlay(), leftFloatingOverlayContent.Add, true);
|
||||||
loadComponentSingleFile(changelogOverlay = new ChangelogOverlay(), overlayContent.Add, true);
|
loadComponentSingleFile(changelogOverlay = new ChangelogOverlay(), overlayContent.Add, true);
|
||||||
loadComponentSingleFile(userProfile = new UserProfileOverlay(), overlayContent.Add, true);
|
loadComponentSingleFile(userProfile = new UserProfileOverlay(), overlayContent.Add, true);
|
||||||
|
@ -296,7 +296,7 @@ namespace osu.Game
|
|||||||
dependencies.Cache(ScoreDownloader = new ScoreModelDownloader(ScoreManager, API));
|
dependencies.Cache(ScoreDownloader = new ScoreModelDownloader(ScoreManager, API));
|
||||||
|
|
||||||
// Add after all the above cache operations as it depends on them.
|
// Add after all the above cache operations as it depends on them.
|
||||||
AddInternal(difficultyCache);
|
base.Content.Add(difficultyCache);
|
||||||
|
|
||||||
// TODO: OsuGame or OsuGameBase?
|
// TODO: OsuGame or OsuGameBase?
|
||||||
dependencies.CacheAs(beatmapUpdater = new BeatmapUpdater(BeatmapManager, difficultyCache, API, Storage));
|
dependencies.CacheAs(beatmapUpdater = new BeatmapUpdater(BeatmapManager, difficultyCache, API, Storage));
|
||||||
@ -305,19 +305,19 @@ namespace osu.Game
|
|||||||
dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints));
|
dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints));
|
||||||
dependencies.CacheAs(soloStatisticsWatcher = new SoloStatisticsWatcher());
|
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);
|
BeatmapManager.ProcessBeatmap = args => beatmapUpdater.Process(args.beatmapSet, !args.isBatch);
|
||||||
|
|
||||||
dependencies.Cache(userCache = new UserLookupCache());
|
dependencies.Cache(userCache = new UserLookupCache());
|
||||||
AddInternal(userCache);
|
base.Content.Add(userCache);
|
||||||
|
|
||||||
dependencies.Cache(beatmapCache = new BeatmapLookupCache());
|
dependencies.Cache(beatmapCache = new BeatmapLookupCache());
|
||||||
AddInternal(beatmapCache);
|
base.Content.Add(beatmapCache);
|
||||||
|
|
||||||
var scorePerformanceManager = new ScorePerformanceCache();
|
var scorePerformanceManager = new ScorePerformanceCache();
|
||||||
dependencies.Cache(scorePerformanceManager);
|
dependencies.Cache(scorePerformanceManager);
|
||||||
AddInternal(scorePerformanceManager);
|
base.Content.Add(scorePerformanceManager);
|
||||||
|
|
||||||
dependencies.CacheAs<IRulesetConfigCache>(rulesetConfigCache = new RulesetConfigCache(realm, RulesetStore));
|
dependencies.CacheAs<IRulesetConfigCache>(rulesetConfigCache = new RulesetConfigCache(realm, RulesetStore));
|
||||||
|
|
||||||
@ -344,14 +344,24 @@ namespace osu.Game
|
|||||||
|
|
||||||
// add api components to hierarchy.
|
// add api components to hierarchy.
|
||||||
if (API is APIAccess apiAccess)
|
if (API is APIAccess apiAccess)
|
||||||
AddInternal(apiAccess);
|
base.Content.Add(apiAccess);
|
||||||
|
|
||||||
AddInternal(spectatorClient);
|
base.Content.Add(spectatorClient);
|
||||||
AddInternal(MultiplayerClient);
|
base.Content.Add(MultiplayerClient);
|
||||||
AddInternal(metadataClient);
|
base.Content.Add(metadataClient);
|
||||||
AddInternal(soloStatisticsWatcher);
|
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;
|
GlobalActionContainer globalBindings;
|
||||||
|
|
||||||
@ -378,16 +388,6 @@ namespace osu.Game
|
|||||||
|
|
||||||
dependencies.Cache(globalBindings);
|
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);
|
Ruleset.BindValueChanged(onRulesetChanged);
|
||||||
Beatmap.BindValueChanged(onBeatmapChanged);
|
Beatmap.BindValueChanged(onBeatmapChanged);
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,9 @@ namespace osu.Game.Overlays.AccountCreation
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private GameHost host { get; set; }
|
private GameHost host { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private OsuGame game { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
{
|
{
|
||||||
@ -194,9 +197,20 @@ namespace osu.Game.Overlays.AccountCreation
|
|||||||
{
|
{
|
||||||
if (errors != null)
|
if (errors != null)
|
||||||
{
|
{
|
||||||
usernameDescription.AddErrors(errors.User.Username);
|
if (errors.User != null)
|
||||||
emailAddressDescription.AddErrors(errors.User.Email);
|
{
|
||||||
passwordDescription.AddErrors(errors.User.Password);
|
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
|
else
|
||||||
{
|
{
|
||||||
|
@ -145,6 +145,12 @@ namespace osu.Game.Overlays.BeatmapListing
|
|||||||
public void Search(string query)
|
public void Search(string query)
|
||||||
=> Schedule(() => searchControl.Query.Value = query);
|
=> Schedule(() => searchControl.Query.Value = query);
|
||||||
|
|
||||||
|
public void FilterGenre(SearchGenre genre)
|
||||||
|
=> Schedule(() => searchControl.Genre.Value = genre);
|
||||||
|
|
||||||
|
public void FilterLanguage(SearchLanguage language)
|
||||||
|
=> Schedule(() => searchControl.Language.Value = language);
|
||||||
|
|
||||||
protected override void LoadComplete()
|
protected override void LoadComplete()
|
||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
@ -12,6 +12,7 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
@ -100,9 +101,30 @@ namespace osu.Game.Overlays.BeatmapListing
|
|||||||
|
|
||||||
protected partial class MultipleSelectionFilterTabItem : FilterTabItem<T>
|
protected partial class MultipleSelectionFilterTabItem : FilterTabItem<T>
|
||||||
{
|
{
|
||||||
|
private readonly Box selectedUnderline;
|
||||||
|
|
||||||
|
protected override bool HighlightOnHoverWhenActive => true;
|
||||||
|
|
||||||
public MultipleSelectionFilterTabItem(T value)
|
public MultipleSelectionFilterTabItem(T value)
|
||||||
: base(value)
|
: base(value)
|
||||||
{
|
{
|
||||||
|
// This doesn't match any actual design, but should make it easier for the user to understand
|
||||||
|
// that filters are applied until we settle on a final design.
|
||||||
|
AddInternal(selectedUnderline = new Box
|
||||||
|
{
|
||||||
|
Depth = float.MaxValue,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Height = 1.5f,
|
||||||
|
Anchor = Anchor.BottomLeft,
|
||||||
|
Origin = Anchor.CentreLeft,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void UpdateState()
|
||||||
|
{
|
||||||
|
base.UpdateState();
|
||||||
|
selectedUnderline.FadeTo(Active.Value ? 1 : 0, 200, Easing.OutQuint);
|
||||||
|
selectedUnderline.FadeColour(IsHovered ? ColourProvider.Content2 : GetStateColour(), 200, Easing.OutQuint);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnClick(ClickEvent e)
|
protected override bool OnClick(ClickEvent e)
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Localisation;
|
using osu.Framework.Localisation;
|
||||||
|
using osu.Game.Extensions;
|
||||||
using osu.Game.Resources.Localisation.Web;
|
using osu.Game.Resources.Localisation.Web;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
|
|
||||||
@ -28,7 +29,13 @@ namespace osu.Game.Overlays.BeatmapListing
|
|||||||
AddTabItem(new RulesetFilterTabItemAny());
|
AddTabItem(new RulesetFilterTabItemAny());
|
||||||
|
|
||||||
foreach (var r in rulesets.AvailableRulesets)
|
foreach (var r in rulesets.AvailableRulesets)
|
||||||
|
{
|
||||||
|
// Don't display non-legacy rulesets
|
||||||
|
if (!r.IsLegacyRuleset())
|
||||||
|
continue;
|
||||||
|
|
||||||
AddItem(r);
|
AddItem(r);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Overlays.BeatmapListing
|
|||||||
public partial class FilterTabItem<T> : TabItem<T>
|
public partial class FilterTabItem<T> : TabItem<T>
|
||||||
{
|
{
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OverlayColourProvider colourProvider { get; set; }
|
protected OverlayColourProvider ColourProvider { get; private set; }
|
||||||
|
|
||||||
private OsuSpriteText text;
|
private OsuSpriteText text;
|
||||||
|
|
||||||
@ -52,38 +52,42 @@ namespace osu.Game.Overlays.BeatmapListing
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
updateState();
|
UpdateState();
|
||||||
FinishTransforms(true);
|
FinishTransforms(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnHover(HoverEvent e)
|
protected override bool OnHover(HoverEvent e)
|
||||||
{
|
{
|
||||||
base.OnHover(e);
|
base.OnHover(e);
|
||||||
updateState();
|
UpdateState();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnHoverLost(HoverLostEvent e)
|
protected override void OnHoverLost(HoverLostEvent e)
|
||||||
{
|
{
|
||||||
base.OnHoverLost(e);
|
base.OnHoverLost(e);
|
||||||
updateState();
|
UpdateState();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnActivated() => updateState();
|
protected override void OnActivated() => UpdateState();
|
||||||
|
|
||||||
protected override void OnDeactivated() => updateState();
|
protected override void OnDeactivated() => UpdateState();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the label text to be used for the supplied <paramref name="value"/>.
|
/// Returns the label text to be used for the supplied <paramref name="value"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected virtual LocalisableString LabelFor(T value) => (value as Enum)?.GetLocalisableDescription() ?? value.ToString();
|
protected virtual LocalisableString LabelFor(T value) => (value as Enum)?.GetLocalisableDescription() ?? value.ToString();
|
||||||
|
|
||||||
private void updateState()
|
protected virtual bool HighlightOnHoverWhenActive => false;
|
||||||
|
|
||||||
|
protected virtual void UpdateState()
|
||||||
{
|
{
|
||||||
text.FadeColour(IsHovered ? colourProvider.Light1 : GetStateColour(), 200, Easing.OutQuint);
|
bool highlightHover = IsHovered && (!Active.Value || HighlightOnHoverWhenActive);
|
||||||
text.Font = text.Font.With(weight: Active.Value ? FontWeight.SemiBold : FontWeight.Regular);
|
|
||||||
|
text.FadeColour(highlightHover ? ColourProvider.Content2 : GetStateColour(), 200, Easing.OutQuint);
|
||||||
|
text.Font = text.Font.With(weight: Active.Value ? FontWeight.Bold : FontWeight.Regular);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual Color4 GetStateColour() => Active.Value ? colourProvider.Content1 : colourProvider.Light2;
|
protected virtual Color4 GetStateColour() => Active.Value ? ColourProvider.Content1 : ColourProvider.Light2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,6 +110,18 @@ namespace osu.Game.Overlays
|
|||||||
ScrollFlow.ScrollToStart();
|
ScrollFlow.ScrollToStart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ShowWithGenreFilter(SearchGenre genre)
|
||||||
|
{
|
||||||
|
ShowWithSearch(string.Empty);
|
||||||
|
filterControl.FilterGenre(genre);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowWithLanguageFilter(SearchLanguage language)
|
||||||
|
{
|
||||||
|
ShowWithSearch(string.Empty);
|
||||||
|
filterControl.FilterLanguage(language);
|
||||||
|
}
|
||||||
|
|
||||||
protected override BeatmapListingHeader CreateHeader() => new BeatmapListingHeader();
|
protected override BeatmapListingHeader CreateHeader() => new BeatmapListingHeader();
|
||||||
|
|
||||||
protected override Color4 BackgroundColour => ColourProvider.Background6;
|
protected override Color4 BackgroundColour => ColourProvider.Background6;
|
||||||
|
@ -10,7 +10,6 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Colour;
|
using osu.Framework.Graphics.Colour;
|
||||||
using osu.Game.Graphics.Cursor;
|
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
@ -91,79 +90,74 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new OsuContextMenuContainer
|
new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
AutoSizeAxes = Axes.Y,
|
AutoSizeAxes = Axes.Y,
|
||||||
Child = new Container
|
Padding = new MarginPadding
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
Vertical = BeatmapSetOverlay.Y_PADDING,
|
||||||
AutoSizeAxes = Axes.Y,
|
Left = BeatmapSetOverlay.X_PADDING,
|
||||||
Padding = new MarginPadding
|
Right = BeatmapSetOverlay.X_PADDING + BeatmapSetOverlay.RIGHT_WIDTH,
|
||||||
|
},
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
fadeContent = new FillFlowContainer
|
||||||
{
|
{
|
||||||
Vertical = BeatmapSetOverlay.Y_PADDING,
|
RelativeSizeAxes = Axes.X,
|
||||||
Left = BeatmapSetOverlay.X_PADDING,
|
AutoSizeAxes = Axes.Y,
|
||||||
Right = BeatmapSetOverlay.X_PADDING + BeatmapSetOverlay.RIGHT_WIDTH,
|
Direction = FillDirection.Vertical,
|
||||||
},
|
Children = new Drawable[]
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
fadeContent = new FillFlowContainer
|
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
new Container
|
||||||
AutoSizeAxes = Axes.Y,
|
|
||||||
Direction = FillDirection.Vertical,
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
{
|
||||||
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,
|
favouriteButton = new FavouriteButton
|
||||||
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
|
BeatmapSet = { BindTarget = BeatmapSet }
|
||||||
{
|
|
||||||
BeatmapSet = { BindTarget = BeatmapSet }
|
|
||||||
},
|
|
||||||
downloadButtonsContainer = new FillFlowContainer
|
|
||||||
{
|
|
||||||
RelativeSizeAxes = Axes.Both,
|
|
||||||
Padding = new MarginPadding { Left = buttons_height + buttons_spacing },
|
|
||||||
Spacing = new Vector2(buttons_spacing),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
downloadButtonsContainer = new FillFlowContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Padding = new MarginPadding { Left = buttons_height + buttons_spacing },
|
||||||
|
Spacing = new Vector2(buttons_spacing),
|
||||||
|
},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
loading = new LoadingSpinner
|
loading = new LoadingSpinner
|
||||||
{
|
{
|
||||||
|
@ -3,14 +3,17 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
using System;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Overlays.BeatmapListing;
|
||||||
|
|
||||||
namespace osu.Game.Overlays.BeatmapSet
|
namespace osu.Game.Overlays.BeatmapSet
|
||||||
{
|
{
|
||||||
@ -34,7 +37,10 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
|
|
||||||
public Info()
|
public Info()
|
||||||
{
|
{
|
||||||
MetadataSection source, tags, genre, language;
|
MetadataSectionNominators nominators;
|
||||||
|
MetadataSection source, tags;
|
||||||
|
MetadataSectionGenre genre;
|
||||||
|
MetadataSectionLanguage language;
|
||||||
OsuSpriteText notRankedPlaceholder;
|
OsuSpriteText notRankedPlaceholder;
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
@ -59,7 +65,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
Child = new Container
|
Child = new Container
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Child = new MetadataSection(MetadataType.Description),
|
Child = new MetadataSectionDescription(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
new Container
|
new Container
|
||||||
@ -76,12 +82,13 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
AutoSizeAxes = Axes.Y,
|
AutoSizeAxes = Axes.Y,
|
||||||
Direction = FillDirection.Full,
|
Direction = FillDirection.Full,
|
||||||
Children = new[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
source = new MetadataSection(MetadataType.Source),
|
nominators = new MetadataSectionNominators(),
|
||||||
genre = new MetadataSection(MetadataType.Genre) { Width = 0.5f },
|
source = new MetadataSectionSource(),
|
||||||
language = new MetadataSection(MetadataType.Language) { Width = 0.5f },
|
genre = new MetadataSectionGenre { Width = 0.5f },
|
||||||
tags = new MetadataSection(MetadataType.Tags),
|
language = new MetadataSectionLanguage { Width = 0.5f },
|
||||||
|
tags = new MetadataSectionTags(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -118,10 +125,11 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
|
|
||||||
BeatmapSet.ValueChanged += b =>
|
BeatmapSet.ValueChanged += b =>
|
||||||
{
|
{
|
||||||
source.Text = b.NewValue?.Source ?? string.Empty;
|
nominators.Metadata = (b.NewValue?.CurrentNominations ?? Array.Empty<BeatmapSetOnlineNomination>(), b.NewValue?.RelatedUsers ?? Array.Empty<APIUser>());
|
||||||
tags.Text = b.NewValue?.Tags ?? string.Empty;
|
source.Metadata = b.NewValue?.Source ?? string.Empty;
|
||||||
genre.Text = b.NewValue?.Genre.Name ?? string.Empty;
|
tags.Metadata = b.NewValue?.Tags ?? string.Empty;
|
||||||
language.Text = b.NewValue?.Language.Name ?? string.Empty;
|
genre.Metadata = b.NewValue?.Genre ?? new BeatmapSetOnlineGenre { Id = (int)SearchGenre.Unspecified };
|
||||||
|
language.Metadata = b.NewValue?.Language ?? new BeatmapSetOnlineLanguage { Id = (int)SearchLanguage.Unspecified };
|
||||||
bool setHasLeaderboard = b.NewValue?.Status > 0;
|
bool setHasLeaderboard = b.NewValue?.Status > 0;
|
||||||
successRate.Alpha = setHasLeaderboard ? 1 : 0;
|
successRate.Alpha = setHasLeaderboard ? 1 : 0;
|
||||||
notRankedPlaceholder.Alpha = setHasLeaderboard ? 0 : 1;
|
notRankedPlaceholder.Alpha = setHasLeaderboard ? 0 : 1;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// 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.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using osu.Framework.Extensions;
|
using osu.Framework.Extensions;
|
||||||
using osu.Framework.Extensions.Color4Extensions;
|
using osu.Framework.Extensions.Color4Extensions;
|
||||||
@ -11,26 +9,45 @@ using osu.Framework.Graphics.Containers;
|
|||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Online.Chat;
|
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
namespace osu.Game.Overlays.BeatmapSet
|
namespace osu.Game.Overlays.BeatmapSet
|
||||||
{
|
{
|
||||||
public partial class MetadataSection : Container
|
public abstract partial class MetadataSection : MetadataSection<string>
|
||||||
|
{
|
||||||
|
public override string Metadata
|
||||||
|
{
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
this.FadeOut(TRANSITION_DURATION);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
base.Metadata = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected MetadataSection(MetadataType type, Action<string>? searchAction = null)
|
||||||
|
: base(type, searchAction)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract partial class MetadataSection<T> : Container
|
||||||
{
|
{
|
||||||
private readonly FillFlowContainer textContainer;
|
private readonly FillFlowContainer textContainer;
|
||||||
private readonly MetadataType type;
|
private TextFlowContainer? textFlow;
|
||||||
private TextFlowContainer textFlow;
|
|
||||||
|
|
||||||
private readonly Action<string> searchAction;
|
protected readonly Action<T>? SearchAction;
|
||||||
|
|
||||||
private const float transition_duration = 250;
|
protected const float TRANSITION_DURATION = 250;
|
||||||
|
|
||||||
public MetadataSection(MetadataType type, Action<string> searchAction = null)
|
protected MetadataSection(MetadataType type, Action<T>? searchAction = null)
|
||||||
{
|
{
|
||||||
this.type = type;
|
SearchAction = searchAction;
|
||||||
this.searchAction = searchAction;
|
|
||||||
|
|
||||||
Alpha = 0;
|
Alpha = 0;
|
||||||
|
|
||||||
@ -53,7 +70,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
AutoSizeAxes = Axes.Y,
|
AutoSizeAxes = Axes.Y,
|
||||||
Child = new OsuSpriteText
|
Child = new OsuSpriteText
|
||||||
{
|
{
|
||||||
Text = this.type.GetLocalisableDescription(),
|
Text = type.GetLocalisableDescription(),
|
||||||
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 14),
|
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 14),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -61,23 +78,23 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Text
|
public virtual T Metadata
|
||||||
{
|
{
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(value))
|
if (value == null)
|
||||||
{
|
{
|
||||||
this.FadeOut(transition_duration);
|
this.FadeOut(TRANSITION_DURATION);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.FadeIn(transition_duration);
|
this.FadeIn(TRANSITION_DURATION);
|
||||||
|
|
||||||
setTextAsync(value);
|
setTextFlowAsync(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setTextAsync(string text)
|
private void setTextFlowAsync(T metadata)
|
||||||
{
|
{
|
||||||
LoadComponentAsync(new LinkFlowContainer(s => s.Font = s.Font.With(size: 14))
|
LoadComponentAsync(new LinkFlowContainer(s => s.Font = s.Font.With(size: 14))
|
||||||
{
|
{
|
||||||
@ -88,44 +105,15 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
{
|
{
|
||||||
textFlow?.Expire();
|
textFlow?.Expire();
|
||||||
|
|
||||||
switch (type)
|
AddMetadata(metadata, loaded);
|
||||||
{
|
|
||||||
case MetadataType.Tags:
|
|
||||||
string[] tags = text.Split(" ");
|
|
||||||
|
|
||||||
for (int i = 0; i <= tags.Length - 1; i++)
|
|
||||||
{
|
|
||||||
string tag = tags[i];
|
|
||||||
|
|
||||||
if (searchAction != null)
|
|
||||||
loaded.AddLink(tag, () => searchAction(tag));
|
|
||||||
else
|
|
||||||
loaded.AddLink(tag, LinkAction.SearchBeatmapSet, tag);
|
|
||||||
|
|
||||||
if (i != tags.Length - 1)
|
|
||||||
loaded.AddText(" ");
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
case MetadataType.Source:
|
|
||||||
if (searchAction != null)
|
|
||||||
loaded.AddLink(text, () => searchAction(text));
|
|
||||||
else
|
|
||||||
loaded.AddLink(text, LinkAction.SearchBeatmapSet, text);
|
|
||||||
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
loaded.AddText(text);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
textContainer.Add(textFlow = loaded);
|
textContainer.Add(textFlow = loaded);
|
||||||
|
|
||||||
// fade in if we haven't yet.
|
// fade in if we haven't yet.
|
||||||
textContainer.FadeIn(transition_duration);
|
textContainer.FadeIn(TRANSITION_DURATION);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract void AddMetadata(T metadata, LinkFlowContainer loaded);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
21
osu.Game/Overlays/BeatmapSet/MetadataSectionDescription.cs
Normal file
21
osu.Game/Overlays/BeatmapSet/MetadataSectionDescription.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// 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.Game.Graphics.Containers;
|
||||||
|
|
||||||
|
namespace osu.Game.Overlays.BeatmapSet
|
||||||
|
{
|
||||||
|
public partial class MetadataSectionDescription : MetadataSection
|
||||||
|
{
|
||||||
|
public MetadataSectionDescription(Action<string>? searchAction = null)
|
||||||
|
: base(MetadataType.Description, searchAction)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void AddMetadata(string metadata, LinkFlowContainer loaded)
|
||||||
|
{
|
||||||
|
loaded.AddText(metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
osu.Game/Overlays/BeatmapSet/MetadataSectionGenre.cs
Normal file
30
osu.Game/Overlays/BeatmapSet/MetadataSectionGenre.cs
Normal 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 System;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Online.Chat;
|
||||||
|
using osu.Game.Overlays.BeatmapListing;
|
||||||
|
|
||||||
|
namespace osu.Game.Overlays.BeatmapSet
|
||||||
|
{
|
||||||
|
public partial class MetadataSectionGenre : MetadataSection<BeatmapSetOnlineGenre>
|
||||||
|
{
|
||||||
|
public MetadataSectionGenre(Action<BeatmapSetOnlineGenre>? searchAction = null)
|
||||||
|
: base(MetadataType.Genre, searchAction)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void AddMetadata(BeatmapSetOnlineGenre metadata, LinkFlowContainer loaded)
|
||||||
|
{
|
||||||
|
var genre = (SearchGenre)metadata.Id;
|
||||||
|
|
||||||
|
if (Enum.IsDefined(genre))
|
||||||
|
loaded.AddLink(genre.GetLocalisableDescription(), LinkAction.FilterBeatmapSetGenre, genre);
|
||||||
|
else
|
||||||
|
loaded.AddText(metadata.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
30
osu.Game/Overlays/BeatmapSet/MetadataSectionLanguage.cs
Normal file
30
osu.Game/Overlays/BeatmapSet/MetadataSectionLanguage.cs
Normal 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 System;
|
||||||
|
using osu.Framework.Extensions;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Online.Chat;
|
||||||
|
using osu.Game.Overlays.BeatmapListing;
|
||||||
|
|
||||||
|
namespace osu.Game.Overlays.BeatmapSet
|
||||||
|
{
|
||||||
|
public partial class MetadataSectionLanguage : MetadataSection<BeatmapSetOnlineLanguage>
|
||||||
|
{
|
||||||
|
public MetadataSectionLanguage(Action<BeatmapSetOnlineLanguage>? searchAction = null)
|
||||||
|
: base(MetadataType.Language, searchAction)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void AddMetadata(BeatmapSetOnlineLanguage metadata, LinkFlowContainer loaded)
|
||||||
|
{
|
||||||
|
var language = (SearchLanguage)metadata.Id;
|
||||||
|
|
||||||
|
if (Enum.IsDefined(language))
|
||||||
|
loaded.AddLink(language.GetLocalisableDescription(), LinkAction.FilterBeatmapSetLanguage, language);
|
||||||
|
else
|
||||||
|
loaded.AddText(metadata.Name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
63
osu.Game/Overlays/BeatmapSet/MetadataSectionNominators.cs
Normal file
63
osu.Game/Overlays/BeatmapSet/MetadataSectionNominators.cs
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// 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.Linq;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
|
using osu.Game.Resources.Localisation.Web;
|
||||||
|
|
||||||
|
namespace osu.Game.Overlays.BeatmapSet
|
||||||
|
{
|
||||||
|
public partial class MetadataSectionNominators : MetadataSection<(BeatmapSetOnlineNomination[] CurrentNominations, APIUser[] RelatedUsers)>
|
||||||
|
{
|
||||||
|
public override (BeatmapSetOnlineNomination[] CurrentNominations, APIUser[] RelatedUsers) Metadata
|
||||||
|
{
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value.CurrentNominations.Length == 0)
|
||||||
|
{
|
||||||
|
this.FadeOut(TRANSITION_DURATION);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
base.Metadata = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public MetadataSectionNominators(Action<(BeatmapSetOnlineNomination[] CurrentNominations, APIUser[] RelatedUsers)>? searchAction = null)
|
||||||
|
: base(MetadataType.Nominators, searchAction)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void AddMetadata((BeatmapSetOnlineNomination[] CurrentNominations, APIUser[] RelatedUsers) metadata, LinkFlowContainer loaded)
|
||||||
|
{
|
||||||
|
int[] nominatorIds = metadata.CurrentNominations.Select(n => n.UserId).ToArray();
|
||||||
|
|
||||||
|
int nominatorsFound = 0;
|
||||||
|
|
||||||
|
foreach (int nominatorId in nominatorIds)
|
||||||
|
{
|
||||||
|
foreach (var user in metadata.RelatedUsers)
|
||||||
|
{
|
||||||
|
if (nominatorId != user.OnlineID) continue;
|
||||||
|
|
||||||
|
nominatorsFound++;
|
||||||
|
|
||||||
|
loaded.AddUserLink(new APIUser
|
||||||
|
{
|
||||||
|
Username = user.Username,
|
||||||
|
Id = nominatorId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (nominatorsFound < nominatorIds.Length)
|
||||||
|
loaded.AddText(CommonStrings.ArrayAndWordsConnector);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
25
osu.Game/Overlays/BeatmapSet/MetadataSectionSource.cs
Normal file
25
osu.Game/Overlays/BeatmapSet/MetadataSectionSource.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
// 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.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Online.Chat;
|
||||||
|
|
||||||
|
namespace osu.Game.Overlays.BeatmapSet
|
||||||
|
{
|
||||||
|
public partial class MetadataSectionSource : MetadataSection
|
||||||
|
{
|
||||||
|
public MetadataSectionSource(Action<string>? searchAction = null)
|
||||||
|
: base(MetadataType.Source, searchAction)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void AddMetadata(string metadata, LinkFlowContainer loaded)
|
||||||
|
{
|
||||||
|
if (SearchAction != null)
|
||||||
|
loaded.AddLink(metadata, () => SearchAction(metadata));
|
||||||
|
else
|
||||||
|
loaded.AddLink(metadata, LinkAction.SearchBeatmapSet, metadata);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
35
osu.Game/Overlays/BeatmapSet/MetadataSectionTags.cs
Normal file
35
osu.Game/Overlays/BeatmapSet/MetadataSectionTags.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// 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.Game.Graphics.Containers;
|
||||||
|
using osu.Game.Online.Chat;
|
||||||
|
|
||||||
|
namespace osu.Game.Overlays.BeatmapSet
|
||||||
|
{
|
||||||
|
public partial class MetadataSectionTags : MetadataSection
|
||||||
|
{
|
||||||
|
public MetadataSectionTags(Action<string>? searchAction = null)
|
||||||
|
: base(MetadataType.Tags, searchAction)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void AddMetadata(string metadata, LinkFlowContainer loaded)
|
||||||
|
{
|
||||||
|
string[] tags = metadata.Split(" ");
|
||||||
|
|
||||||
|
for (int i = 0; i <= tags.Length - 1; i++)
|
||||||
|
{
|
||||||
|
string tag = tags[i];
|
||||||
|
|
||||||
|
if (SearchAction != null)
|
||||||
|
loaded.AddLink(tag, () => SearchAction(tag));
|
||||||
|
else
|
||||||
|
loaded.AddLink(tag, LinkAction.SearchBeatmapSet, tag);
|
||||||
|
|
||||||
|
if (i != tags.Length - 1)
|
||||||
|
loaded.AddText(" ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -23,6 +23,9 @@ namespace osu.Game.Overlays.BeatmapSet
|
|||||||
Genre,
|
Genre,
|
||||||
|
|
||||||
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoLanguage))]
|
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoLanguage))]
|
||||||
Language
|
Language,
|
||||||
|
|
||||||
|
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoNominators))]
|
||||||
|
Nominators,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests;
|
using osu.Game.Online.API.Requests;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Overlays.BeatmapSet;
|
using osu.Game.Overlays.BeatmapSet;
|
||||||
@ -30,6 +31,13 @@ namespace osu.Game.Overlays
|
|||||||
|
|
||||||
private readonly Bindable<APIBeatmapSet> beatmapSet = new Bindable<APIBeatmapSet>();
|
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>
|
/// <remarks>
|
||||||
/// Isolates the beatmap set overlay from the game-wide selected mods bindable
|
/// 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"/>).
|
/// 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 BeatmapSetHeader CreateHeader() => new BeatmapSetHeader();
|
||||||
|
|
||||||
protected override Color4 BackgroundColour => ColourProvider.Background6;
|
protected override Color4 BackgroundColour => ColourProvider.Background6;
|
||||||
@ -84,27 +103,20 @@ namespace osu.Game.Overlays
|
|||||||
|
|
||||||
public void FetchAndShowBeatmap(int beatmapId)
|
public void FetchAndShowBeatmap(int beatmapId)
|
||||||
{
|
{
|
||||||
|
lastLookup = (BeatmapSetLookupType.BeatmapId, beatmapId);
|
||||||
beatmapSet.Value = null;
|
beatmapSet.Value = null;
|
||||||
|
|
||||||
var req = new GetBeatmapSetRequest(beatmapId, BeatmapSetLookupType.BeatmapId);
|
performFetch();
|
||||||
req.Success += res =>
|
|
||||||
{
|
|
||||||
beatmapSet.Value = res;
|
|
||||||
Header.HeaderContent.Picker.Beatmap.Value = Header.BeatmapSet.Value.Beatmaps.First(b => b.OnlineID == beatmapId);
|
|
||||||
};
|
|
||||||
API.Queue(req);
|
|
||||||
|
|
||||||
Show();
|
Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void FetchAndShowBeatmapSet(int beatmapSetId)
|
public void FetchAndShowBeatmapSet(int beatmapSetId)
|
||||||
{
|
{
|
||||||
|
lastLookup = (BeatmapSetLookupType.SetId, beatmapSetId);
|
||||||
|
|
||||||
beatmapSet.Value = null;
|
beatmapSet.Value = null;
|
||||||
|
|
||||||
var req = new GetBeatmapSetRequest(beatmapSetId);
|
performFetch();
|
||||||
req.Success += res => beatmapSet.Value = res;
|
|
||||||
API.Queue(req);
|
|
||||||
|
|
||||||
Show();
|
Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,6 +130,24 @@ namespace osu.Game.Overlays
|
|||||||
Show();
|
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
|
private partial class CommentsSection : BeatmapSetLayoutSection
|
||||||
{
|
{
|
||||||
public readonly Bindable<APIBeatmapSet> BeatmapSet = new Bindable<APIBeatmapSet>();
|
public readonly Bindable<APIBeatmapSet> BeatmapSet = new Bindable<APIBeatmapSet>();
|
||||||
|
@ -128,9 +128,8 @@ namespace osu.Game.Overlays.Chat
|
|||||||
chattingTextContainer.FadeTo(showSearch ? 0 : 1);
|
chattingTextContainer.FadeTo(showSearch ? 0 : 1);
|
||||||
searchIconContainer.FadeTo(showSearch ? 1 : 0);
|
searchIconContainer.FadeTo(showSearch ? 1 : 0);
|
||||||
|
|
||||||
// Clear search terms if any exist when switching back to chat mode
|
if (showSearch)
|
||||||
if (!showSearch)
|
OnSearchTermsChanged?.Invoke(chatTextBox.Current.Value);
|
||||||
OnSearchTermsChanged?.Invoke(string.Empty);
|
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
currentChannel.BindValueChanged(change =>
|
currentChannel.BindValueChanged(change =>
|
||||||
@ -151,6 +150,12 @@ namespace osu.Game.Overlays.Chat
|
|||||||
chattingText.Text = ChatStrings.TalkingIn(newChannel.Name);
|
chattingText.Text = ChatStrings.TalkingIn(newChannel.Name);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (change.OldValue != null)
|
||||||
|
chatTextBox.Current.UnbindFrom(change.OldValue.TextBoxMessage);
|
||||||
|
|
||||||
|
if (newChannel != null)
|
||||||
|
chatTextBox.Current.BindTo(newChannel.TextBoxMessage);
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +24,6 @@ namespace osu.Game.Overlays.Chat
|
|||||||
bool showSearch = change.NewValue;
|
bool showSearch = change.NewValue;
|
||||||
|
|
||||||
PlaceholderText = showSearch ? HomeStrings.SearchPlaceholder : ChatStrings.InputPlaceholder;
|
PlaceholderText = showSearch ? HomeStrings.SearchPlaceholder : ChatStrings.InputPlaceholder;
|
||||||
Text = string.Empty;
|
|
||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,9 +17,11 @@ using osu.Game.Graphics;
|
|||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Localisation;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Online.Chat;
|
using osu.Game.Online.Chat;
|
||||||
|
using osu.Game.Resources.Localisation.Web;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
|
|
||||||
@ -148,11 +150,11 @@ namespace osu.Game.Overlays.Chat
|
|||||||
|
|
||||||
List<MenuItem> items = new List<MenuItem>
|
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))
|
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();
|
return items.ToArray();
|
||||||
}
|
}
|
||||||
|
@ -38,7 +38,7 @@ namespace osu.Game.Overlays.Chat.Listing
|
|||||||
private Box hoverBox = null!;
|
private Box hoverBox = null!;
|
||||||
private SpriteIcon checkbox = null!;
|
private SpriteIcon checkbox = null!;
|
||||||
private OsuSpriteText channelText = null!;
|
private OsuSpriteText channelText = null!;
|
||||||
private OsuSpriteText topicText = null!;
|
private OsuTextFlowContainer topicText = null!;
|
||||||
private IBindable<bool> channelJoined = null!;
|
private IBindable<bool> channelJoined = null!;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
@ -65,8 +65,8 @@ namespace osu.Game.Overlays.Chat.Listing
|
|||||||
|
|
||||||
Masking = true;
|
Masking = true;
|
||||||
CornerRadius = 5;
|
CornerRadius = 5;
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Content.RelativeSizeAxes = Axes.X;
|
||||||
Height = 20 + (vertical_margin * 2);
|
AutoSizeAxes = Content.AutoSizeAxes = Axes.Y;
|
||||||
|
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
@ -79,14 +79,19 @@ namespace osu.Game.Overlays.Chat.Listing
|
|||||||
},
|
},
|
||||||
new GridContainer
|
new GridContainer
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
ColumnDimensions = new[]
|
ColumnDimensions = new[]
|
||||||
{
|
{
|
||||||
new Dimension(GridSizeMode.Absolute, 40),
|
new Dimension(GridSizeMode.Absolute, 40),
|
||||||
new Dimension(GridSizeMode.Absolute, 200),
|
new Dimension(GridSizeMode.Absolute, 200),
|
||||||
new Dimension(GridSizeMode.Absolute, 400),
|
new Dimension(maxSize: 400),
|
||||||
new Dimension(GridSizeMode.AutoSize),
|
new Dimension(GridSizeMode.AutoSize),
|
||||||
new Dimension(),
|
new Dimension(GridSizeMode.Absolute, 50), // enough for any 5 digit user count
|
||||||
|
},
|
||||||
|
RowDimensions = new[]
|
||||||
|
{
|
||||||
|
new Dimension(GridSizeMode.AutoSize, minSize: 20 + (vertical_margin * 2)),
|
||||||
},
|
},
|
||||||
Content = new[]
|
Content = new[]
|
||||||
{
|
{
|
||||||
@ -108,12 +113,13 @@ namespace osu.Game.Overlays.Chat.Listing
|
|||||||
Font = OsuFont.Torus.With(size: text_size, weight: FontWeight.SemiBold),
|
Font = OsuFont.Torus.With(size: text_size, weight: FontWeight.SemiBold),
|
||||||
Margin = new MarginPadding { Bottom = 2 },
|
Margin = new MarginPadding { Bottom = 2 },
|
||||||
},
|
},
|
||||||
topicText = new OsuSpriteText
|
topicText = new OsuTextFlowContainer(t => t.Font = OsuFont.Torus.With(size: text_size))
|
||||||
{
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
Anchor = Anchor.CentreLeft,
|
Anchor = Anchor.CentreLeft,
|
||||||
Origin = Anchor.CentreLeft,
|
Origin = Anchor.CentreLeft,
|
||||||
Text = Channel.Topic,
|
Text = Channel.Topic,
|
||||||
Font = OsuFont.Torus.With(size: text_size),
|
|
||||||
Margin = new MarginPadding { Bottom = 2 },
|
Margin = new MarginPadding { Bottom = 2 },
|
||||||
},
|
},
|
||||||
new SpriteIcon
|
new SpriteIcon
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user