diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000000..6c327f01b3
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,137 @@
+# Contributing Guidelines
+
+Thank you for showing interest in the development of osu!lazer! We aim to provide a good collaborating environment for everyone involved, and as such have decided to list some of the most important things to keep in mind in the process. The guidelines below have been chosen based on past experience.
+
+These are not "official rules" *per se*, but following them will help everyone deal with things in the most efficient manner.
+
+## Table of contents
+
+1. [I would like to submit an issue!](#i-would-like-to-submit-an-issue)
+2. [I would like to submit a pull request!](#i-would-like-to-submit-a-pull-request)
+
+## I would like to submit an issue!
+
+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.
+
+* **Before submitting an issue, try searching existing issues first.**
+
+ 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.
+
+* **When submitting a bug report, please try to include as much detail as possible.**
+
+ 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:
+
+ * the in-game logs, which are located at:
+ * `%AppData%/osu/logs` (on Windows),
+ * `~/.local/share/osu/logs` (on Linux and macOS),
+ * `Android/Data/sh.ppy.osulazer/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.**
+
+ 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 lazer 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!
+
+* **When submitting a feature proposal, please describe it in the most understandable way you can.**
+
+ 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.
+
+* **Refrain from posting "+1" comments.**
+
+ 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.
+
+* **Refrain from asking if an issue has been resolved yet.**
+
+ 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.
+
+* **Avoid long discussions about non-development topics.**
+
+ 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.
+
+## I would like to submit a pull request!
+
+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.
+
+However, do keep in mind that the core team is committed to bringing osu!lazer up to par with 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).
+
+Here are some key things to note before jumping in:
+
+* **Make sure you are comfortable with C\# and your development environment.**
+
+ 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.
+
+ 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.
+
+* **Make sure you are familiar with git and the pull request workflow.**
+
+ [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.
+
+ 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).
+
+* **Double-check designs before starting work on new functionality.**
+
+ When implementing new features, keep in mind that we already have a lot of the UI designed. If you wish to work on something with the intention of having it included in the official distribution, please open an issue for discussion and we will give you what you need from a design perspective to proceed. If you want to make *changes* to the design, we recommend you open an issue with your intentions before spending too much time to ensure no effort is wasted.
+
+* **Make sure to submit pull requests off of a topic branch.**
+
+ As described in the article linked in the previous point, topic branches help you parallelise your work and separate it from the main `master` branch, and additionally are easier for maintainers to work with. Working with multiple `master` branches across many remotes is difficult to keep track of, and it's easy to make a mistake and push to the wrong `master` branch by accident.
+
+* **Refrain from making changes through the GitHub web interface.**
+
+ Even though GitHub provides an option to edit code or replace files in the repository using the web interface, we strongly discourage using it in most scenarios. Editing files this way is inefficient and likely to introduce whitespace or file encoding changes that make it more difficult to review the code.
+
+ Code written through the web interface will also very likely be questioned outright by the reviewers, as it is likely that it has not been properly tested or that it will fail continuous integration checks. We strongly encourage using an IDE like [Visual Studio](https://visualstudio.microsoft.com/), [Visual Studio Code](https://code.visualstudio.com/) or [JetBrains Rider](https://www.jetbrains.com/rider/) instead.
+
+* **Add tests for your code whenever possible.**
+
+ Automated tests are an essential part of a quality and reliable codebase. They help to make the code more maintainable by ensuring it is safe to reorganise (or refactor) the code in various ways, and also prevent regressions - bugs that resurface after having been fixed at some point in the past. If it is viable, please put in the time to add tests, so that the changes you make can last for a (hopefully) very long time.
+
+* **Run tests before opening a pull request.**
+
+ Tying into the previous point, sometimes changes in one part of the codebase can result in unpredictable changes in behaviour in other pieces of the code. This is why it is best to always try to run tests before opening a PR.
+
+ Continuous integration will always run the tests for you (and us), too, but it is best not to rely on it, as there might be many builds queued at any time. Running tests on your own will help you be more certain that at the point of clicking the "Create pull request" button, your changes are as ready as can be.
+
+* **Run code style analysis before opening a pull request.**
+
+ As part of continuous integration, we also run code style analysis, which is supposed to make sure that your code is formatted the same way as all the pre-existing code in the repository. The reason we enforce a particular code style everywhere is to make sure the codebase is consistent in that regard - having one whitespace convention in one place and another one elsewhere causes disorganisation.
+
+* **Make sure that the pull request is complete before opening it.**
+
+ Whether it's fixing a bug or implementing new functionality, it's best that you make sure that the change you want to submit as a pull request is as complete as it can be before clicking the *Create pull request* button. Having to track if a pull request is ready for review or not places additional burden on reviewers.
+
+ Draft pull requests are an option, but use them sparingly and within reason. They are best suited to discuss code changes that cannot be easily described in natural language or have a potential large impact on the future direction of the project. When in doubt, don't open drafts unless a maintainer asks you to do so.
+
+* **Only push code when it's ready.**
+
+ As an extension of the above, when making changes to an already-open PR, please try to only push changes you are reasonably certain of. Pushing after every commit causes the continuous integration build queue to grow in size, slowing down work and taking up time that could be spent verifying other changes.
+
+* **Make sure to keep the *Allow edits from maintainers* check box checked.**
+
+ To speed up the merging process, collaborators and team members will sometimes want to push changes to your branch themselves, to make minor code style adjustments or to otherwise refactor the code without having to describe how they'd like the code to look like in painstaking detail. Having the *Allow edits from maintainers* check box checked lets them do that; without it they are forced to report issues back to you and wait for you to address them.
+
+* **Refrain from continually merging the master branch back to the PR.**
+
+ Unless there are merge conflicts that need resolution, there is no need to keep merging `master` back to a branch over and over again. One of the maintainers will merge `master` themselves before merging the PR itself anyway, and continual merge commits can cause CI to get overwhelmed due to queueing up too many builds.
+
+* **Refrain from force-pushing to the PR branch.**
+
+ Force-pushing should be avoided, as it can lead to accidentally overwriting a maintainer's changes or CI building wrong commits. We value all history in the project, so there is no need to squash or amend commits in most cases.
+
+ The cases in which force-pushing is warranted are very rare (such as accidentally leaking sensitive info in one of the files committed, adding unrelated files, or mis-merging a dependent PR).
+
+* **Be patient when waiting for the code to be reviewed and merged.**
+
+ As much as we'd like to review all contributions as fast as possible, our time is limited, as team members have to work on their own tasks in addition to reviewing code. As such, work needs to be prioritised, and it can unfortunately take weeks or months for your PR to be merged, depending on how important it is deemed to be.
+
+* **Don't mistake criticism of code for criticism of your person.**
+
+ As mentioned before, we are highly committed to quality when it comes to the lazer 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.
diff --git a/README.md b/README.md
index 59d72247f5..dc3ee63844 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,8 @@ Rhythm is just a *click* away. The future of [osu!](https://osu.ppy.sh) and the
This project is under heavy development, but is in a stable state. Users are encouraged to try it out and keep it installed alongside the stable *osu!* client. It will continue to evolve to the point of eventually replacing the existing stable client as an update.
+**IMPORTANT:** Gameplay mechanics (and other features which you may have come to know and love) are in a constant state of flux. Game balance and final quality-of-life passses come at the end of development, preceeded by experimentation and changes which may potentially **reduce playability or usability**. This is done in order to allow us to move forward as developers and designers more efficiently. If this offends you, please consider sticking to the stable releases of osu! (found on the website). We are not yet open to heated discussion over game mechanics and will not be using github as a forum for such discussions just yet.
+
We are accepting bug reports (please report with as much detail as possible and follow the existing issue templates). Feature requests are also welcome, but understand that our focus is on completing the game to feature parity before adding new features. A few resources are available as starting points to getting involved and understanding the project:
- Detailed release changelogs are available on the [official osu! site](https://osu.ppy.sh/home/changelog/lazer).
@@ -91,11 +93,7 @@ JetBrains ReSharper InspectCode is also used for wider rule sets. You can run it
## Contributing
-We welcome all contributions, but 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.
-
-If you're unsure of what you can help with, check out the [list of open issues](https://github.com/ppy/osu/issues) (especially those with the ["good first issue"](https://github.com/ppy/osu/issues?q=is%3Aopen+label%3Agood-first-issue+sort%3Aupdated-desc) label).
-
-Before starting, please make sure you are familiar with the [development and testing](https://github.com/ppy/osu-framework/wiki/Development-and-Testing) procedure we have set up. New component development, and where possible, bug fixing and debugging existing components **should always be done under VisualTests**.
+When it comes to contributing to the project, the two main things you can do to help out are reporting issues and submitting pull requests. Based on past experiences, we have prepared a [list of contributing guidelines](CONTRIBUTING.md) that should hopefully ease you into our collaboration process and answer the most frequently-asked questions.
Note that while we already have certain standards in place, nothing is set in stone. If you have an issue with the way code is structured, with any libraries we are using, or with any processes involved with contributing, *please* bring it up. We welcome all feedback so we can make contributing to this project as painless as possible.
diff --git a/osu.Android.props b/osu.Android.props
index eaad4daf35..07be3ab0d2 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -51,7 +51,7 @@
-
-
+
+
diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs
index bd91bcc933..285a813d97 100644
--- a/osu.Desktop/Program.cs
+++ b/osu.Desktop/Program.cs
@@ -33,13 +33,11 @@ namespace osu.Desktop
if (args.Length > 0 && args[0].Contains('.')) // easy way to check for a file import in args
{
var importer = new ArchiveImportIPCChannel(host);
- // Restore the cwd so relative paths given at the command line work correctly
- Directory.SetCurrentDirectory(cwd);
foreach (var file in args)
{
Console.WriteLine(@"Importing {0}", file);
- if (!importer.ImportAsync(Path.GetFullPath(file)).Wait(3000))
+ if (!importer.ImportAsync(Path.GetFullPath(file, cwd)).Wait(3000))
throw new TimeoutException(@"IPC took too long to send");
}
diff --git a/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs b/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs
index 394fd75488..1d207d04c7 100644
--- a/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs
+++ b/osu.Game.Benchmarks/BenchmarkBeatmapParsing.cs
@@ -8,7 +8,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO;
using osu.Game.IO.Archives;
-using osu.Game.Resources;
+using osu.Game.Tests.Resources;
namespace osu.Game.Benchmarks
{
@@ -18,8 +18,8 @@ namespace osu.Game.Benchmarks
public override void SetUp()
{
- using (var resources = new DllResourceStore(OsuResources.ResourceAssembly))
- using (var archive = resources.GetStream("Beatmaps/241526 Soleily - Renatus.osz"))
+ using (var resources = new DllResourceStore(typeof(TestResources).Assembly))
+ using (var archive = resources.GetStream("Resources/Archives/241526 Soleily - Renatus.osz"))
using (var reader = new ZipArchiveReader(archive))
reader.GetStream("Soleily - Renatus (Gamu) [Insane].osu").CopyTo(beatmapStream);
}
diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
index 88fe8f1150..41e726e05c 100644
--- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
+++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj
@@ -13,6 +13,7 @@
+
diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
index 90a6e609f0..0de2060e2d 100644
--- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs
@@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
switch (obj)
{
- case IHasCurve curveData:
+ case IHasPathWithRepeats curveData:
return new JuiceStream
{
StartTime = obj.StartTime,
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0
}.Yield();
- case IHasEndTime endTime:
+ case IHasDuration endTime:
return new BananaShower
{
StartTime = obj.StartTime,
diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
index c3488aec11..04a995c77e 100644
--- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
+++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs
@@ -1,12 +1,13 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Threading;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Objects
{
- public class BananaShower : CatchHitObject, IHasEndTime
+ public class BananaShower : CatchHitObject, IHasDuration
{
public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana;
@@ -14,13 +15,13 @@ namespace osu.Game.Rulesets.Catch.Objects
public override Judgement CreateJudgement() => new IgnoreJudgement();
- protected override void CreateNestedHitObjects()
+ protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
- base.CreateNestedHitObjects();
- createBananas();
+ base.CreateNestedHitObjects(cancellationToken);
+ createBananas(cancellationToken);
}
- private void createBananas()
+ private void createBananas(CancellationToken cancellationToken)
{
double spacing = Duration;
while (spacing > 100)
@@ -31,6 +32,8 @@ namespace osu.Game.Rulesets.Catch.Objects
for (double i = StartTime; i <= EndTime; i += spacing)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
AddNested(new Banana
{
Samples = Samples,
diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
index 01011645bd..2c96ee2b19 100644
--- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
+++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Linq;
+using System.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -13,7 +14,7 @@ using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Catch.Objects
{
- public class JuiceStream : CatchHitObject, IHasCurve
+ public class JuiceStream : CatchHitObject, IHasPathWithRepeats
{
///
/// Positional distance that results in a duration of one second, before any speed adjustments.
@@ -45,9 +46,9 @@ namespace osu.Game.Rulesets.Catch.Objects
TickDistance = scoringDistance / difficulty.SliderTickRate;
}
- protected override void CreateNestedHitObjects()
+ protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
- base.CreateNestedHitObjects();
+ base.CreateNestedHitObjects(cancellationToken);
var dropletSamples = Samples.Select(s => new HitSampleInfo
{
@@ -58,7 +59,7 @@ namespace osu.Game.Rulesets.Catch.Objects
SliderEventDescriptor? lastEvent = null;
- foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset))
+ foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken))
{
// generate tiny droplets since the last point
if (lastEvent != null)
@@ -73,6 +74,8 @@ namespace osu.Game.Rulesets.Catch.Objects
for (double t = timeBetweenTiny; t < sinceLastTick; t += timeBetweenTiny)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
AddNested(new TinyDroplet
{
StartTime = t + lastEvent.Value.Time,
@@ -112,15 +115,15 @@ namespace osu.Game.Rulesets.Catch.Objects
}
}
- public double EndTime
+ public float EndX => X + this.CurvePositionAt(1).X / CatchPlayfield.BASE_WIDTH;
+
+ public double Duration
{
- get => StartTime + this.SpanCount() * Path.Distance / Velocity;
+ get => this.SpanCount() * Path.Distance / Velocity;
set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
}
- public float EndX => X + this.CurvePositionAt(1).X / CatchPlayfield.BASE_WIDTH;
-
- public double Duration => EndTime - StartTime;
+ public double EndTime => StartTime + Duration;
private readonly SliderPath path = new SliderPath();
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs
index aac77c9c1c..0fe4a3c669 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs
@@ -7,20 +7,18 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
-using osu.Game.Rulesets.Mania.Edit;
+using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
-using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests
{
- [Cached(Type = typeof(IManiaHitObjectComposer))]
- public abstract class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene, IManiaHitObjectComposer
+ public abstract class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene
{
private readonly Column column;
@@ -43,12 +41,18 @@ namespace osu.Game.Rulesets.Mania.Tests
});
}
+ protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint)
+ {
+ var time = column.TimeAtScreenSpacePosition(InputManager.CurrentState.Mouse.Position);
+ var pos = column.ScreenSpacePositionAtTime(time);
+
+ return new SnapResult(pos, time, column);
+ }
+
protected override Container CreateHitObjectContainer() => new ScrollingTestContainer(ScrollingDirection.Down) { RelativeSizeAxes = Axes.Both };
protected override void AddHitObject(DrawableHitObject hitObject) => column.Add((DrawableManiaHitObject)hitObject);
- public Column ColumnAt(Vector2 screenSpacePosition) => column;
-
- public int TotalColumns => 1;
+ public ManiaPlayfield Playfield => null;
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs
index b598893e8c..149f6582ab 100644
--- a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs
+++ b/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs
@@ -4,25 +4,20 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Timing;
-using osu.Game.Rulesets.Mania.Edit;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Tests.Visual;
-using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Tests
{
- [Cached(Type = typeof(IManiaHitObjectComposer))]
- public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene, IManiaHitObjectComposer
+ public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene
{
[Cached(Type = typeof(IAdjustableClock))]
private readonly IAdjustableClock clock = new StopwatchClock();
- private readonly Column column;
-
protected ManiaSelectionBlueprintTestScene()
{
- Add(column = new Column(0)
+ Add(new Column(0)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -31,8 +26,6 @@ namespace osu.Game.Rulesets.Mania.Tests
});
}
- public Column ColumnAt(Vector2 screenSpacePosition) => column;
-
- public int TotalColumns => 1;
+ public ManiaPlayfield Playfield => null;
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-left.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-left.png
new file mode 100644
index 0000000000..03ca371c4e
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-left.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-right.png b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-right.png
new file mode 100644
index 0000000000..45b7be0255
Binary files /dev/null and b/osu.Game.Rulesets.Mania.Tests/Resources/special-skin/mania-stage-right.png differ
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs
new file mode 100644
index 0000000000..639be0bc11
--- /dev/null
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs
@@ -0,0 +1,124 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Input.Events;
+using osu.Framework.Timing;
+using osu.Game.Beatmaps.ControlPoints;
+using osu.Game.Configuration;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Edit;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Screens.Edit;
+using osu.Game.Tests.Visual;
+using osuTK;
+
+namespace osu.Game.Rulesets.Mania.Tests
+{
+ public class TestSceneManiaBeatSnapGrid : EditorClockTestScene
+ {
+ [Cached(typeof(IScrollingInfo))]
+ private ScrollingTestContainer.TestScrollingInfo scrollingInfo = new ScrollingTestContainer.TestScrollingInfo();
+
+ [Cached(typeof(EditorBeatmap))]
+ private EditorBeatmap editorBeatmap = new EditorBeatmap(new ManiaBeatmap(new StageDefinition()));
+
+ private readonly ManiaBeatSnapGrid beatSnapGrid;
+
+ public TestSceneManiaBeatSnapGrid()
+ {
+ editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 200 });
+ editorBeatmap.ControlPointInfo.Add(10000, new TimingControlPoint { BeatLength = 200 });
+
+ BeatDivisor.Value = 3;
+
+ // Some sane defaults
+ scrollingInfo.Algorithm.Algorithm = ScrollVisualisationMethod.Constant;
+ scrollingInfo.Direction.Value = ScrollingDirection.Up;
+ scrollingInfo.TimeRange.Value = 1000;
+
+ Children = new Drawable[]
+ {
+ Playfield = new ManiaPlayfield(new List
+ {
+ new StageDefinition { Columns = 4 },
+ new StageDefinition { Columns = 3 }
+ })
+ {
+ Clock = new FramedClock(new StopwatchClock())
+ },
+ new TestHitObjectComposer(Playfield)
+ {
+ Child = beatSnapGrid = new ManiaBeatSnapGrid()
+ }
+ };
+ }
+
+ protected override bool OnMouseMove(MouseMoveEvent e)
+ {
+ // We're providing a constant scroll algorithm.
+ float relativePosition = Playfield.Stages[0].HitObjectContainer.ToLocalSpace(e.ScreenSpaceMousePosition).Y / Playfield.Stages[0].HitObjectContainer.DrawHeight;
+ double timeValue = scrollingInfo.TimeRange.Value * relativePosition;
+
+ beatSnapGrid.SelectionTimeRange = (timeValue, timeValue);
+
+ return true;
+ }
+
+ public ManiaPlayfield Playfield { get; }
+ }
+
+ public class TestHitObjectComposer : HitObjectComposer
+ {
+ public override Playfield Playfield { get; }
+ public override IEnumerable HitObjects => Enumerable.Empty();
+ public override bool CursorInPlacementArea => false;
+
+ public TestHitObjectComposer(Playfield playfield)
+ {
+ Playfield = playfield;
+ }
+
+ public Drawable Child
+ {
+ set => InternalChild = value;
+ }
+
+ public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public override float GetBeatSnapDistanceAt(double referenceTime)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public override float DurationToDistance(double referenceTime, double duration)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public override double DistanceToDuration(double referenceTime, float distance)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public override double GetSnappedDurationFromDistance(double referenceTime, float distance)
+ {
+ throw new System.NotImplementedException();
+ }
+
+ public override float GetSnappedDistanceFromDistance(double referenceTime, float distance)
+ {
+ throw new System.NotImplementedException();
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs
index 6274bb1005..1a3fa29d4a 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs
@@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Mania.Tests
public void TestDragOffscreenSelectionVerticallyUpScroll()
{
DrawableHitObject lastObject = null;
+ double originalTime = 0;
Vector2 originalPosition = Vector2.Zero;
setScrollStep(ScrollingDirection.Up);
@@ -49,6 +50,7 @@ namespace osu.Game.Rulesets.Mania.Tests
AddStep("seek to last object", () =>
{
lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
+ originalTime = lastObject.HitObject.StartTime;
Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
});
@@ -64,19 +66,20 @@ namespace osu.Game.Rulesets.Mania.Tests
AddStep("move mouse downwards", () =>
{
- InputManager.MoveMouseTo(lastObject, new Vector2(0, 20));
+ InputManager.MoveMouseTo(lastObject, new Vector2(0, lastObject.ScreenSpaceDrawQuad.Height * 4));
InputManager.ReleaseButton(MouseButton.Left);
});
AddAssert("hitobjects not moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 0));
AddAssert("hitobjects moved downwards", () => lastObject.DrawPosition.Y - originalPosition.Y > 0);
- AddAssert("hitobjects not moved too far", () => lastObject.DrawPosition.Y - originalPosition.Y < 50);
+ AddAssert("hitobject has moved time", () => lastObject.HitObject.StartTime == originalTime + 125);
}
[Test]
public void TestDragOffscreenSelectionVerticallyDownScroll()
{
DrawableHitObject lastObject = null;
+ double originalTime = 0;
Vector2 originalPosition = Vector2.Zero;
setScrollStep(ScrollingDirection.Down);
@@ -84,6 +87,7 @@ namespace osu.Game.Rulesets.Mania.Tests
AddStep("seek to last object", () =>
{
lastObject = this.ChildrenOfType().Single(d => d.HitObject == composer.EditorBeatmap.HitObjects.Last());
+ originalTime = lastObject.HitObject.StartTime;
Clock.Seek(composer.EditorBeatmap.HitObjects.Last().StartTime);
});
@@ -99,13 +103,13 @@ namespace osu.Game.Rulesets.Mania.Tests
AddStep("move mouse upwards", () =>
{
- InputManager.MoveMouseTo(lastObject, new Vector2(0, -20));
+ InputManager.MoveMouseTo(lastObject, new Vector2(0, -lastObject.ScreenSpaceDrawQuad.Height * 4));
InputManager.ReleaseButton(MouseButton.Left);
});
AddAssert("hitobjects not moved columns", () => composer.EditorBeatmap.HitObjects.All(h => ((ManiaHitObject)h).Column == 0));
AddAssert("hitobjects moved upwards", () => originalPosition.Y - lastObject.DrawPosition.Y > 0);
- AddAssert("hitobjects not moved too far", () => originalPosition.Y - lastObject.DrawPosition.Y < 50);
+ AddAssert("hitobject has moved time", () => lastObject.HitObject.StartTime == originalTime + 125);
}
[Test]
@@ -207,7 +211,7 @@ namespace osu.Game.Rulesets.Mania.Tests
};
for (int i = 0; i < 10; i++)
- EditorBeatmap.Add(new Note { StartTime = 100 * i });
+ EditorBeatmap.Add(new Note { StartTime = 125 * i });
}
}
}
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
index ea6a1e2e6a..dd5fd93710 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs
@@ -156,7 +156,7 @@ namespace osu.Game.Rulesets.Mania.Tests
foreach (var obj in content.OfType())
{
- if (!(obj.HitObject is IHasEndTime endTime))
+ if (!(obj.HitObject is IHasDuration endTime))
continue;
foreach (var nested in obj.NestedHitObjects)
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
index 1c8116754f..32abf5e7f9 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs
@@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
}
else
{
- float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasEndTime) / beatmap.HitObjects.Count;
+ float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasDuration) / beatmap.HitObjects.Count;
if (percentSliderOrSpinner < 0.2)
TargetColumns = 7;
else if (percentSliderOrSpinner < 0.3 || roundedCircleSize >= 5)
@@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
break;
}
- case IHasEndTime endTimeData:
+ case IHasDuration endTimeData:
{
conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, originalBeatmap);
@@ -231,7 +231,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
var pattern = new Pattern();
- if (HitObject is IHasEndTime endTimeData)
+ if (HitObject is IHasDuration endTimeData)
{
pattern.Add(new HoldNote
{
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
index d8d5b67c0e..1bd796511b 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs
@@ -474,7 +474,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
///
private IList sampleInfoListAt(double time)
{
- if (!(HitObject is IHasCurve curveData))
+ if (!(HitObject is IHasPathWithRepeats curveData))
return HitObject.Samples;
double segmentTime = (EndTime - HitObject.StartTime) / spanCount;
diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
index 907bed0d65..d5286a3779 100644
--- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, new Pattern(), originalBeatmap)
{
- endTime = (HitObject as IHasEndTime)?.EndTime ?? 0;
+ endTime = (HitObject as IHasDuration)?.EndTime ?? 0;
}
public override IEnumerable Generate()
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
index c63e30e98a..b5ec1e1a2a 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs
@@ -2,10 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System;
+using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
+using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
using osuTK.Input;
@@ -17,6 +20,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
private readonly EditNotePiece headPiece;
private readonly EditNotePiece tailPiece;
+ [Resolved]
+ private IScrollingInfo scrollingInfo { get; set; }
+
public HoldNotePlacementBlueprint()
: base(new HoldNote())
{
@@ -36,8 +42,21 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
if (Column != null)
{
- headPiece.Y = PositionAt(HitObject.StartTime);
- tailPiece.Y = PositionAt(HitObject.EndTime);
+ headPiece.Y = Parent.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.StartTime)).Y;
+ tailPiece.Y = Parent.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.EndTime)).Y;
+
+ switch (scrollingInfo.Direction.Value)
+ {
+ case ScrollingDirection.Down:
+ headPiece.Y -= headPiece.DrawHeight / 2;
+ tailPiece.Y -= tailPiece.DrawHeight / 2;
+ break;
+
+ case ScrollingDirection.Up:
+ headPiece.Y += headPiece.DrawHeight / 2;
+ tailPiece.Y += tailPiece.DrawHeight / 2;
+ break;
+ }
}
var topPosition = new Vector2(headPiece.DrawPosition.X, Math.Min(headPiece.DrawPosition.Y, tailPiece.DrawPosition.Y));
@@ -59,23 +78,28 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
private double originalStartTime;
- public override void UpdatePosition(Vector2 screenSpacePosition)
+ public override void UpdatePosition(SnapResult result)
{
- base.UpdatePosition(screenSpacePosition);
+ base.UpdatePosition(result);
if (PlacementActive)
{
- var endTime = TimeAt(screenSpacePosition);
-
- HitObject.StartTime = endTime < originalStartTime ? endTime : originalStartTime;
- HitObject.Duration = Math.Abs(endTime - originalStartTime);
+ if (result.Time is double endTime)
+ {
+ HitObject.StartTime = endTime < originalStartTime ? endTime : originalStartTime;
+ HitObject.Duration = Math.Abs(endTime - originalStartTime);
+ }
}
else
{
- headPiece.Width = tailPiece.Width = SnappedWidth;
- headPiece.X = tailPiece.X = SnappedMousePosition.X;
+ if (result.Playfield != null)
+ {
+ headPiece.Width = tailPiece.Width = result.Playfield.DrawWidth;
+ headPiece.X = tailPiece.X = ToLocalSpace(result.ScreenSpacePosition).X;
+ }
- originalStartTime = HitObject.StartTime = TimeAt(screenSpacePosition);
+ if (result.Time is double startTime)
+ originalStartTime = HitObject.StartTime = startTime;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
index 43d43ef252..1737c4d2e5 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNoteSelectionBlueprint.cs
@@ -77,6 +77,6 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
public override Quad SelectionQuad => ScreenSpaceDrawQuad;
- public override Vector2 SelectionPoint => DrawableObject.Head.ScreenSpaceDrawQuad.Centre;
+ public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.Head.ScreenSpaceDrawQuad.Centre;
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
index 3fb03d642f..27a279e044 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs
@@ -1,43 +1,34 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Objects;
-using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Mania.UI;
-using osu.Game.Rulesets.UI.Scrolling;
-using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
- public abstract class ManiaPlacementBlueprint : PlacementBlueprint,
- IRequireHighFrequencyMousePosition // the playfield could be moving behind us
+ public abstract class ManiaPlacementBlueprint : PlacementBlueprint
where T : ManiaHitObject
{
protected new T HitObject => (T)base.HitObject;
- protected Column Column;
+ private Column column;
- ///
- /// The current mouse position, snapped to the closest column.
- ///
- protected Vector2 SnappedMousePosition { get; private set; }
+ public Column Column
+ {
+ get => column;
+ set
+ {
+ if (value == column)
+ return;
- ///
- /// The width of the closest column to the current mouse position.
- ///
- protected float SnappedWidth { get; private set; }
-
- [Resolved]
- private IManiaHitObjectComposer composer { get; set; }
-
- [Resolved]
- private IScrollingInfo scrollingInfo { get; set; }
+ column = value;
+ HitObject.Column = column.Index;
+ }
+ }
protected ManiaPlacementBlueprint(T hitObject)
: base(hitObject)
@@ -51,105 +42,18 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
return false;
if (Column == null)
- return base.OnMouseDown(e);
+ return false;
- HitObject.Column = Column.Index;
- BeginPlacement(TimeAt(e.ScreenSpaceMousePosition), true);
+ BeginPlacement(true);
return true;
}
- public override void UpdatePosition(Vector2 screenSpacePosition)
+ public override void UpdatePosition(SnapResult result)
{
+ base.UpdatePosition(result);
+
if (!PlacementActive)
- Column = ColumnAt(screenSpacePosition);
-
- if (Column == null) return;
-
- SnappedWidth = Column.DrawWidth;
-
- // Snap to the column
- var parentPos = Parent.ToLocalSpace(Column.ToScreenSpace(new Vector2(Column.DrawWidth / 2, 0)));
- SnappedMousePosition = new Vector2(parentPos.X, Parent.ToLocalSpace(screenSpacePosition).Y);
- }
-
- protected double TimeAt(Vector2 screenSpacePosition)
- {
- if (Column == null)
- return 0;
-
- var hitObjectContainer = Column.HitObjectContainer;
-
- // If we're scrolling downwards, a position of 0 is actually further away from the hit target
- // so we need to flip the vertical coordinate in the hitobject container's space
- var hitObjectPos = mouseToHitObjectPosition(Column.HitObjectContainer.ToLocalSpace(screenSpacePosition)).Y;
- if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
- hitObjectPos = hitObjectContainer.DrawHeight - hitObjectPos;
-
- return scrollingInfo.Algorithm.TimeAt(hitObjectPos,
- EditorClock.CurrentTime,
- scrollingInfo.TimeRange.Value,
- hitObjectContainer.DrawHeight);
- }
-
- protected float PositionAt(double time)
- {
- var pos = scrollingInfo.Algorithm.PositionAt(time,
- EditorClock.CurrentTime,
- scrollingInfo.TimeRange.Value,
- Column.HitObjectContainer.DrawHeight);
-
- if (scrollingInfo.Direction.Value == ScrollingDirection.Down)
- pos = Column.HitObjectContainer.DrawHeight - pos;
-
- return hitObjectToMousePosition(Column.HitObjectContainer.ToSpaceOfOtherDrawable(new Vector2(0, pos), Parent)).Y;
- }
-
- protected Column ColumnAt(Vector2 screenSpacePosition)
- => composer.ColumnAt(screenSpacePosition);
-
- ///
- /// Converts a mouse position to a hitobject position.
- ///
- ///
- /// Blueprints are centred on the mouse position, such that the hitobject position is anchored at the top or bottom of the blueprint depending on the scroll direction.
- ///
- /// The mouse position.
- /// The resulting hitobject position, acnhored at the top or bottom of the blueprint depending on the scroll direction.
- private Vector2 mouseToHitObjectPosition(Vector2 mousePosition)
- {
- switch (scrollingInfo.Direction.Value)
- {
- case ScrollingDirection.Up:
- mousePosition.Y -= DefaultNotePiece.NOTE_HEIGHT / 2;
- break;
-
- case ScrollingDirection.Down:
- mousePosition.Y += DefaultNotePiece.NOTE_HEIGHT / 2;
- break;
- }
-
- return mousePosition;
- }
-
- ///
- /// Converts a hitobject position to a mouse position.
- ///
- /// The hitobject position.
- /// The resulting mouse position, anchored at the centre of the hitobject.
- private Vector2 hitObjectToMousePosition(Vector2 hitObjectPosition)
- {
- switch (scrollingInfo.Direction.Value)
- {
- case ScrollingDirection.Up:
- hitObjectPosition.Y += DefaultNotePiece.NOTE_HEIGHT / 2;
- break;
-
- case ScrollingDirection.Down:
- hitObjectPosition.Y -= DefaultNotePiece.NOTE_HEIGHT / 2;
- break;
- }
-
- return hitObjectPosition;
+ Column = result.Playfield as Column;
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
index b8574b804e..384f49d9b2 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaSelectionBlueprint.cs
@@ -11,17 +11,14 @@ using osuTK;
namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
- public class ManiaSelectionBlueprint : OverlaySelectionBlueprint
+ public abstract class ManiaSelectionBlueprint : OverlaySelectionBlueprint
{
public new DrawableManiaHitObject DrawableObject => (DrawableManiaHitObject)base.DrawableObject;
[Resolved]
private IScrollingInfo scrollingInfo { get; set; }
- [Resolved]
- private IManiaHitObjectComposer composer { get; set; }
-
- public ManiaSelectionBlueprint(DrawableHitObject drawableObject)
+ protected ManiaSelectionBlueprint(DrawableHitObject drawableObject)
: base(drawableObject)
{
RelativeSizeAxes = Axes.None;
diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
index a4c0791253..684004b558 100644
--- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs
@@ -3,6 +3,7 @@
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
+using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints.Components;
using osu.Game.Rulesets.Mania.Objects;
using osuTK.Input;
@@ -11,22 +12,25 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
{
public class NotePlacementBlueprint : ManiaPlacementBlueprint
{
+ private readonly EditNotePiece piece;
+
public NotePlacementBlueprint()
: base(new Note())
{
- Origin = Anchor.Centre;
+ RelativeSizeAxes = Axes.Both;
- AutoSizeAxes = Axes.Y;
-
- InternalChild = new EditNotePiece { RelativeSizeAxes = Axes.X };
+ InternalChild = piece = new EditNotePiece { Origin = Anchor.Centre };
}
- protected override void Update()
+ public override void UpdatePosition(SnapResult result)
{
- base.Update();
+ base.UpdatePosition(result);
- Width = SnappedWidth;
- Position = SnappedMousePosition;
+ if (result.Playfield != null)
+ {
+ piece.Width = result.Playfield.DrawWidth;
+ piece.Position = ToLocalSpace(result.ScreenSpacePosition);
+ }
}
protected override bool OnMouseDown(MouseDownEvent e)
diff --git a/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs
deleted file mode 100644
index f64bab1fae..0000000000
--- a/osu.Game.Rulesets.Mania/Edit/IManiaHitObjectComposer.cs
+++ /dev/null
@@ -1,15 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using osu.Game.Rulesets.Mania.UI;
-using osuTK;
-
-namespace osu.Game.Rulesets.Mania.Edit
-{
- public interface IManiaHitObjectComposer
- {
- Column ColumnAt(Vector2 screenSpacePosition);
-
- int TotalColumns { get; }
- }
-}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs
new file mode 100644
index 0000000000..2028cae9a5
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs
@@ -0,0 +1,215 @@
+// Copyright (c) ppy Pty Ltd . 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.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Caching;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Beatmaps;
+using osu.Game.Graphics;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Mania.UI;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.UI.Scrolling;
+using osu.Game.Screens.Edit;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Mania.Edit
+{
+ ///
+ /// A grid which displays coloured beat divisor lines in proximity to the selection or placement cursor.
+ ///
+ public class ManiaBeatSnapGrid : Component
+ {
+ private const double visible_range = 750;
+
+ ///
+ /// The range of time values of the current selection.
+ ///
+ public (double start, double end)? SelectionTimeRange
+ {
+ set
+ {
+ if (value == selectionTimeRange)
+ return;
+
+ selectionTimeRange = value;
+ lineCache.Invalidate();
+ }
+ }
+
+ [Resolved]
+ private EditorBeatmap beatmap { get; set; }
+
+ [Resolved]
+ private IScrollingInfo scrollingInfo { get; set; }
+
+ [Resolved]
+ private Bindable working { get; set; }
+
+ [Resolved]
+ private OsuColour colours { get; set; }
+
+ [Resolved]
+ private BindableBeatDivisor beatDivisor { get; set; }
+
+ private readonly List grids = new List();
+
+ private readonly Cached lineCache = new Cached();
+
+ private (double start, double end)? selectionTimeRange;
+
+ [BackgroundDependencyLoader]
+ private void load(HitObjectComposer composer)
+ {
+ foreach (var stage in ((ManiaPlayfield)composer.Playfield).Stages)
+ {
+ foreach (var column in stage.Columns)
+ {
+ var lineContainer = new ScrollingHitObjectContainer();
+
+ grids.Add(lineContainer);
+ column.UnderlayElements.Add(lineContainer);
+ }
+ }
+
+ beatDivisor.BindValueChanged(_ => createLines(), true);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!lineCache.IsValid)
+ {
+ lineCache.Validate();
+ createLines();
+ }
+ }
+
+ private readonly Stack availableLines = new Stack();
+
+ private void createLines()
+ {
+ foreach (var grid in grids)
+ {
+ foreach (var line in grid.Objects.OfType())
+ availableLines.Push(line);
+
+ grid.Clear(false);
+ }
+
+ if (selectionTimeRange == null)
+ return;
+
+ var range = selectionTimeRange.Value;
+
+ var timingPoint = beatmap.ControlPointInfo.TimingPointAt(range.start - visible_range);
+
+ double time = timingPoint.Time;
+ int beat = 0;
+
+ // progress time until in the visible range.
+ while (time < range.start - visible_range)
+ {
+ time += timingPoint.BeatLength / beatDivisor.Value;
+ beat++;
+ }
+
+ while (time < range.end + visible_range)
+ {
+ var nextTimingPoint = beatmap.ControlPointInfo.TimingPointAt(time);
+
+ // switch to the next timing point if we have reached it.
+ if (nextTimingPoint.Time > timingPoint.Time)
+ {
+ beat = 0;
+ time = nextTimingPoint.Time;
+ timingPoint = nextTimingPoint;
+ }
+
+ Color4 colour = BindableBeatDivisor.GetColourFor(
+ BindableBeatDivisor.GetDivisorForBeatIndex(Math.Max(1, beat), beatDivisor.Value), colours);
+
+ foreach (var grid in grids)
+ {
+ if (!availableLines.TryPop(out var line))
+ line = new DrawableGridLine();
+
+ line.HitObject.StartTime = time;
+ line.Colour = colour;
+
+ grid.Add(line);
+ }
+
+ beat++;
+ time += timingPoint.BeatLength / beatDivisor.Value;
+ }
+
+ foreach (var grid in grids)
+ {
+ // required to update ScrollingHitObjectContainer's cache.
+ grid.UpdateSubTree();
+
+ foreach (var line in grid.Objects.OfType())
+ {
+ time = line.HitObject.StartTime;
+
+ if (time >= range.start && time <= range.end)
+ line.Alpha = 1;
+ else
+ {
+ double timeSeparation = time < range.start ? range.start - time : time - range.end;
+ line.Alpha = (float)Math.Max(0, 1 - timeSeparation / visible_range);
+ }
+ }
+ }
+ }
+
+ private class DrawableGridLine : DrawableHitObject
+ {
+ [Resolved]
+ private IScrollingInfo scrollingInfo { get; set; }
+
+ private readonly IBindable direction = new Bindable();
+
+ public DrawableGridLine()
+ : base(new HitObject())
+ {
+ RelativeSizeAxes = Axes.X;
+ Height = 2;
+
+ AddInternal(new Box { RelativeSizeAxes = Axes.Both });
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ direction.BindTo(scrollingInfo.Direction);
+ direction.BindValueChanged(onDirectionChanged, true);
+ }
+
+ private void onDirectionChanged(ValueChangedEvent direction)
+ {
+ Origin = Anchor = direction.NewValue == ScrollingDirection.Up
+ ? Anchor.TopLeft
+ : Anchor.BottomLeft;
+ }
+
+ protected override void UpdateInitialTransforms()
+ {
+ // don't perform any fading – we are handling that ourselves.
+ }
+
+ protected override void UpdateStateTransforms(ArmedState state)
+ {
+ LifetimeEnd = HitObject.StartTime + visible_range;
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
index dfa933baad..7e2469a794 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs
@@ -6,9 +6,14 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mania.Objects;
using System.Collections.Generic;
+using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Input;
+using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit.Compose.Components;
@@ -16,56 +21,64 @@ using osuTK;
namespace osu.Game.Rulesets.Mania.Edit
{
- [Cached(Type = typeof(IManiaHitObjectComposer))]
- public class ManiaHitObjectComposer : HitObjectComposer, IManiaHitObjectComposer
+ public class ManiaHitObjectComposer : HitObjectComposer
{
private DrawableManiaEditRuleset drawableRuleset;
+ private ManiaBeatSnapGrid beatSnapGrid;
+ private InputManager inputManager;
public ManiaHitObjectComposer(Ruleset ruleset)
: base(ruleset)
{
}
- ///
- /// Retrieves the column that intersects a screen-space position.
- ///
- /// The screen-space position.
- /// The column which intersects with .
- public Column ColumnAt(Vector2 screenSpacePosition) => drawableRuleset.GetColumnByPosition(screenSpacePosition);
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ AddInternal(beatSnapGrid = new ManiaBeatSnapGrid());
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ inputManager = GetContainingInputManager();
+ }
private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
- public ManiaPlayfield Playfield => ((ManiaPlayfield)drawableRuleset.Playfield);
+ public new ManiaPlayfield Playfield => ((ManiaPlayfield)drawableRuleset.Playfield);
public IScrollingInfo ScrollingInfo => drawableRuleset.ScrollingInfo;
- public int TotalColumns => Playfield.TotalColumns;
+ protected override Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) =>
+ Playfield.GetColumnByPosition(screenSpacePosition);
- public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time)
+ public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
{
- var hoc = Playfield.GetColumn(0).HitObjectContainer;
+ var result = base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
- float targetPosition = hoc.ToLocalSpace(ToScreenSpace(position)).Y;
-
- if (drawableRuleset.ScrollingInfo.Direction.Value == ScrollingDirection.Down)
+ switch (ScrollingInfo.Direction.Value)
{
- // We're dealing with screen coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time.
- // The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position,
- // so when scrolling downwards the coordinates need to be flipped.
- targetPosition = hoc.DrawHeight - targetPosition;
+ case ScrollingDirection.Down:
+ result.ScreenSpacePosition -= new Vector2(0, getNoteHeight() / 2);
+ break;
+
+ case ScrollingDirection.Up:
+ result.ScreenSpacePosition += new Vector2(0, getNoteHeight() / 2);
+ break;
}
- double targetTime = drawableRuleset.ScrollingInfo.Algorithm.TimeAt(targetPosition,
- EditorClock.CurrentTime,
- drawableRuleset.ScrollingInfo.TimeRange.Value,
- hoc.DrawHeight);
-
- return base.GetSnappedPosition(position, targetTime);
+ return result;
}
+ private float getNoteHeight() =>
+ Playfield.GetColumn(0).ToScreenSpace(new Vector2(DefaultNotePiece.NOTE_HEIGHT)).Y -
+ Playfield.GetColumn(0).ToScreenSpace(Vector2.Zero).Y;
+
protected override DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
{
drawableRuleset = new DrawableManiaEditRuleset(ruleset, beatmap, mods);
@@ -76,12 +89,36 @@ namespace osu.Game.Rulesets.Mania.Edit
return drawableRuleset;
}
- protected override ComposeBlueprintContainer CreateBlueprintContainer() => new ManiaBlueprintContainer(drawableRuleset.Playfield.AllHitObjects);
+ protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects)
+ => new ManiaBlueprintContainer(hitObjects);
protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[]
{
new NoteCompositionTool(),
new HoldNoteCompositionTool()
};
+
+ protected override void UpdateAfterChildren()
+ {
+ base.UpdateAfterChildren();
+
+ if (BlueprintContainer.CurrentTool is SelectTool)
+ {
+ if (EditorBeatmap.SelectedHitObjects.Any())
+ {
+ beatSnapGrid.SelectionTimeRange = (EditorBeatmap.SelectedHitObjects.Min(h => h.StartTime), EditorBeatmap.SelectedHitObjects.Max(h => h.GetEndTime()));
+ }
+ else
+ beatSnapGrid.SelectionTimeRange = null;
+ }
+ else
+ {
+ var result = SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position);
+ if (result.Time is double time)
+ beatSnapGrid.SelectionTimeRange = (time, time);
+ else
+ beatSnapGrid.SelectionTimeRange = null;
+ }
+ }
}
}
diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
index 55245198c8..65f40d7d0a 100644
--- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
+++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs
@@ -4,6 +4,7 @@
using System;
using System.Linq;
using osu.Framework.Allocation;
+using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.UI.Scrolling;
@@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Edit
private IScrollingInfo scrollingInfo { get; set; }
[Resolved]
- private IManiaHitObjectComposer composer { get; set; }
+ private HitObjectComposer composer { get; set; }
public override bool HandleMovement(MoveSelectionEvent moveEvent)
{
@@ -31,7 +32,9 @@ namespace osu.Game.Rulesets.Mania.Edit
private void performColumnMovement(int lastColumn, MoveSelectionEvent moveEvent)
{
- var currentColumn = composer.ColumnAt(moveEvent.ScreenSpacePosition);
+ var maniaPlayfield = ((ManiaHitObjectComposer)composer).Playfield;
+
+ var currentColumn = maniaPlayfield.GetColumnByPosition(moveEvent.ScreenSpacePosition);
if (currentColumn == null)
return;
@@ -50,7 +53,7 @@ namespace osu.Game.Rulesets.Mania.Edit
maxColumn = obj.Column;
}
- columnDelta = Math.Clamp(columnDelta, -minColumn, composer.TotalColumns - 1 - maxColumn);
+ columnDelta = Math.Clamp(columnDelta, -minColumn, maniaPlayfield.TotalColumns - 1 - maxColumn);
foreach (var obj in SelectedHitObjects.OfType())
obj.Column += columnDelta;
diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
index eea2c31260..a100c9a58e 100644
--- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
+++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
+using System.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
@@ -14,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Objects
///
/// Represents a hit object which requires pressing, holding, and releasing a key.
///
- public class HoldNote : ManiaHitObject, IHasEndTime
+ public class HoldNote : ManiaHitObject, IHasDuration
{
public double EndTime
{
@@ -91,11 +92,11 @@ namespace osu.Game.Rulesets.Mania.Objects
tickSpacing = timingPoint.BeatLength / difficulty.SliderTickRate;
}
- protected override void CreateNestedHitObjects()
+ protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
- base.CreateNestedHitObjects();
+ base.CreateNestedHitObjects(cancellationToken);
- createTicks();
+ createTicks(cancellationToken);
AddNested(Head = new Note
{
@@ -112,13 +113,15 @@ namespace osu.Game.Rulesets.Mania.Objects
});
}
- private void createTicks()
+ private void createTicks(CancellationToken cancellationToken)
{
if (tickSpacing == 0)
return;
for (double t = StartTime + tickSpacing; t <= EndTime - tickSpacing; t += tickSpacing)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
AddNested(new HoldNoteTick
{
StartTime = t,
diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs
index 7680526ac4..f177284399 100644
--- a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs
@@ -52,10 +52,10 @@ namespace osu.Game.Rulesets.Mania.Skinning
base.Update();
if (leftSprite?.Height > 0)
- leftSprite.Scale = new Vector2(DrawHeight / leftSprite.Height);
+ leftSprite.Scale = new Vector2(1, DrawHeight / leftSprite.Height);
if (rightSprite?.Height > 0)
- rightSprite.Scale = new Vector2(DrawHeight / rightSprite.Height);
+ rightSprite.Scale = new Vector2(1, DrawHeight / rightSprite.Height);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs
index 506a07f26b..511d6c8623 100644
--- a/osu.Game.Rulesets.Mania/UI/Column.cs
+++ b/osu.Game.Rulesets.Mania/UI/Column.cs
@@ -37,6 +37,8 @@ namespace osu.Game.Rulesets.Mania.UI
internal readonly Container TopLevelContainer;
+ public Container UnderlayElements => hitObjectArea.UnderlayElements;
+
public Column(int index)
{
Index = index;
diff --git a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs
index cb79bf7f43..b365ae45a9 100644
--- a/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs
+++ b/osu.Game.Rulesets.Mania/UI/Components/ColumnHitObjectArea.cs
@@ -12,6 +12,9 @@ namespace osu.Game.Rulesets.Mania.UI.Components
public class ColumnHitObjectArea : HitObjectArea
{
public readonly Container Explosions;
+
+ public readonly Container UnderlayElements;
+
private readonly Drawable hitTarget;
public ColumnHitObjectArea(int columnIndex, HitObjectContainer hitObjectContainer)
@@ -19,6 +22,11 @@ namespace osu.Game.Rulesets.Mania.UI.Components
{
AddRangeInternal(new[]
{
+ UnderlayElements = new Container
+ {
+ RelativeSizeAxes = Axes.Both,
+ Depth = 2,
+ },
hitTarget = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitTarget, columnIndex), _ => new DefaultHitTarget())
{
RelativeSizeAxes = Axes.X,
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
index f3f843f366..94b5ee9486 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
@@ -23,7 +23,6 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
-using osuTK;
namespace osu.Game.Rulesets.Mania.UI
{
@@ -108,13 +107,6 @@ namespace osu.Game.Rulesets.Mania.UI
private void updateTimeRange() => TimeRange.Value = configTimeRange.Value * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value;
- ///
- /// Retrieves the column that intersects a screen-space position.
- ///
- /// The screen-space position.
- /// The column which intersects with .
- public Column GetColumnByPosition(Vector2 screenSpacePosition) => Playfield.GetColumnByPosition(screenSpacePosition);
-
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new ManiaPlayfieldAdjustmentContainer();
protected override Playfield CreatePlayfield() => new ManiaPlayfield(Beatmap.Stages);
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
index 1af7d06998..271e432e8d 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
@@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Mania.UI
[Cached]
public class ManiaPlayfield : ScrollingPlayfield
{
+ public IReadOnlyList Stages => stages;
+
private readonly List stages = new List();
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => stages.Any(s => s.ReceivePositionalInputAt(screenSpacePos));
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs
index c182aa5d63..0d0be2953b 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests
[Cached]
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
- [Cached(typeof(IDistanceSnapProvider))]
+ [Cached(typeof(IPositionSnapProvider))]
private readonly SnapProvider snapProvider = new SnapProvider();
private TestOsuDistanceSnapGrid grid;
@@ -172,9 +172,9 @@ namespace osu.Game.Rulesets.Osu.Tests
}
}
- private class SnapProvider : IDistanceSnapProvider
+ private class SnapProvider : IPositionSnapProvider
{
- public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) => (position, time);
+ public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
public float GetBeatSnapDistanceAt(double referenceTime) => (float)beat_length;
diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
index 147d74c929..fcad356a1c 100644
--- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
switch (original)
{
- case IHasCurve curveData:
+ case IHasPathWithRepeats curveData:
return new Slider
{
StartTime = original.StartTime,
@@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / beatmap.ControlPointInfo.DifficultyPointAt(original.StartTime).SpeedMultiplier : 1
}.Yield();
- case IHasEndTime endTimeData:
+ case IHasDuration endTimeData:
return new Spinner
{
StartTime = original.StartTime,
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
index dad199715e..3dbbdcc5d0 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs
@@ -5,7 +5,6 @@ using osu.Framework.Input.Events;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components;
using osu.Game.Rulesets.Osu.Objects;
-using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
@@ -40,6 +39,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
return base.OnMouseDown(e);
}
- public override void UpdatePosition(Vector2 screenSpacePosition) => HitObject.Position = ToLocalSpace(screenSpacePosition);
+ public override void UpdatePosition(SnapResult result)
+ {
+ base.UpdatePosition(result);
+ HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
+ }
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs
index b0e13808a5..8dd550bb96 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/OsuSelectionBlueprint.cs
@@ -12,6 +12,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints
{
protected new T HitObject => (T)DrawableObject.HitObject;
+ protected override bool AlwaysShowWhenSelected => true;
+
protected OsuSelectionBlueprint(DrawableHitObject drawableObject)
: base(drawableObject)
{
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
index d0c1eb5317..c06904c0c2 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private IEditorChangeHandler changeHandler { get; set; }
[Resolved(CanBeNull = true)]
- private IDistanceSnapProvider snapProvider { get; set; }
+ private IPositionSnapProvider snapProvider { get; set; }
[Resolved]
private OsuColour colours { get; set; }
@@ -162,11 +162,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (ControlPoint == slider.Path.ControlPoints[0])
{
// Special handling for the head control point - the position of the slider changes which means the snapped position and time have to be taken into account
- (Vector2 snappedPosition, double snappedTime) = snapProvider?.GetSnappedPosition(e.MousePosition, slider.StartTime) ?? (e.MousePosition, slider.StartTime);
- Vector2 movementDelta = snappedPosition - slider.Position;
+ var result = snapProvider?.SnapScreenSpacePositionToValidTime(e.ScreenSpaceMousePosition);
+
+ Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? e.ScreenSpaceMousePosition) - slider.Position;
slider.Position += movementDelta;
- slider.StartTime = snappedTime;
+ slider.StartTime = result?.Time ?? slider.StartTime;
// Since control points are relative to the position of the slider, they all need to be offset backwards by the delta
for (int i = 1; i < slider.Path.ControlPoints.Count; i++)
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
index ac30f5a762..4b99cc23ed 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs
@@ -67,13 +67,15 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
inputManager = GetContainingInputManager();
}
- public override void UpdatePosition(Vector2 screenSpacePosition)
+ public override void UpdatePosition(SnapResult result)
{
+ base.UpdatePosition(result);
+
switch (state)
{
case PlacementState.Initial:
BeginPlacement();
- HitObject.Position = ToLocalSpace(screenSpacePosition);
+ HitObject.Position = ToLocalSpace(result.ScreenSpacePosition);
break;
case PlacementState.Body:
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index b7074b7ee5..6633136673 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
new OsuMenuItem("Add control point", MenuItemType.Standard, () => addControlPoint(rightClickPosition)),
};
- public override Vector2 SelectionPoint => ((DrawableSlider)DrawableObject).HeadCircle.ScreenSpaceDrawQuad.Centre;
+ public override Vector2 ScreenSpaceSelectionPoint => ((DrawableSlider)DrawableObject).HeadCircle.ScreenSpaceDrawQuad.Centre;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => BodyPiece.ReceivePositionalInputAt(screenSpacePos);
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs
index 74b563d922..cc4ed0eccf 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/SpinnerPlacementBlueprint.cs
@@ -8,7 +8,6 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
-using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
@@ -60,9 +59,5 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners
return true;
}
-
- public override void UpdatePosition(Vector2 screenSpacePosition)
- {
- }
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
index cdf78a5902..37019a7a05 100644
--- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
+++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs
@@ -4,14 +4,20 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using osu.Framework.Allocation;
+using osu.Framework.Caching;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Compose.Components;
+using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
@@ -32,9 +38,81 @@ namespace osu.Game.Rulesets.Osu.Edit
new SpinnerCompositionTool()
};
- protected override ComposeBlueprintContainer CreateBlueprintContainer() => new OsuBlueprintContainer(HitObjects);
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ LayerBelowRuleset.Add(distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both });
- protected override DistanceSnapGrid CreateDistanceSnapGrid(IEnumerable selectedHitObjects)
+ EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid();
+ EditorBeatmap.PlacementObject.ValueChanged += _ => updateDistanceSnapGrid();
+ }
+
+ protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects)
+ => new OsuBlueprintContainer(hitObjects);
+
+ private DistanceSnapGrid distanceSnapGrid;
+ private Container distanceSnapGridContainer;
+
+ private readonly Cached distanceSnapGridCache = new Cached();
+ private double? lastDistanceSnapGridTime;
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!(BlueprintContainer.CurrentTool is SelectTool))
+ {
+ if (EditorClock.CurrentTime != lastDistanceSnapGridTime)
+ {
+ distanceSnapGridCache.Invalidate();
+ lastDistanceSnapGridTime = EditorClock.CurrentTime;
+ }
+
+ if (!distanceSnapGridCache.IsValid)
+ updateDistanceSnapGrid();
+ }
+ }
+
+ public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
+ {
+ if (distanceSnapGrid == null)
+ return base.SnapScreenSpacePositionToValidTime(screenSpacePosition);
+
+ (Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
+
+ return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition));
+ }
+
+ private void updateDistanceSnapGrid()
+ {
+ distanceSnapGridContainer.Clear();
+ distanceSnapGridCache.Invalidate();
+
+ switch (BlueprintContainer.CurrentTool)
+ {
+ case SelectTool _:
+ if (!EditorBeatmap.SelectedHitObjects.Any())
+ return;
+
+ distanceSnapGrid = createDistanceSnapGrid(EditorBeatmap.SelectedHitObjects);
+ break;
+
+ default:
+ if (!CursorInPlacementArea)
+ return;
+
+ distanceSnapGrid = createDistanceSnapGrid(Enumerable.Empty());
+ break;
+ }
+
+ if (distanceSnapGrid != null)
+ {
+ distanceSnapGridContainer.Add(distanceSnapGrid);
+ distanceSnapGridCache.Validate();
+ }
+ }
+
+ private DistanceSnapGrid createDistanceSnapGrid(IEnumerable selectedHitObjects)
{
if (BlueprintContainer.CurrentTool is SpinnerCompositionTool)
return null;
@@ -42,7 +120,8 @@ namespace osu.Game.Rulesets.Osu.Edit
var objects = selectedHitObjects.ToList();
if (objects.Count == 0)
- return createGrid(h => h.StartTime <= EditorClock.CurrentTime);
+ // use accurate time value to give more instantaneous feedback to the user.
+ return createGrid(h => h.StartTime <= EditorClock.CurrentTimeAccurate);
double minTime = objects.Min(h => h.StartTime);
return createGrid(h => h.StartTime < minTime, objects.Count + 1);
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
index 7b1941b7f9..5d191119b9 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModRelax.cs
@@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Mods
break;
// already hit or beyond the hittable end time.
- if (h.IsHit || (h.HitObject is IHasEndTime hasEnd && time > hasEnd.EndTime))
+ if (h.IsHit || (h.HitObject is IHasDuration hasEnd && time > hasEnd.EndTime))
continue;
switch (h)
diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
index 297a0fea79..3cad52faeb 100644
--- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
+++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs
@@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Mods
}
// Keep wiggling sliders and spinners for their duration
- if (!(osuObject is IHasEndTime endTime))
+ if (!(osuObject is IHasDuration endTime))
return;
amountWiggles = (int)(endTime.Duration / wiggle_duration);
diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs
index e5d6c20738..705e88040f 100644
--- a/osu.Game.Rulesets.Osu/Objects/Slider.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs
@@ -6,6 +6,7 @@ using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic;
using osu.Game.Rulesets.Objects;
using System.Linq;
+using System.Threading;
using osu.Framework.Caching;
using osu.Game.Audio;
using osu.Game.Beatmaps;
@@ -16,16 +17,16 @@ using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
{
- public class Slider : OsuHitObject, IHasCurve
+ public class Slider : OsuHitObject, IHasPathWithRepeats
{
- public double EndTime
+ public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity;
+
+ public double Duration
{
- get => StartTime + this.SpanCount() * Path.Distance / Velocity;
+ get => EndTime - StartTime;
set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
}
- public double Duration => EndTime - StartTime;
-
private readonly Cached endPositionCache = new Cached();
public override Vector2 EndPosition => endPositionCache.IsValid ? endPositionCache.Value : endPositionCache.Value = Position + this.CurvePositionAt(1);
@@ -133,12 +134,12 @@ namespace osu.Game.Rulesets.Osu.Objects
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
}
- protected override void CreateNestedHitObjects()
+ protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{
- base.CreateNestedHitObjects();
+ base.CreateNestedHitObjects(cancellationToken);
foreach (var e in
- SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset))
+ SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken))
{
switch (e.Type)
{
diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
index 0b8d03d118..418375c090 100644
--- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs
@@ -11,7 +11,7 @@ using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
{
- public class Spinner : OsuHitObject, IHasEndTime
+ public class Spinner : OsuHitObject, IHasDuration
{
public double EndTime
{
diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
index 1de7d488f3..79a6ea7e92 100644
--- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
+++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs
@@ -8,7 +8,7 @@ using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring
{
- internal class OsuScoreProcessor : ScoreProcessor
+ public class OsuScoreProcessor : ScoreProcessor
{
protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new OsuJudgementResult(hitObject, judgement);
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
index 37df5ec540..9bcb3abc63 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
@@ -237,6 +237,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
{
Position = new Vector2(part.Position.X - size.X / 2, part.Position.Y + size.Y / 2),
TexturePosition = textureRect.BottomLeft,
+ TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.BottomLeft.Linear,
Time = part.Time
});
@@ -245,6 +246,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
{
Position = new Vector2(part.Position.X + size.X / 2, part.Position.Y + size.Y / 2),
TexturePosition = textureRect.BottomRight,
+ TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.BottomRight.Linear,
Time = part.Time
});
@@ -253,6 +255,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
{
Position = new Vector2(part.Position.X + size.X / 2, part.Position.Y - size.Y / 2),
TexturePosition = textureRect.TopRight,
+ TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.TopRight.Linear,
Time = part.Time
});
@@ -261,6 +264,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
{
Position = new Vector2(part.Position.X - size.X / 2, part.Position.Y - size.Y / 2),
TexturePosition = textureRect.TopLeft,
+ TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.TopLeft.Linear,
Time = part.Time
});
@@ -290,6 +294,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
[VertexMember(2, VertexAttribPointerType.Float)]
public Vector2 TexturePosition;
+ [VertexMember(4, VertexAttribPointerType.Float)]
+ public Vector4 TextureRect;
+
[VertexMember(1, VertexAttribPointerType.Float)]
public float Time;
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.cs
new file mode 100644
index 0000000000..089a7ad00b
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.cs
@@ -0,0 +1,17 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Taiko.Tests
+{
+ [TestFixture]
+ public class TestSceneEditor : EditorTestScene
+ {
+ public TestSceneEditor()
+ : base(new TaikoRuleset())
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs
new file mode 100644
index 0000000000..34d5fdf857
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs
@@ -0,0 +1,55 @@
+// Copyright (c) ppy Pty Ltd . 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.Containers;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Taiko.Beatmaps;
+using osu.Game.Rulesets.Taiko.Edit;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Screens.Edit;
+using osu.Game.Tests.Visual;
+
+namespace osu.Game.Rulesets.Taiko.Tests
+{
+ public class TestSceneTaikoHitObjectComposer : EditorClockTestScene
+ {
+ [SetUp]
+ public void Setup() => Schedule(() =>
+ {
+ BeatDivisor.Value = 8;
+ Clock.Seek(0);
+
+ Child = new TestComposer { RelativeSizeAxes = Axes.Both };
+ });
+
+ [Test]
+ public void BasicTest()
+ {
+ }
+
+ private class TestComposer : CompositeDrawable
+ {
+ [Cached(typeof(EditorBeatmap))]
+ [Cached(typeof(IBeatSnapProvider))]
+ public readonly EditorBeatmap EditorBeatmap;
+
+ public TestComposer()
+ {
+ InternalChildren = new Drawable[]
+ {
+ EditorBeatmap = new EditorBeatmap(new TaikoBeatmap())
+ {
+ BeatmapInfo = { Ruleset = new TaikoRuleset().RulesetInfo }
+ },
+ new TaikoHitObjectComposer(new TaikoRuleset())
+ };
+
+ for (int i = 0; i < 10; i++)
+ EditorBeatmap.Add(new Hit { StartTime = 125 * i });
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
index d324441285..78550ed270 100644
--- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
+++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs
@@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
if (!isForCurrentRuleset && tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength)
{
- List> allSamples = obj is IHasCurve curveData ? curveData.NodeSamples : new List>(new[] { samples });
+ List> allSamples = obj is IHasPathWithRepeats curveData ? curveData.NodeSamples : new List>(new[] { samples });
int i = 0;
@@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
break;
}
- case IHasEndTime endTimeData:
+ case IHasDuration endTimeData:
{
double hitMultiplier = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 3, 5, 7.5) * swell_hit_multiplier;
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/DrumRollPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/DrumRollPlacementBlueprint.cs
new file mode 100644
index 0000000000..eb07ce7635
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/DrumRollPlacementBlueprint.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Taiko.Objects;
+
+namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
+{
+ public class DrumRollPlacementBlueprint : TaikoSpanPlacementBlueprint
+ {
+ public DrumRollPlacementBlueprint()
+ : base(new DrumRoll())
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPiece.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPiece.cs
new file mode 100644
index 0000000000..b02e3aa9ba
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPiece.cs
@@ -0,0 +1,35 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
+{
+ public class HitPiece : CompositeDrawable
+ {
+ public HitPiece()
+ {
+ Origin = Anchor.Centre;
+
+ InternalChild = new CircularContainer
+ {
+ Masking = true,
+ BorderThickness = 10,
+ BorderColour = Color4.Yellow,
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ AlwaysPresent = true,
+ Alpha = 0,
+ RelativeSizeAxes = Axes.Both
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
new file mode 100644
index 0000000000..c5191ab241
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs
@@ -0,0 +1,52 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Input.Events;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.UI;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
+{
+ public class HitPlacementBlueprint : PlacementBlueprint
+ {
+ private readonly HitPiece piece;
+
+ private static Hit hit;
+
+ public HitPlacementBlueprint()
+ : base(hit = new Hit())
+ {
+ InternalChild = piece = new HitPiece
+ {
+ Size = new Vector2(TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.DEFAULT_HEIGHT)
+ };
+ }
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ switch (e.Button)
+ {
+ case MouseButton.Left:
+ hit.Type = HitType.Centre;
+ EndPlacement(true);
+ return true;
+
+ case MouseButton.Right:
+ hit.Type = HitType.Rim;
+ EndPlacement(true);
+ return true;
+ }
+
+ return false;
+ }
+
+ public override void UpdatePosition(SnapResult result)
+ {
+ piece.Position = ToLocalSpace(result.ScreenSpacePosition);
+ base.UpdatePosition(result);
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/LengthPiece.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/LengthPiece.cs
new file mode 100644
index 0000000000..6b651fd739
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/LengthPiece.cs
@@ -0,0 +1,40 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osuTK.Graphics;
+
+namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
+{
+ public class LengthPiece : CompositeDrawable
+ {
+ public LengthPiece()
+ {
+ Origin = Anchor.CentreLeft;
+
+ InternalChild = new Container
+ {
+ Masking = true,
+ Colour = Color4.Yellow,
+ RelativeSizeAxes = Axes.Both,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 8,
+ },
+ new Box
+ {
+ Origin = Anchor.BottomLeft,
+ Anchor = Anchor.BottomLeft,
+ RelativeSizeAxes = Axes.X,
+ Height = 8,
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/SwellPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/SwellPlacementBlueprint.cs
new file mode 100644
index 0000000000..95fa82a0f2
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/SwellPlacementBlueprint.cs
@@ -0,0 +1,15 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Taiko.Objects;
+
+namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
+{
+ public class SwellPlacementBlueprint : TaikoSpanPlacementBlueprint
+ {
+ public SwellPlacementBlueprint()
+ : base(new Swell())
+ {
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSelectionBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSelectionBlueprint.cs
new file mode 100644
index 0000000000..62f69122cc
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSelectionBlueprint.cs
@@ -0,0 +1,40 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Graphics;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects.Drawables;
+using osuTK;
+
+namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
+{
+ public class TaikoSelectionBlueprint : OverlaySelectionBlueprint
+ {
+ public TaikoSelectionBlueprint(DrawableHitObject hitObject)
+ : base(hitObject)
+ {
+ RelativeSizeAxes = Axes.None;
+
+ AddInternal(new HitPiece
+ {
+ RelativeSizeAxes = Axes.Both,
+ Origin = Anchor.TopLeft
+ });
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ // Move the rectangle to cover the hitobjects
+ var topLeft = new Vector2(float.MaxValue, float.MaxValue);
+ var bottomRight = new Vector2(float.MinValue, float.MinValue);
+
+ topLeft = Vector2.ComponentMin(topLeft, Parent.ToLocalSpace(DrawableObject.ScreenSpaceDrawQuad.TopLeft));
+ bottomRight = Vector2.ComponentMax(bottomRight, Parent.ToLocalSpace(DrawableObject.ScreenSpaceDrawQuad.BottomRight));
+
+ Size = bottomRight - topLeft;
+ Position = topLeft;
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
new file mode 100644
index 0000000000..468d980b23
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs
@@ -0,0 +1,110 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Graphics;
+using osu.Framework.Input.Events;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Rulesets.Taiko.UI;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
+{
+ public class TaikoSpanPlacementBlueprint : PlacementBlueprint
+ {
+ private readonly HitPiece headPiece;
+ private readonly HitPiece tailPiece;
+
+ private readonly LengthPiece lengthPiece;
+
+ private readonly IHasDuration spanPlacementObject;
+
+ public TaikoSpanPlacementBlueprint(HitObject hitObject)
+ : base(hitObject)
+ {
+ spanPlacementObject = hitObject as IHasDuration;
+
+ RelativeSizeAxes = Axes.Both;
+
+ InternalChildren = new Drawable[]
+ {
+ headPiece = new HitPiece
+ {
+ Size = new Vector2(TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.DEFAULT_HEIGHT)
+ },
+ lengthPiece = new LengthPiece
+ {
+ Height = TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.DEFAULT_HEIGHT
+ },
+ tailPiece = new HitPiece
+ {
+ Size = new Vector2(TaikoHitObject.DEFAULT_SIZE * TaikoPlayfield.DEFAULT_HEIGHT)
+ }
+ };
+ }
+
+ private double originalStartTime;
+ private Vector2 originalPosition;
+
+ protected override bool OnMouseDown(MouseDownEvent e)
+ {
+ if (e.Button != MouseButton.Left)
+ return false;
+
+ BeginPlacement(true);
+ return true;
+ }
+
+ protected override void OnMouseUp(MouseUpEvent e)
+ {
+ if (e.Button != MouseButton.Left)
+ return;
+
+ base.OnMouseUp(e);
+ EndPlacement(true);
+ }
+
+ public override void UpdatePosition(SnapResult result)
+ {
+ base.UpdatePosition(result);
+
+ if (PlacementActive)
+ {
+ if (result.Time is double dragTime)
+ {
+ if (dragTime < originalStartTime)
+ {
+ HitObject.StartTime = dragTime;
+ spanPlacementObject.Duration = Math.Abs(dragTime - originalStartTime);
+ headPiece.Position = ToLocalSpace(result.ScreenSpacePosition);
+ tailPiece.Position = originalPosition;
+ }
+ else
+ {
+ HitObject.StartTime = originalStartTime;
+ spanPlacementObject.Duration = Math.Abs(dragTime - originalStartTime);
+ tailPiece.Position = ToLocalSpace(result.ScreenSpacePosition);
+ headPiece.Position = originalPosition;
+ }
+
+ lengthPiece.X = headPiece.X;
+ lengthPiece.Width = tailPiece.X - headPiece.X;
+ }
+ }
+ else
+ {
+ lengthPiece.Position = headPiece.Position = tailPiece.Position = ToLocalSpace(result.ScreenSpacePosition);
+
+ if (result.Time is double startTime)
+ {
+ originalStartTime = HitObject.StartTime = startTime;
+ originalPosition = ToLocalSpace(result.ScreenSpacePosition);
+ }
+ }
+ }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs
new file mode 100644
index 0000000000..bf77c76670
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs
@@ -0,0 +1,20 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Tools;
+using osu.Game.Rulesets.Taiko.Edit.Blueprints;
+using osu.Game.Rulesets.Taiko.Objects;
+
+namespace osu.Game.Rulesets.Taiko.Edit
+{
+ public class DrumRollCompositionTool : HitObjectCompositionTool
+ {
+ public DrumRollCompositionTool()
+ : base(nameof(DrumRoll))
+ {
+ }
+
+ public override PlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint();
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs
new file mode 100644
index 0000000000..e877cf6240
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs
@@ -0,0 +1,20 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Tools;
+using osu.Game.Rulesets.Taiko.Edit.Blueprints;
+using osu.Game.Rulesets.Taiko.Objects;
+
+namespace osu.Game.Rulesets.Taiko.Edit
+{
+ public class HitCompositionTool : HitObjectCompositionTool
+ {
+ public HitCompositionTool()
+ : base(nameof(Hit))
+ {
+ }
+
+ public override PlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint();
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs
new file mode 100644
index 0000000000..a6191fcedc
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs
@@ -0,0 +1,20 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Tools;
+using osu.Game.Rulesets.Taiko.Edit.Blueprints;
+using osu.Game.Rulesets.Taiko.Objects;
+
+namespace osu.Game.Rulesets.Taiko.Edit
+{
+ public class SwellCompositionTool : HitObjectCompositionTool
+ {
+ public SwellCompositionTool()
+ : base(nameof(Swell))
+ {
+ }
+
+ public override PlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint();
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs
new file mode 100644
index 0000000000..35227b3c64
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs
@@ -0,0 +1,24 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Taiko.Edit.Blueprints;
+using osu.Game.Screens.Edit.Compose.Components;
+
+namespace osu.Game.Rulesets.Taiko.Edit
+{
+ public class TaikoBlueprintContainer : ComposeBlueprintContainer
+ {
+ public TaikoBlueprintContainer(IEnumerable hitObjects)
+ : base(hitObjects)
+ {
+ }
+
+ protected override SelectionHandler CreateSelectionHandler() => new TaikoSelectionHandler();
+
+ public override OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) =>
+ new TaikoSelectionBlueprint(hitObject);
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs
new file mode 100644
index 0000000000..cdc9672a8e
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs
@@ -0,0 +1,30 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Collections.Generic;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Edit.Tools;
+using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Screens.Edit.Compose.Components;
+
+namespace osu.Game.Rulesets.Taiko.Edit
+{
+ public class TaikoHitObjectComposer : HitObjectComposer
+ {
+ public TaikoHitObjectComposer(TaikoRuleset ruleset)
+ : base(ruleset)
+ {
+ }
+
+ protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[]
+ {
+ new HitCompositionTool(),
+ new DrumRollCompositionTool(),
+ new SwellCompositionTool()
+ };
+
+ protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects)
+ => new TaikoBlueprintContainer(hitObjects);
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
new file mode 100644
index 0000000000..eebf6980fe
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs
@@ -0,0 +1,80 @@
+// Copyright (c) ppy Pty Ltd . 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.Linq;
+using osu.Framework.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Rulesets.Edit;
+using osu.Game.Rulesets.Taiko.Objects;
+using osu.Game.Screens.Edit.Compose.Components;
+
+namespace osu.Game.Rulesets.Taiko.Edit
+{
+ public class TaikoSelectionHandler : SelectionHandler
+ {
+ protected override IEnumerable
[Test]
- public void PlayerLoaderSettingsHoverTest()
+ public void TestPlayerLoaderSettingsHover()
{
setupUserSettings();
AddStep("Start player loader", () => songSelect.Push(playerLoader = new TestPlayerLoader(player = new LoadBlockingTestPlayer { BlockLoad = true })));
@@ -79,11 +79,9 @@ namespace osu.Game.Tests.Visual.Background
InputManager.MoveMouseTo(playerLoader.ScreenPos);
InputManager.MoveMouseTo(playerLoader.VisualSettingsPos);
});
- waitForDim();
- AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
+ AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddStep("Stop background preview", () => InputManager.MoveMouseTo(playerLoader.ScreenPos));
- waitForDim();
- AddAssert("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && playerLoader.IsBlurCorrect());
+ AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && playerLoader.IsBlurCorrect());
}
///
@@ -92,20 +90,19 @@ namespace osu.Game.Tests.Visual.Background
/// We need to check that in this scenario, the dim and blur is still properly applied after entering player.
///
[Test]
- public void PlayerLoaderTransitionTest()
+ public void TestPlayerLoaderTransition()
{
performFullSetup();
AddStep("Trigger hover event", () => playerLoader.TriggerOnHover());
AddAssert("Background retained from song select", () => songSelect.IsBackgroundCurrent());
- waitForDim();
- AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
+ AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
}
///
/// Make sure the background is fully invisible (Alpha == 0) when the background should be disabled by the storyboard.
///
[Test]
- public void StoryboardBackgroundVisibilityTest()
+ public void TestStoryboardBackgroundVisibility()
{
performFullSetup();
createFakeStoryboard();
@@ -114,52 +111,46 @@ namespace osu.Game.Tests.Visual.Background
player.ReplacesBackground.Value = true;
player.StoryboardEnabled.Value = true;
});
- waitForDim();
- AddAssert("Background is invisible, storyboard is visible", () => songSelect.IsBackgroundInvisible() && player.IsStoryboardVisible);
+ AddUntilStep("Background is invisible, storyboard is visible", () => songSelect.IsBackgroundInvisible() && player.IsStoryboardVisible);
AddStep("Disable Storyboard", () =>
{
player.ReplacesBackground.Value = false;
player.StoryboardEnabled.Value = false;
});
- waitForDim();
- AddAssert("Background is visible, storyboard is invisible", () => songSelect.IsBackgroundVisible() && !player.IsStoryboardVisible);
+ AddUntilStep("Background is visible, storyboard is invisible", () => songSelect.IsBackgroundVisible() && !player.IsStoryboardVisible);
}
///
/// When exiting player, the screen that it suspends/exits to needs to have a fully visible (Alpha == 1) background.
///
[Test]
- public void StoryboardTransitionTest()
+ public void TestStoryboardTransition()
{
performFullSetup();
createFakeStoryboard();
AddStep("Exit to song select", () => player.Exit());
- waitForDim();
- AddAssert("Background is visible", () => songSelect.IsBackgroundVisible());
+ AddUntilStep("Background is visible", () => songSelect.IsBackgroundVisible());
}
///
/// Ensure is properly accepting user-defined visual changes for a background.
///
[Test]
- public void DisableUserDimBackgroundTest()
+ public void TestDisableUserDimBackground()
{
performFullSetup();
- waitForDim();
- AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
+ AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddStep("Enable user dim", () => songSelect.DimEnabled.Value = false);
- waitForDim();
- AddAssert("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsUserBlurDisabled());
+ AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsUserBlurDisabled());
AddStep("Disable user dim", () => songSelect.DimEnabled.Value = true);
- waitForDim();
- AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
+ AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
}
///
/// Ensure is properly accepting user-defined visual changes for a storyboard.
///
[Test]
- public void DisableUserDimStoryboardTest()
+ public void TestDisableUserDimStoryboard()
{
performFullSetup();
createFakeStoryboard();
@@ -170,41 +161,36 @@ namespace osu.Game.Tests.Visual.Background
});
AddStep("Enable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = true);
AddStep("Set dim level to 1", () => songSelect.DimLevel.Value = 1f);
- waitForDim();
- AddAssert("Storyboard is invisible", () => !player.IsStoryboardVisible);
+ AddUntilStep("Storyboard is invisible", () => !player.IsStoryboardVisible);
AddStep("Disable user dim", () => player.DimmableStoryboard.EnableUserDim.Value = false);
- waitForDim();
- AddAssert("Storyboard is visible", () => player.IsStoryboardVisible);
+ AddUntilStep("Storyboard is visible", () => player.IsStoryboardVisible);
}
///
/// Check if the visual settings container retains dim and blur when pausing
///
[Test]
- public void PauseTest()
+ public void TestPause()
{
performFullSetup(true);
AddStep("Pause", () => player.Pause());
- waitForDim();
- AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
+ AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddStep("Unpause", () => player.Resume());
- waitForDim();
- AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
+ AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
}
///
/// Check if the visual settings container removes user dim when suspending for
///
[Test]
- public void TransitionTest()
+ public void TestTransition()
{
performFullSetup();
FadeAccessibleResults results = null;
AddStep("Transition to Results", () => player.Push(results =
new FadeAccessibleResults(new ScoreInfo { User = new User { Username = "osu!" } })));
AddUntilStep("Wait for results is current", () => results.IsCurrentScreen());
- waitForDim();
- AddAssert("Screen is undimmed, original background retained", () =>
+ AddUntilStep("Screen is undimmed, original background retained", () =>
songSelect.IsBackgroundUndimmed() && songSelect.IsBackgroundCurrent() && results.IsBlurCorrect());
}
@@ -212,32 +198,27 @@ namespace osu.Game.Tests.Visual.Background
/// Check if background gets undimmed and unblurred when leaving for
///
[Test]
- public void TransitionOutTest()
+ public void TestTransitionOut()
{
performFullSetup();
AddStep("Exit to song select", () => player.Exit());
- waitForDim();
- AddAssert("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsBlurCorrect());
+ AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.IsBlurCorrect());
}
///
/// Check if hovering on the visual settings dialogue after resuming from player still previews the background dim.
///
[Test]
- public void ResumeFromPlayerTest()
+ public void TestResumeFromPlayer()
{
performFullSetup();
AddStep("Move mouse to Visual Settings", () => InputManager.MoveMouseTo(playerLoader.VisualSettingsPos));
AddStep("Resume PlayerLoader", () => player.Restart());
- waitForDim();
- AddAssert("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
+ AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied());
AddStep("Move mouse to center of screen", () => InputManager.MoveMouseTo(playerLoader.ScreenPos));
- waitForDim();
- AddAssert("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && playerLoader.IsBlurCorrect());
+ AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && playerLoader.IsBlurCorrect());
}
- private void waitForDim() => AddWaitStep("Wait for dim", 5);
-
private void createFakeStoryboard() => AddStep("Create storyboard", () =>
{
player.StoryboardEnabled.Value = false;
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
index 417d16fdb0..8190cf5f89 100644
--- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
+++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Editing
[Cached(typeof(EditorBeatmap))]
private readonly EditorBeatmap editorBeatmap;
- [Cached(typeof(IDistanceSnapProvider))]
+ [Cached(typeof(IPositionSnapProvider))]
private readonly SnapProvider snapProvider = new SnapProvider();
public TestSceneDistanceSnapGrid()
@@ -151,9 +151,9 @@ namespace osu.Game.Tests.Visual.Editing
=> (Vector2.Zero, 0);
}
- private class SnapProvider : IDistanceSnapProvider
+ private class SnapProvider : IPositionSnapProvider
{
- public (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) => (position, time);
+ public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, 0);
public float GetBeatSnapDistanceAt(double referenceTime) => 10;
diff --git a/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs b/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs
index 3af976cae0..6aa884a197 100644
--- a/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs
+++ b/osu.Game.Tests/Visual/Editing/TestScenePlaybackControl.cs
@@ -4,8 +4,8 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
-using osu.Framework.Timing;
using osu.Game.Beatmaps;
+using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components;
using osuTK;
@@ -17,9 +17,8 @@ namespace osu.Game.Tests.Visual.Editing
[BackgroundDependencyLoader]
private void load()
{
- var clock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
- Dependencies.CacheAs(clock);
- Dependencies.CacheAs(clock);
+ var clock = new EditorClock { IsCoupled = false };
+ Dependencies.CacheAs(clock);
var playback = new PlaybackControl
{
diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs
index 01ef7e6170..fdb8781563 100644
--- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs
+++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs
@@ -7,7 +7,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit;
@@ -69,7 +68,7 @@ namespace osu.Game.Tests.Visual.Editing
private IBindable beatmap { get; set; }
[Resolved]
- private IAdjustableClock adjustableClock { get; set; }
+ private EditorClock editorClock { get; set; }
public AudioVisualiser()
{
@@ -96,13 +95,15 @@ namespace osu.Game.Tests.Visual.Editing
base.Update();
if (beatmap.Value.Track.IsLoaded)
- marker.X = (float)(adjustableClock.CurrentTime / beatmap.Value.Track.Length);
+ marker.X = (float)(editorClock.CurrentTime / beatmap.Value.Track.Length);
}
}
private class StartStopButton : OsuButton
{
- private IAdjustableClock adjustableClock;
+ [Resolved]
+ private EditorClock editorClock { get; set; }
+
private bool started;
public StartStopButton()
@@ -114,22 +115,16 @@ namespace osu.Game.Tests.Visual.Editing
Action = onClick;
}
- [BackgroundDependencyLoader]
- private void load(IAdjustableClock adjustableClock)
- {
- this.adjustableClock = adjustableClock;
- }
-
private void onClick()
{
if (started)
{
- adjustableClock.Stop();
+ editorClock.Stop();
Text = "Start";
}
else
{
- adjustableClock.Start();
+ editorClock.Start();
Text = "Stop";
}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs
index b25b81c9af..bd7e894cf8 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs
@@ -281,7 +281,7 @@ namespace osu.Game.Tests.Visual.Gameplay
yield return new TestHitObject
{
StartTime = original.StartTime,
- EndTime = (original as IHasEndTime)?.EndTime ?? (original.StartTime + 100)
+ Duration = (original as IHasDuration)?.Duration ?? 100
};
}
}
@@ -290,11 +290,11 @@ namespace osu.Game.Tests.Visual.Gameplay
#region HitObject
- private class TestHitObject : ConvertHitObject, IHasEndTime
+ private class TestHitObject : ConvertHitObject, IHasDuration
{
- public double EndTime { get; set; }
+ public double EndTime => StartTime + Duration;
- public double Duration => EndTime - StartTime;
+ public double Duration { get; set; }
}
private class DrawableTestHitObject : DrawableHitObject
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs
index 1908988739..3a71d4ca54 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
var beatmap = Beatmap.Value.GetPlayableBeatmap(ruleset.RulesetInfo, Array.Empty());
- return new ScoreAccessibleReplayPlayer(ruleset.GetAutoplayMod().CreateReplayScore(beatmap));
+ return new ScoreAccessibleReplayPlayer(ruleset.GetAutoplayMod()?.CreateReplayScore(beatmap));
}
protected override void AddCheckSteps()
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
index a35437a286..1809332bce 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayDownloadButton.cs
@@ -28,6 +28,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep(@"locally available state", () => downloadButton.SetDownloadState(DownloadState.LocallyAvailable));
AddStep(@"not downloaded state", () => downloadButton.SetDownloadState(DownloadState.NotDownloaded));
createButton(false);
+ createButtonNoScore();
}
private void createButton(bool withReplay)
@@ -40,6 +41,22 @@ namespace osu.Game.Tests.Visual.Gameplay
Origin = Anchor.Centre,
};
});
+
+ AddUntilStep("wait for load", () => downloadButton.IsLoaded);
+ }
+
+ private void createButtonNoScore()
+ {
+ AddStep("create button with null score", () =>
+ {
+ Child = downloadButton = new TestReplayDownloadButton(null)
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ };
+ });
+
+ AddUntilStep("wait for load", () => downloadButton.IsLoaded);
}
private ScoreInfo getScoreInfo(bool replayAvailable)
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
index 0d15e495e3..2f15e549f7 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScrollingHitObjects.cs
@@ -16,6 +16,7 @@ using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
+using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Timing;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
@@ -77,19 +78,18 @@ namespace osu.Game.Tests.Visual.Gameplay
}
};
- setUpHitObjects();
+ hitObjectSpawnDelegate?.Cancel();
});
- private void setUpHitObjects()
+ private void setUpHitObjects() => AddStep("set up hit objects", () =>
{
scrollContainers.ForEach(c => c.ControlPoints.Add(new MultiplierControlPoint(0)));
for (int i = spawn_rate / 2; i <= time_range; i += spawn_rate)
addHitObject(Time.Current + i);
- hitObjectSpawnDelegate?.Cancel();
hitObjectSpawnDelegate = Scheduler.AddDelayed(() => addHitObject(Time.Current + time_range), spawn_rate, true);
- }
+ });
private IList testControlPoints => new List
{
@@ -101,6 +101,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestScrollAlgorithms()
{
+ setUpHitObjects();
+
AddStep("constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant));
AddStep("overlapping scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Overlapping));
AddStep("sequential scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Sequential));
@@ -113,6 +115,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestConstantScrollLifetime()
{
+ setUpHitObjects();
+
AddStep("set constant scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Constant));
// scroll container time range must be less than the rate of spawning hitobjects
// otherwise the hitobjects will spawn already partly visible on screen and look wrong
@@ -122,14 +126,40 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestSequentialScrollLifetime()
{
+ setUpHitObjects();
+
AddStep("set sequential scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Sequential));
AddStep("set time range", () => scrollContainers.ForEach(c => c.TimeRange = time_range / 2.0));
AddStep("add control points", () => addControlPoints(testControlPoints, Time.Current));
}
+ [Test]
+ public void TestSlowSequentialScroll()
+ {
+ AddStep("set sequential scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Sequential));
+ AddStep("set time range", () => scrollContainers.ForEach(c => c.TimeRange = time_range));
+ AddStep("add control points", () => addControlPoints(
+ new List
+ {
+ new MultiplierControlPoint { Velocity = 0.1 }
+ },
+ Time.Current + time_range));
+
+ // All of the hit objects added below should be immediately visible on screen
+ AddStep("add hit objects", () =>
+ {
+ for (int i = 0; i < 20; ++i)
+ {
+ addHitObject(Time.Current + time_range * (2 + 0.1 * i));
+ }
+ });
+ }
+
[Test]
public void TestOverlappingScrollLifetime()
{
+ setUpHitObjects();
+
AddStep("set overlapping scroll", () => setScrollAlgorithm(ScrollVisualisationMethod.Overlapping));
AddStep("set time range", () => scrollContainers.ForEach(c => c.TimeRange = time_range / 2.0));
AddStep("add control points", () => addControlPoints(testControlPoints, Time.Current));
@@ -221,7 +251,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private class TestDrawableControlPoint : DrawableHitObject
{
public TestDrawableControlPoint(ScrollingDirection direction, double time)
- : base(new HitObject { StartTime = time })
+ : base(new HitObject { StartTime = time, HitWindows = HitWindows.Empty })
{
Origin = Anchor.Centre;
@@ -252,7 +282,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private class TestDrawableHitObject : DrawableHitObject
{
public TestDrawableHitObject(double time)
- : base(new HitObject { StartTime = time })
+ : base(new HitObject { StartTime = time, HitWindows = HitWindows.Empty })
{
Origin = Anchor.Custom;
OriginPosition = new Vector2(75 / 4.0f);
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
index 05b33e4386..0025a26baf 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
@@ -246,7 +246,12 @@ namespace osu.Game.Tests.Visual.Online
{
((BindableList)ChannelManager.AvailableChannels).AddRange(channels);
- Child = ChatOverlay = new TestChatOverlay { RelativeSizeAxes = Axes.Both, };
+ InternalChildren = new Drawable[]
+ {
+ ChannelManager,
+ ChatOverlay = new TestChatOverlay { RelativeSizeAxes = Axes.Both, },
+ };
+
ChatOverlay.Show();
}
}
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs
new file mode 100644
index 0000000000..76cfe75b59
--- /dev/null
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneContractedPanelMiddleContent.cs
@@ -0,0 +1,61 @@
+// Copyright (c) ppy Pty Ltd . 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.Bindables;
+using osu.Framework.Extensions.Color4Extensions;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Scoring;
+using osu.Game.Screens.Ranking;
+using osu.Game.Screens.Ranking.Contracted;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Ranking
+{
+ public class TestSceneContractedPanelMiddleContent : OsuTestScene
+ {
+ [Resolved]
+ private RulesetStore rulesetStore { get; set; }
+
+ [Test]
+ public void TestShowPanel()
+ {
+ AddStep("show example score", () => showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), new TestScoreInfo(new OsuRuleset().RulesetInfo)));
+ }
+
+ private void showPanel(WorkingBeatmap workingBeatmap, ScoreInfo score)
+ {
+ Child = new ContractedPanelMiddleContentContainer(workingBeatmap, score);
+ }
+
+ private class ContractedPanelMiddleContentContainer : Container
+ {
+ [Cached]
+ private Bindable workingBeatmap { get; set; }
+
+ public ContractedPanelMiddleContentContainer(WorkingBeatmap beatmap, ScoreInfo score)
+ {
+ workingBeatmap = new Bindable(beatmap);
+
+ Anchor = Anchor.Centre;
+ Origin = Anchor.Centre;
+ Size = new Vector2(ScorePanel.CONTRACTED_WIDTH, 460);
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = Color4Extensions.FromHex("#353535"),
+ },
+ new ContractedPanelMiddleContent(score),
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
index 106b4187ee..69511b85c0 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -14,10 +13,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets;
-using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
-using osu.Game.Rulesets.Osu.Mods;
-using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Ranking.Expanded;
@@ -37,7 +33,7 @@ namespace osu.Game.Tests.Visual.Ranking
{
var author = new User { Username = "mapper_name" };
- AddStep("show example score", () => showPanel(createTestBeatmap(author), createTestScore()));
+ AddStep("show example score", () => showPanel(createTestBeatmap(author), new TestScoreInfo(new OsuRuleset().RulesetInfo)));
AddAssert("mapper name present", () => this.ChildrenOfType().Any(spriteText => spriteText.Text == "mapper_name"));
}
@@ -45,7 +41,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestMapWithUnknownMapper()
{
- AddStep("show example score", () => showPanel(createTestBeatmap(null), createTestScore()));
+ AddStep("show example score", () => showPanel(createTestBeatmap(null), new TestScoreInfo(new OsuRuleset().RulesetInfo)));
AddAssert("mapped by text not present", () =>
this.ChildrenOfType().All(spriteText => !containsAny(spriteText.Text, "mapped", "by")));
@@ -66,29 +62,6 @@ namespace osu.Game.Tests.Visual.Ranking
return new TestWorkingBeatmap(beatmap);
}
- private ScoreInfo createTestScore() => new ScoreInfo
- {
- User = new User
- {
- Id = 2,
- Username = "peppy",
- },
- Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo,
- Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() },
- TotalScore = 999999,
- Accuracy = 0.95,
- MaxCombo = 999,
- Rank = ScoreRank.S,
- Date = DateTimeOffset.Now,
- Statistics =
- {
- { HitResult.Miss, 1 },
- { HitResult.Meh, 50 },
- { HitResult.Good, 100 },
- { HitResult.Great, 300 },
- }
- };
-
private bool containsAny(string text, params string[] stringsToMatch) => stringsToMatch.Any(text.Contains);
private class ExpandedPanelMiddleContentContainer : Container
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs
index afaa607099..a32bcbe7f0 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelTopContent.cs
@@ -5,8 +5,8 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
+using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Ranking.Expanded;
-using osu.Game.Users;
using osuTK;
namespace osu.Game.Tests.Visual.Ranking
@@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Ranking
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#444"),
},
- new ExpandedPanelTopContent(new User { Id = 2, Username = "peppy" }),
+ new ExpandedPanelTopContent(new TestScoreInfo(new OsuRuleset().RulesetInfo).User),
}
};
}
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
index aa0ce89d93..125aa0a1e7 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs
@@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd . 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
@@ -11,13 +9,10 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu;
-using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
-using osu.Game.Tests.Beatmaps;
-using osu.Game.Users;
namespace osu.Game.Tests.Visual.Ranking
{
@@ -41,31 +36,14 @@ namespace osu.Game.Tests.Visual.Ranking
Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmapInfo);
}
- private TestSoloResults createResultsScreen() => new TestSoloResults(new ScoreInfo
- {
- TotalScore = 2845370,
- Accuracy = 0.98,
- MaxCombo = 123,
- Rank = ScoreRank.A,
- Date = DateTimeOffset.Now,
- Statistics = new Dictionary
- {
- { HitResult.Great, 50 },
- { HitResult.Good, 20 },
- { HitResult.Meh, 50 },
- { HitResult.Miss, 1 }
- },
- Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo,
- User = new User
- {
- Username = "peppy",
- }
- });
+ private TestResultsScreen createResultsScreen() => new TestResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo));
+
+ private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo));
[Test]
public void ResultsWithoutPlayer()
{
- TestSoloResults screen = null;
+ TestResultsScreen screen = null;
OsuScreenStack stack;
AddStep("load results", () =>
@@ -84,13 +62,23 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void ResultsWithPlayer()
{
- TestSoloResults screen = null;
+ TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddAssert("retry overlay present", () => screen.RetryOverlay != null);
}
+ [Test]
+ public void ResultsForUnranked()
+ {
+ UnrankedSoloResultsScreen screen = null;
+
+ AddStep("load results", () => Child = new TestResultsContainer(screen = createUnrankedSoloResultsScreen()));
+ AddUntilStep("wait for loaded", () => screen.IsLoaded);
+ AddAssert("retry overlay present", () => screen.RetryOverlay != null);
+ }
+
private class TestResultsContainer : Container
{
[Cached(typeof(Player))]
@@ -110,11 +98,11 @@ namespace osu.Game.Tests.Visual.Ranking
}
}
- private class TestSoloResults : ResultsScreen
+ private class TestResultsScreen : ResultsScreen
{
public HotkeyRetryOverlay RetryOverlay;
- public TestSoloResults(ScoreInfo score)
+ public TestResultsScreen(ScoreInfo score)
: base(score)
{
}
@@ -126,5 +114,24 @@ namespace osu.Game.Tests.Visual.Ranking
RetryOverlay = InternalChildren.OfType().SingleOrDefault();
}
}
+
+ private class UnrankedSoloResultsScreen : SoloResultsScreen
+ {
+ public HotkeyRetryOverlay RetryOverlay;
+
+ public UnrankedSoloResultsScreen(ScoreInfo score)
+ : base(score)
+ {
+ Score.Beatmap.OnlineBeatmapID = 0;
+ Score.Beatmap.Status = BeatmapSetOnlineStatus.Pending;
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ RetryOverlay = InternalChildren.OfType().SingleOrDefault();
+ }
+ }
}
}
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs
index 880e331b92..250fdc5ebd 100644
--- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs
@@ -1,28 +1,23 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
-using System;
using NUnit.Framework;
using osu.Framework.Graphics;
-using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
-using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
-using osu.Game.Tests.Beatmaps;
-using osu.Game.Users;
namespace osu.Game.Tests.Visual.Ranking
{
public class TestSceneScorePanel : OsuTestScene
{
+ private ScorePanel panel;
+
[Test]
public void TestDRank()
{
- var score = createScore();
- score.Accuracy = 0.5;
- score.Rank = ScoreRank.D;
+ var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.5, Rank = ScoreRank.D };
addPanelStep(score);
}
@@ -30,9 +25,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestCRank()
{
- var score = createScore();
- score.Accuracy = 0.75;
- score.Rank = ScoreRank.C;
+ var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.75, Rank = ScoreRank.C };
addPanelStep(score);
}
@@ -40,9 +33,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestBRank()
{
- var score = createScore();
- score.Accuracy = 0.85;
- score.Rank = ScoreRank.B;
+ var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.85, Rank = ScoreRank.B };
addPanelStep(score);
}
@@ -50,9 +41,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestARank()
{
- var score = createScore();
- score.Accuracy = 0.925;
- score.Rank = ScoreRank.A;
+ var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A };
addPanelStep(score);
}
@@ -60,9 +49,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestSRank()
{
- var score = createScore();
- score.Accuracy = 0.975;
- score.Rank = ScoreRank.S;
+ var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.975, Rank = ScoreRank.S };
addPanelStep(score);
}
@@ -70,9 +57,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestAlmostSSRank()
{
- var score = createScore();
- score.Accuracy = 0.9999;
- score.Rank = ScoreRank.S;
+ var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.9999, Rank = ScoreRank.S };
addPanelStep(score);
}
@@ -80,9 +65,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestSSRank()
{
- var score = createScore();
- score.Accuracy = 1;
- score.Rank = ScoreRank.X;
+ var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 1, Rank = ScoreRank.X };
addPanelStep(score);
}
@@ -90,44 +73,42 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestAllHitResults()
{
- var score = createScore();
- score.Statistics[HitResult.Perfect] = 350;
- score.Statistics[HitResult.Ok] = 200;
+ var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Statistics = { [HitResult.Perfect] = 350, [HitResult.Ok] = 200 } };
addPanelStep(score);
}
- private void addPanelStep(ScoreInfo score) => AddStep("add panel", () =>
+ [Test]
+ public void TestContractedPanel()
{
- Child = new ScorePanel(score)
+ var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A };
+
+ addPanelStep(score, PanelState.Contracted);
+ }
+
+ [Test]
+ public void TestExpandAndContract()
+ {
+ var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A };
+
+ addPanelStep(score, PanelState.Contracted);
+ AddWaitStep("wait for transition", 10);
+
+ AddStep("expand panel", () => panel.State = PanelState.Expanded);
+ AddWaitStep("wait for transition", 10);
+
+ AddStep("contract panel", () => panel.State = PanelState.Contracted);
+ AddWaitStep("wait for transition", 10);
+ }
+
+ private void addPanelStep(ScoreInfo score, PanelState state = PanelState.Expanded) => AddStep("add panel", () =>
+ {
+ Child = panel = new ScorePanel(score)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- State = PanelState.Expanded
+ State = state
};
});
-
- private ScoreInfo createScore() => new ScoreInfo
- {
- User = new User
- {
- Id = 2,
- Username = "peppy",
- },
- Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo,
- Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() },
- TotalScore = 2845370,
- Accuracy = 0.95,
- MaxCombo = 999,
- Rank = ScoreRank.S,
- Date = DateTimeOffset.Now,
- Statistics =
- {
- { HitResult.Miss, 1 },
- { HitResult.Meh, 50 },
- { HitResult.Good, 100 },
- { HitResult.Great, 300 },
- }
- };
}
}
diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs
new file mode 100644
index 0000000000..e65dcb19b1
--- /dev/null
+++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanelList.cs
@@ -0,0 +1,208 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Graphics;
+using osu.Framework.Testing;
+using osu.Framework.Utils;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Scoring;
+using osu.Game.Screens.Ranking;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Ranking
+{
+ public class TestSceneScorePanelList : OsuManualInputManagerTestScene
+ {
+ private ScorePanelList list;
+
+ [Test]
+ public void TestEmptyList()
+ {
+ createListStep(() => new ScorePanelList());
+ }
+
+ [Test]
+ public void TestEmptyListWithSelectedScore()
+ {
+ createListStep(() => new ScorePanelList
+ {
+ SelectedScore = { Value = new TestScoreInfo(new OsuRuleset().RulesetInfo) }
+ });
+ }
+
+ [Test]
+ public void TestAddPanelAfterSelectingScore()
+ {
+ var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
+
+ createListStep(() => new ScorePanelList
+ {
+ SelectedScore = { Value = score }
+ });
+
+ AddStep("add panel", () => list.AddScore(score));
+
+ assertScoreState(score, true);
+ assertExpandedPanelCentred();
+ }
+
+ [Test]
+ public void TestAddPanelBeforeSelectingScore()
+ {
+ var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
+
+ createListStep(() => new ScorePanelList());
+
+ AddStep("add panel", () => list.AddScore(score));
+
+ assertScoreState(score, false);
+ assertFirstPanelCentred();
+
+ AddStep("select score", () => list.SelectedScore.Value = score);
+
+ assertScoreState(score, true);
+ assertExpandedPanelCentred();
+ }
+
+ [Test]
+ public void TestAddManyNonExpandedPanels()
+ {
+ createListStep(() => new ScorePanelList());
+
+ AddStep("add many scores", () =>
+ {
+ for (int i = 0; i < 20; i++)
+ list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo));
+ });
+
+ assertFirstPanelCentred();
+ }
+
+ [Test]
+ public void TestAddManyScoresAfterExpandedPanel()
+ {
+ var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
+
+ createListStep(() => new ScorePanelList());
+
+ AddStep("add initial panel and select", () =>
+ {
+ list.AddScore(initialScore);
+ list.SelectedScore.Value = initialScore;
+ });
+
+ AddStep("add many scores", () =>
+ {
+ for (int i = 0; i < 20; i++)
+ list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore - i - 1 });
+ });
+
+ assertScoreState(initialScore, true);
+ assertExpandedPanelCentred();
+ }
+
+ [Test]
+ public void TestAddManyScoresBeforeExpandedPanel()
+ {
+ var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
+
+ createListStep(() => new ScorePanelList());
+
+ AddStep("add initial panel and select", () =>
+ {
+ list.AddScore(initialScore);
+ list.SelectedScore.Value = initialScore;
+ });
+
+ AddStep("add scores", () =>
+ {
+ for (int i = 0; i < 20; i++)
+ list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore + i + 1 });
+ });
+
+ assertScoreState(initialScore, true);
+ assertExpandedPanelCentred();
+ }
+
+ [Test]
+ public void TestAddManyPanelsOnBothSidesOfExpandedPanel()
+ {
+ var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
+
+ createListStep(() => new ScorePanelList());
+
+ AddStep("add initial panel and select", () =>
+ {
+ list.AddScore(initialScore);
+ list.SelectedScore.Value = initialScore;
+ });
+
+ AddStep("add scores after", () =>
+ {
+ for (int i = 0; i < 20; i++)
+ list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore - i - 1 });
+
+ for (int i = 0; i < 20; i++)
+ list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore + i + 1 });
+ });
+
+ assertScoreState(initialScore, true);
+ assertExpandedPanelCentred();
+ }
+
+ [Test]
+ public void TestSelectMultipleScores()
+ {
+ var firstScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
+ var secondScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
+
+ createListStep(() => new ScorePanelList());
+
+ AddStep("add scores and select first", () =>
+ {
+ list.AddScore(firstScore);
+ list.AddScore(secondScore);
+ list.SelectedScore.Value = firstScore;
+ });
+
+ assertScoreState(firstScore, true);
+ assertScoreState(secondScore, false);
+
+ AddStep("select second score", () =>
+ {
+ InputManager.MoveMouseTo(list.ChildrenOfType().Single(p => p.Score == secondScore));
+ InputManager.Click(MouseButton.Left);
+ });
+
+ assertScoreState(firstScore, false);
+ assertScoreState(secondScore, true);
+ assertExpandedPanelCentred();
+ }
+
+ private void createListStep(Func creationFunc)
+ {
+ AddStep("create list", () => Child = list = creationFunc().With(d =>
+ {
+ d.Anchor = Anchor.Centre;
+ d.Origin = Anchor.Centre;
+ }));
+
+ AddUntilStep("wait for load", () => list.IsLoaded);
+ }
+
+ private void assertExpandedPanelCentred() => AddUntilStep("expanded panel centred", () =>
+ {
+ var expandedPanel = list.ChildrenOfType().Single(p => p.State == PanelState.Expanded);
+ return Precision.AlmostEquals(expandedPanel.ScreenSpaceDrawQuad.Centre.X, list.ScreenSpaceDrawQuad.Centre.X, 1);
+ });
+
+ private void assertFirstPanelCentred()
+ => AddUntilStep("first panel centred", () => Precision.AlmostEquals(list.ChildrenOfType().First().ScreenSpaceDrawQuad.Centre.X, list.ScreenSpaceDrawQuad.Centre.X, 1));
+
+ private void assertScoreState(ScoreInfo score, bool expanded)
+ => AddUntilStep($"score expanded = {expanded}", () => (list.ChildrenOfType().Single(p => p.Score == score).State == PanelState.Expanded) == expanded);
+ }
+}
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
index 745820696a..3d335995ac 100644
--- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
+++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs
@@ -1,13 +1,19 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Diagnostics;
+using System.Linq;
using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Framework.Threading;
using osu.Game.Overlays;
+using osu.Game.Overlays.KeyBinding;
+using osuTK.Input;
namespace osu.Game.Tests.Visual.Settings
{
[TestFixture]
- public class TestSceneKeyBindingPanel : OsuTestScene
+ public class TestSceneKeyBindingPanel : OsuManualInputManagerTestScene
{
private readonly KeyBindingPanel panel;
@@ -21,5 +27,42 @@ namespace osu.Game.Tests.Visual.Settings
base.LoadComplete();
panel.Show();
}
+
+ [Test]
+ public void TestClickTwiceOnClearButton()
+ {
+ KeyBindingRow firstRow = null;
+
+ AddStep("click first row", () =>
+ {
+ firstRow = panel.ChildrenOfType().First();
+ InputManager.MoveMouseTo(firstRow);
+ InputManager.Click(MouseButton.Left);
+ });
+
+ AddStep("schedule button clicks", () =>
+ {
+ var clearButton = firstRow.ChildrenOfType().Single();
+
+ InputManager.MoveMouseTo(clearButton);
+
+ int buttonClicks = 0;
+ ScheduledDelegate clickDelegate = null;
+
+ clickDelegate = Scheduler.AddDelayed(() =>
+ {
+ InputManager.PressButton(MouseButton.Left);
+ InputManager.ReleaseButton(MouseButton.Left);
+
+ if (++buttonClicks == 2)
+ {
+ // ReSharper disable once AccessToModifiedClosure
+ Debug.Assert(clickDelegate != null);
+ // ReSharper disable once AccessToModifiedClosure
+ clickDelegate.Cancel();
+ }
+ }, 0, true);
+ });
+ }
}
}
diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs
index c91379b2d6..cf8eb8bd6c 100644
--- a/osu.Game.Tournament/Screens/SetupScreen.cs
+++ b/osu.Game.Tournament/Screens/SetupScreen.cs
@@ -25,7 +25,7 @@ namespace osu.Game.Tournament.Screens
private FillFlowContainer fillFlow;
private LoginOverlay loginOverlay;
- private ActionableInfo resolution;
+ private ResolutionSelector resolution;
[Resolved]
private MatchIPCInfo ipc { get; set; }
@@ -108,18 +108,20 @@ namespace osu.Game.Tournament.Screens
Items = rulesets.AvailableRulesets,
Current = LadderInfo.Ruleset,
},
- resolution = new ActionableInfo
+ resolution = new ResolutionSelector
{
Label = "Stream area resolution",
- ButtonText = "Set to 1080p",
- Action = () =>
+ ButtonText = "Set height",
+ Action = height =>
{
- windowSize.Value = new Size((int)(1920 / TournamentSceneManager.STREAM_AREA_WIDTH * TournamentSceneManager.REQUIRED_WIDTH), 1080);
+ windowSize.Value = new Size((int)(height * aspect_ratio / TournamentSceneManager.STREAM_AREA_WIDTH * TournamentSceneManager.REQUIRED_WIDTH), height);
}
},
};
}
+ private const float aspect_ratio = 16f / 9f;
+
protected override void Update()
{
base.Update();
@@ -174,6 +176,7 @@ namespace osu.Game.Tournament.Screens
public Action Action;
private TournamentSpriteText valueText;
+ protected FillFlowContainer FlowContainer;
protected override Drawable CreateComponent() => new Container
{
@@ -186,15 +189,67 @@ namespace osu.Game.Tournament.Screens
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
- button = new TriangleButton
+ FlowContainer = new FillFlowContainer
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
- Size = new Vector2(100, 30),
- Action = () => Action?.Invoke()
- },
+ AutoSizeAxes = Axes.Both,
+ Spacing = new Vector2(10, 0),
+ Children = new Drawable[]
+ {
+ button = new TriangleButton
+ {
+ Size = new Vector2(100, 40),
+ Action = () => Action?.Invoke()
+ }
+ }
+ }
}
};
}
+
+ private class ResolutionSelector : ActionableInfo
+ {
+ private const int minimum_window_height = 480;
+ private const int maximum_window_height = 2160;
+
+ public new Action Action;
+
+ private OsuNumberBox numberBox;
+
+ protected override Drawable CreateComponent()
+ {
+ var drawable = base.CreateComponent();
+ FlowContainer.Insert(-1, numberBox = new OsuNumberBox
+ {
+ Text = "1080",
+ Width = 100
+ });
+
+ base.Action = () =>
+ {
+ if (string.IsNullOrEmpty(numberBox.Text))
+ return;
+
+ // box contains text
+ if (!int.TryParse(numberBox.Text, out var number))
+ {
+ // at this point, the only reason we can arrive here is if the input number was too big to parse into an int
+ // so clamp to max allowed value
+ number = maximum_window_height;
+ }
+ else
+ {
+ number = Math.Clamp(number, minimum_window_height, maximum_window_height);
+ }
+
+ // in case number got clamped, reset number in numberBox
+ numberBox.Text = number.ToString();
+
+ Action?.Invoke(number);
+ };
+ return drawable;
+ }
+ }
}
}
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 7aaf0ca08d..e7cef13c68 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -27,14 +27,13 @@ using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using Decoder = osu.Game.Beatmaps.Formats.Decoder;
-using ZipArchive = SharpCompress.Archives.Zip.ZipArchive;
namespace osu.Game.Beatmaps
{
///
/// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps.
///
- public partial class BeatmapManager : DownloadableArchiveModelManager
+ public partial class BeatmapManager : DownloadableArchiveModelManager, IDisposable
{
///
/// Fired when a single difficulty has been hidden.
@@ -66,7 +65,6 @@ namespace osu.Game.Beatmaps
private readonly AudioManager audioManager;
private readonly GameHost host;
private readonly BeatmapOnlineLookupQueue onlineLookupQueue;
- private readonly Storage exportStorage;
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, AudioManager audioManager, GameHost host = null,
WorkingBeatmap defaultBeatmap = null)
@@ -83,7 +81,6 @@ namespace osu.Game.Beatmaps
beatmaps.BeatmapRestored += b => beatmapRestored.Value = new WeakReference(b);
onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
- exportStorage = storage.GetStorageForDirectory("exports");
}
protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) =>
@@ -214,26 +211,6 @@ namespace osu.Game.Beatmaps
workingCache.Remove(working);
}
- ///
- /// Exports a to an .osz package.
- ///
- /// The to export.
- public void Export(BeatmapSetInfo set)
- {
- var localSet = QueryBeatmapSet(s => s.ID == set.ID);
-
- using (var archive = ZipArchive.Create())
- {
- foreach (var file in localSet.Files)
- archive.AddEntry(file.Filename, Files.Storage.GetStream(file.FileInfo.StoragePath));
-
- using (var outputStream = exportStorage.GetStream($"{set}.osz", FileAccess.Write, FileMode.Create))
- archive.SaveTo(outputStream);
-
- exportStorage.OpenInNativeExplorer();
- }
- }
-
private readonly WeakList workingCache = new WeakList();
///
@@ -281,9 +258,9 @@ namespace osu.Game.Beatmaps
/// The first result for the provided query, or null if no results were found.
public BeatmapSetInfo QueryBeatmapSet(Expression> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query);
- protected override bool CanUndelete(BeatmapSetInfo existing, BeatmapSetInfo import)
+ protected override bool CanReuseExisting(BeatmapSetInfo existing, BeatmapSetInfo import)
{
- if (!base.CanUndelete(existing, import))
+ if (!base.CanReuseExisting(existing, import))
return false;
var existingIds = existing.Beatmaps.Select(b => b.OnlineBeatmapID).OrderBy(i => i);
@@ -433,6 +410,11 @@ namespace osu.Game.Beatmaps
return endTime - startTime;
}
+ public void Dispose()
+ {
+ onlineLookupQueue?.Dispose();
+ }
+
///
/// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation.
///
diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
index 2c79a664c5..d47d37806e 100644
--- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
+++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs
@@ -23,7 +23,7 @@ namespace osu.Game.Beatmaps
{
public partial class BeatmapManager
{
- private class BeatmapOnlineLookupQueue
+ private class BeatmapOnlineLookupQueue : IDisposable
{
private readonly IAPIProvider api;
private readonly Storage storage;
@@ -180,6 +180,11 @@ namespace osu.Game.Beatmaps
return false;
}
+ public void Dispose()
+ {
+ cacheDownloadRequest?.Dispose();
+ }
+
[Serializable]
[SuppressMessage("ReSharper", "InconsistentNaming")]
private class CachedOnlineBeatmapLookup
diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
index 7727f25967..cefb47893c 100644
--- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs
@@ -233,14 +233,14 @@ namespace osu.Game.Beatmaps.Formats
writer.Write(FormattableString.Invariant($"{(int)getObjectType(hitObject)},"));
writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(hitObject.Samples)},"));
- if (hitObject is IHasCurve curveData)
+ if (hitObject is IHasPath path)
{
- addCurveData(writer, curveData, position);
+ addPathData(writer, path, position);
writer.Write(getSampleBank(hitObject.Samples, zeroBanks: true));
}
else
{
- if (hitObject is IHasEndTime)
+ if (hitObject is IHasDuration)
addEndTimeData(writer, hitObject);
writer.Write(getSampleBank(hitObject.Samples));
@@ -263,11 +263,11 @@ namespace osu.Game.Beatmaps.Formats
switch (hitObject)
{
- case IHasCurve _:
+ case IHasPath _:
type |= LegacyHitObjectType.Slider;
break;
- case IHasEndTime _:
+ case IHasDuration _:
if (beatmap.BeatmapInfo.RulesetID == 3)
type |= LegacyHitObjectType.Hold;
else
@@ -282,13 +282,13 @@ namespace osu.Game.Beatmaps.Formats
return type;
}
- private void addCurveData(TextWriter writer, IHasCurve curveData, Vector2 position)
+ private void addPathData(TextWriter writer, IHasPath pathData, Vector2 position)
{
PathType? lastType = null;
- for (int i = 0; i < curveData.Path.ControlPoints.Count; i++)
+ for (int i = 0; i < pathData.Path.ControlPoints.Count; i++)
{
- PathControlPoint point = curveData.Path.ControlPoints[i];
+ PathControlPoint point = pathData.Path.ControlPoints[i];
if (point.Type.Value != null)
{
@@ -325,29 +325,34 @@ namespace osu.Game.Beatmaps.Formats
if (i != 0)
{
writer.Write(FormattableString.Invariant($"{position.X + point.Position.Value.X}:{position.Y + point.Position.Value.Y}"));
- writer.Write(i != curveData.Path.ControlPoints.Count - 1 ? "|" : ",");
+ writer.Write(i != pathData.Path.ControlPoints.Count - 1 ? "|" : ",");
}
}
- writer.Write(FormattableString.Invariant($"{curveData.RepeatCount + 1},"));
- writer.Write(FormattableString.Invariant($"{curveData.Path.Distance},"));
+ var curveData = pathData as IHasPathWithRepeats;
- for (int i = 0; i < curveData.NodeSamples.Count; i++)
- {
- writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(curveData.NodeSamples[i])}"));
- writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ",");
- }
+ writer.Write(FormattableString.Invariant($"{(curveData?.RepeatCount ?? 0) + 1},"));
+ writer.Write(FormattableString.Invariant($"{pathData.Path.Distance},"));
- for (int i = 0; i < curveData.NodeSamples.Count; i++)
+ if (curveData != null)
{
- writer.Write(getSampleBank(curveData.NodeSamples[i], true));
- writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ",");
+ for (int i = 0; i < curveData.NodeSamples.Count; i++)
+ {
+ writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(curveData.NodeSamples[i])}"));
+ writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ",");
+ }
+
+ for (int i = 0; i < curveData.NodeSamples.Count; i++)
+ {
+ writer.Write(getSampleBank(curveData.NodeSamples[i], true));
+ writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ",");
+ }
}
}
private void addEndTimeData(TextWriter writer, HitObject hitObject)
{
- var endTimeData = (IHasEndTime)hitObject;
+ var endTimeData = (IHasDuration)hitObject;
var type = getObjectType(hitObject);
char suffix = ',';
diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs
index bf2b9944a4..ac399e37c4 100644
--- a/osu.Game/Beatmaps/WorkingBeatmap.cs
+++ b/osu.Game/Beatmaps/WorkingBeatmap.cs
@@ -63,7 +63,7 @@ namespace osu.Game.Beatmaps
length = emptyLength;
break;
- case IHasEndTime endTime:
+ case IHasDuration endTime:
length = endTime.EndTime + excess_length;
break;
@@ -129,12 +129,19 @@ namespace osu.Game.Beatmaps
processor?.PreProcess();
// Compute default values for hitobjects, including creating nested hitobjects in-case they're needed
- foreach (var obj in converted.HitObjects)
+ try
{
- if (cancellationSource.IsCancellationRequested)
- throw new BeatmapLoadTimeoutException(BeatmapInfo);
+ foreach (var obj in converted.HitObjects)
+ {
+ if (cancellationSource.IsCancellationRequested)
+ throw new BeatmapLoadTimeoutException(BeatmapInfo);
- obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty);
+ obj.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty, cancellationSource.Token);
+ }
+ }
+ catch (OperationCanceledException)
+ {
+ throw new BeatmapLoadTimeoutException(BeatmapInfo);
}
foreach (var mod in mods.OfType())
diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
index 33b16cbaaf..0fe8dd1268 100644
--- a/osu.Game/Database/ArchiveModelManager.cs
+++ b/osu.Game/Database/ArchiveModelManager.cs
@@ -22,6 +22,7 @@ using osu.Game.IO.Archives;
using osu.Game.IPC;
using osu.Game.Overlays.Notifications;
using osu.Game.Utils;
+using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using FileInfo = osu.Game.IO.FileInfo;
@@ -54,12 +55,12 @@ namespace osu.Game.Database
public Action PostNotification { protected get; set; }
///
- /// Fired when a new becomes available in the database.
+ /// Fired when a new or updated becomes available in the database.
/// This is not guaranteed to run on the update thread.
///
- public IBindable> ItemAdded => itemAdded;
+ public IBindable> ItemUpdated => itemUpdated;
- private readonly Bindable> itemAdded = new Bindable>();
+ private readonly Bindable> itemUpdated = new Bindable>();
///
/// Fired when a is removed from the database.
@@ -82,14 +83,18 @@ namespace osu.Game.Database
// ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised)
private ArchiveImportIPCChannel ipc;
+ private readonly Storage exportStorage;
+
protected ArchiveModelManager(Storage storage, IDatabaseContextFactory contextFactory, MutableDatabaseBackedStoreWithFileIncludes modelStore, IIpcHost importHost = null)
{
ContextFactory = contextFactory;
ModelStore = modelStore;
- ModelStore.ItemAdded += item => handleEvent(() => itemAdded.Value = new WeakReference(item));
+ ModelStore.ItemUpdated += item => handleEvent(() => itemUpdated.Value = new WeakReference(item));
ModelStore.ItemRemoved += item => handleEvent(() => itemRemoved.Value = new WeakReference(item));
+ exportStorage = storage.GetStorageForDirectory("exports");
+
Files = new FileStore(contextFactory, storage);
if (importHost != null)
@@ -271,7 +276,7 @@ namespace osu.Game.Database
// for now, concatenate all .osu files in the set to create a unique hash.
MemoryStream hashable = new MemoryStream();
- foreach (TFileModel file in item.Files.Where(f => HashableFileTypes.Any(f.Filename.EndsWith)))
+ foreach (TFileModel file in item.Files.Where(f => HashableFileTypes.Any(f.Filename.EndsWith)).OrderBy(f => f.Filename))
{
using (Stream s = Files.Store.GetStream(file.FileInfo.StoragePath))
s.CopyTo(hashable);
@@ -327,7 +332,7 @@ namespace osu.Game.Database
if (existing != null)
{
- if (CanUndelete(existing, item))
+ if (CanReuseExisting(existing, item))
{
Undelete(existing);
LogForModel(item, $"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import.");
@@ -369,6 +374,29 @@ namespace osu.Game.Database
return item;
}, cancellationToken, TaskCreationOptions.HideScheduler, import_scheduler).Unwrap();
+ ///
+ /// Exports an item to a legacy (.zip based) package.
+ ///
+ /// The item to export.
+ public void Export(TModel item)
+ {
+ var retrievedItem = ModelStore.ConsumableItems.FirstOrDefault(s => s.ID == item.ID);
+
+ if (retrievedItem == null)
+ throw new ArgumentException("Specified model could not be found", nameof(item));
+
+ using (var archive = ZipArchive.Create())
+ {
+ foreach (var file in retrievedItem.Files)
+ archive.AddEntry(file.Filename, Files.Storage.GetStream(file.FileInfo.StoragePath));
+
+ using (var outputStream = exportStorage.GetStream($"{getValidFilename(item.ToString())}{HandledExtensions.First()}", FileAccess.Write, FileMode.Create))
+ archive.SaveTo(outputStream);
+
+ exportStorage.OpenInNativeExplorer();
+ }
+ }
+
public void UpdateFile(TModel model, TFileModel file, Stream contents)
{
using (var usage = ContextFactory.GetForWrite())
@@ -632,13 +660,29 @@ namespace osu.Game.Database
protected TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash);
///
- /// After an existing is found during an import process, the default behaviour is to restore the existing
+ /// After an existing is found during an import process, the default behaviour is to use/restore the existing
/// item and skip the import. This method allows changing that behaviour.
///
/// The existing model.
/// The newly imported model.
/// Whether the existing model should be restored and used. Returning false will delete the existing and force a re-import.
- protected virtual bool CanUndelete(TModel existing, TModel import) => true;
+ protected virtual bool CanReuseExisting(TModel existing, TModel import) =>
+ // for the best or worst, we copy and import files of a new import before checking whether
+ // it is a duplicate. so to check if anything has changed, we can just compare all FileInfo IDs.
+ getIDs(existing.Files).SequenceEqual(getIDs(import.Files)) &&
+ getFilenames(existing.Files).SequenceEqual(getFilenames(import.Files));
+
+ private IEnumerable getIDs(List files)
+ {
+ foreach (var f in files.OrderBy(f => f.Filename))
+ yield return f.FileInfo.ID;
+ }
+
+ private IEnumerable getFilenames(List files)
+ {
+ foreach (var f in files.OrderBy(f => f.Filename))
+ yield return f.Filename;
+ }
private DbSet queryModel() => ContextFactory.Get().Set();
@@ -710,5 +754,12 @@ namespace osu.Game.Database
}
#endregion
+
+ private string getValidFilename(string filename)
+ {
+ foreach (char c in Path.GetInvalidFileNameChars())
+ filename = filename.Replace(c, '_');
+ return filename;
+ }
}
}
diff --git a/osu.Game/Database/IModelManager.cs b/osu.Game/Database/IModelManager.cs
index 852b385798..7f7e5565f1 100644
--- a/osu.Game/Database/IModelManager.cs
+++ b/osu.Game/Database/IModelManager.cs
@@ -13,7 +13,7 @@ namespace osu.Game.Database
public interface IModelManager
where TModel : class
{
- IBindable> ItemAdded { get; }
+ IBindable> ItemUpdated { get; }
IBindable> ItemRemoved { get; }
}
diff --git a/osu.Game/Database/MutableDatabaseBackedStore.cs b/osu.Game/Database/MutableDatabaseBackedStore.cs
index 4ca1eef989..c9d0c4bc41 100644
--- a/osu.Game/Database/MutableDatabaseBackedStore.cs
+++ b/osu.Game/Database/MutableDatabaseBackedStore.cs
@@ -16,7 +16,14 @@ namespace osu.Game.Database
public abstract class MutableDatabaseBackedStore : DatabaseBackedStore
where T : class, IHasPrimaryKey, ISoftDelete
{
- public event Action ItemAdded;
+ ///
+ /// Fired when an item was added or updated.
+ ///
+ public event Action ItemUpdated;
+
+ ///
+ /// Fired when an item was removed.
+ ///
public event Action ItemRemoved;
protected MutableDatabaseBackedStore(IDatabaseContextFactory contextFactory, Storage storage = null)
@@ -41,7 +48,7 @@ namespace osu.Game.Database
context.Attach(item);
}
- ItemAdded?.Invoke(item);
+ ItemUpdated?.Invoke(item);
}
///
@@ -53,8 +60,7 @@ namespace osu.Game.Database
using (var usage = ContextFactory.GetForWrite())
usage.Context.Update(item);
- ItemRemoved?.Invoke(item);
- ItemAdded?.Invoke(item);
+ ItemUpdated?.Invoke(item);
}
///
@@ -91,7 +97,7 @@ namespace osu.Game.Database
item.DeletePending = false;
}
- ItemAdded?.Invoke(item);
+ ItemUpdated?.Invoke(item);
return true;
}
diff --git a/osu.Game/Graphics/UserInterface/TernaryStateMenuItem.cs b/osu.Game/Graphics/UserInterface/TernaryStateMenuItem.cs
index 2d9e2106d4..acf4065f49 100644
--- a/osu.Game/Graphics/UserInterface/TernaryStateMenuItem.cs
+++ b/osu.Game/Graphics/UserInterface/TernaryStateMenuItem.cs
@@ -11,23 +11,13 @@ namespace osu.Game.Graphics.UserInterface
///
public class TernaryStateMenuItem : StatefulMenuItem
{
- ///
- /// Creates a new .
- ///
- /// The text to display.
- /// The type of action which this performs.
- public TernaryStateMenuItem(string text, MenuItemType type = MenuItemType.Standard)
- : this(text, type, null)
- {
- }
-
///
/// Creates a new .
///
/// The text to display.
/// The type of action which this performs.
/// A delegate to be invoked when this is pressed.
- public TernaryStateMenuItem(string text, MenuItemType type, Action action)
+ public TernaryStateMenuItem(string text, MenuItemType type = MenuItemType.Standard, Action action = null)
: this(text, getNextState, type, action)
{
}
diff --git a/osu.Game/IO/WrappedStorage.cs b/osu.Game/IO/WrappedStorage.cs
index 646faba9eb..1dd3afbfae 100644
--- a/osu.Game/IO/WrappedStorage.cs
+++ b/osu.Game/IO/WrappedStorage.cs
@@ -69,7 +69,7 @@ namespace osu.Game.IO
public override void DeleteDatabase(string name) => UnderlyingStorage.DeleteDatabase(MutatePath(name));
- public override void OpenInNativeExplorer() => UnderlyingStorage.OpenInNativeExplorer();
+ public override void OpenPathInNativeExplorer(string path) => UnderlyingStorage.OpenPathInNativeExplorer(MutatePath(path));
public override Storage GetStorageForDirectory(string path)
{
diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
index 0c3272c7de..dde45b5aeb 100644
--- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
+++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs
@@ -27,7 +27,15 @@ namespace osu.Game.Online.API.Requests
private string directionString => SortDirection == SortDirection.Descending ? @"desc" : @"asc";
- public SearchBeatmapSetsRequest(string query, RulesetInfo ruleset, Cursor cursor = null, SearchCategory searchCategory = SearchCategory.Any, SortCriteria sortCriteria = SortCriteria.Ranked, SortDirection sortDirection = SortDirection.Descending)
+ public SearchBeatmapSetsRequest(
+ string query,
+ RulesetInfo ruleset,
+ Cursor cursor = null,
+ SearchCategory searchCategory = SearchCategory.Any,
+ SortCriteria sortCriteria = SortCriteria.Ranked,
+ SortDirection sortDirection = SortDirection.Descending,
+ SearchGenre genre = SearchGenre.Any,
+ SearchLanguage language = SearchLanguage.Any)
{
this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query);
this.ruleset = ruleset;
@@ -36,8 +44,8 @@ namespace osu.Game.Online.API.Requests
SearchCategory = searchCategory;
SortCriteria = sortCriteria;
SortDirection = sortDirection;
- Genre = SearchGenre.Any;
- Language = SearchLanguage.Any;
+ Genre = genre;
+ Language = language;
}
protected override WebRequest CreateWebRequest()
diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs
index 53872ddcba..b17e0812da 100644
--- a/osu.Game/Online/Chat/ChannelManager.cs
+++ b/osu.Game/Online/Chat/ChannelManager.cs
@@ -93,12 +93,6 @@ namespace osu.Game.Online.Chat
{
if (!(e.NewValue is ChannelSelectorTabItem.ChannelSelectorTabChannel))
JoinChannel(e.NewValue);
-
- if (e.NewValue?.MessagesLoaded == false)
- {
- // let's fetch a small number of messages to bring us up-to-date with the backlog.
- fetchInitalMessages(e.NewValue);
- }
}
///
@@ -240,7 +234,6 @@ namespace osu.Game.Online.Chat
}
JoinChannel(channel);
- CurrentChannel.Value = channel;
break;
case "help":
@@ -275,7 +268,7 @@ namespace osu.Game.Online.Chat
// join any channels classified as "defaults"
if (joinDefaults && defaultChannels.Any(c => c.Equals(channel.Name, StringComparison.OrdinalIgnoreCase)))
- JoinChannel(ch);
+ joinChannel(ch);
}
};
req.Failure += error =>
@@ -296,7 +289,7 @@ namespace osu.Game.Online.Chat
/// The channel
private void fetchInitalMessages(Channel channel)
{
- if (channel.Id <= 0) return;
+ if (channel.Id <= 0 || channel.MessagesLoaded) return;
var fetchInitialMsgReq = new GetMessagesRequest(channel);
fetchInitialMsgReq.Success += messages =>
@@ -351,9 +344,10 @@ namespace osu.Game.Online.Chat
/// Joins a channel if it has not already been joined.
///
/// The channel to join.
- /// Whether the channel has already been joined server-side. Will skip a join request.
/// The joined channel. Note that this may not match the parameter channel as it is a backed object.
- public Channel JoinChannel(Channel channel, bool alreadyJoined = false)
+ public Channel JoinChannel(Channel channel) => joinChannel(channel, true);
+
+ private Channel joinChannel(Channel channel, bool fetchInitialMessages = false)
{
if (channel == null) return null;
@@ -362,21 +356,36 @@ namespace osu.Game.Online.Chat
// ensure we are joined to the channel
if (!channel.Joined.Value)
{
- if (alreadyJoined)
- channel.Joined.Value = true;
- else
+ switch (channel.Type)
{
- switch (channel.Type)
- {
- case ChannelType.Public:
- var req = new JoinChannelRequest(channel, api.LocalUser.Value);
- req.Success += () => JoinChannel(channel, true);
- req.Failure += ex => LeaveChannel(channel);
- api.Queue(req);
- return channel;
- }
+ case ChannelType.Multiplayer:
+ // join is implicit. happens when you join a multiplayer game.
+ // this will probably change in the future.
+ channel.Joined.Value = true;
+ joinChannel(channel, fetchInitialMessages);
+ return channel;
+
+ case ChannelType.Private:
+ // can't do this yet.
+ break;
+
+ default:
+ var req = new JoinChannelRequest(channel, api.LocalUser.Value);
+ req.Success += () =>
+ {
+ channel.Joined.Value = true;
+ joinChannel(channel, fetchInitialMessages);
+ };
+ req.Failure += ex => LeaveChannel(channel);
+ api.Queue(req);
+ return channel;
}
}
+ else
+ {
+ if (fetchInitialMessages)
+ fetchInitalMessages(channel);
+ }
if (CurrentChannel.Value == null)
CurrentChannel.Value = channel;
@@ -420,7 +429,8 @@ namespace osu.Game.Online.Chat
foreach (var channel in updates.Presence)
{
// we received this from the server so should mark the channel already joined.
- JoinChannel(channel, true);
+ channel.Joined.Value = true;
+ joinChannel(channel);
}
//todo: handle left channels
diff --git a/osu.Game/Online/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs
index 47de7d75ed..5d9cf612bb 100644
--- a/osu.Game/Online/DownloadTrackingComposite.cs
+++ b/osu.Game/Online/DownloadTrackingComposite.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Online
Model.Value = model;
}
- private IBindable> managerAdded;
+ private IBindable> managedUpdated;
private IBindable> managerRemoved;
private IBindable>> managerDownloadBegan;
private IBindable>> managerDownloadFailed;
@@ -56,8 +56,8 @@ namespace osu.Game.Online
managerDownloadBegan.BindValueChanged(downloadBegan);
managerDownloadFailed = manager.DownloadFailed.GetBoundCopy();
managerDownloadFailed.BindValueChanged(downloadFailed);
- managerAdded = manager.ItemAdded.GetBoundCopy();
- managerAdded.BindValueChanged(itemAdded);
+ managedUpdated = manager.ItemUpdated.GetBoundCopy();
+ managedUpdated.BindValueChanged(itemUpdated);
managerRemoved = manager.ItemRemoved.GetBoundCopy();
managerRemoved.BindValueChanged(itemRemoved);
}
@@ -128,7 +128,7 @@ namespace osu.Game.Online
private void onRequestFailure(Exception e) => Schedule(() => attachDownload(null));
- private void itemAdded(ValueChangedEvent> weakItem)
+ private void itemUpdated(ValueChangedEvent> weakItem)
{
if (weakItem.NewValue.TryGetTarget(out var item))
setDownloadStateFromManager(item, DownloadState.LocallyAvailable);
diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs
index c367c3b636..5e44562144 100644
--- a/osu.Game/OsuGameBase.cs
+++ b/osu.Game/OsuGameBase.cs
@@ -192,7 +192,7 @@ namespace osu.Game
ScoreManager.Delete(getBeatmapScores(item), true);
});
- BeatmapManager.ItemAdded.BindValueChanged(i =>
+ BeatmapManager.ItemUpdated.BindValueChanged(i =>
{
if (i.NewValue.TryGetTarget(out var item))
ScoreManager.Undelete(getBeatmapScores(item), true);
@@ -229,8 +229,8 @@ namespace osu.Game
FileStore.Cleanup();
- if (API is APIAccess apiAcces)
- AddInternal(apiAcces);
+ if (API is APIAccess apiAccess)
+ AddInternal(apiAccess);
AddInternal(RulesetConfigCache);
GlobalActionContainer globalBinding;
@@ -337,6 +337,7 @@ namespace osu.Game
{
base.Dispose(isDisposing);
RulesetStore?.Dispose();
+ BeatmapManager?.Dispose();
contextFactory.FlushConnections();
}
diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
index 41c99d5d03..494a0df8f8 100644
--- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
+++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs
@@ -177,7 +177,9 @@ namespace osu.Game.Overlays.BeatmapListing
lastResponse?.Cursor,
searchControl.Category.Value,
sortControl.Current.Value,
- sortControl.SortDirection.Value);
+ sortControl.SortDirection.Value,
+ searchControl.Genre.Value,
+ searchControl.Language.Value);
getSetsRequest.Success += response =>
{
@@ -186,6 +188,9 @@ namespace osu.Game.Overlays.BeatmapListing
if (sets.Count == 0)
noMoreResults = true;
+ if (CurrentPage == 0)
+ searchControl.BeatmapSet = sets.FirstOrDefault();
+
lastResponse = response;
getSetsRequest = null;
diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs
index 01d5991d3e..eafb4572ca 100644
--- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs
+++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs
@@ -274,6 +274,9 @@ namespace osu.Game.Overlays.KeyBinding
private void clear()
{
+ if (bindTarget == null)
+ return;
+
bindTarget.UpdateKeyCombination(InputKey.None);
finalise();
}
@@ -333,7 +336,7 @@ namespace osu.Game.Overlays.KeyBinding
}
}
- private class ClearButton : TriangleButton
+ public class ClearButton : TriangleButton
{
public ClearButton()
{
diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs
index 35f3cb0e25..92cf490be2 100644
--- a/osu.Game/Overlays/MusicController.cs
+++ b/osu.Game/Overlays/MusicController.cs
@@ -60,14 +60,14 @@ namespace osu.Game.Overlays
[Resolved(canBeNull: true)]
private OnScreenDisplay onScreenDisplay { get; set; }
- private IBindable> managerAdded;
+ private IBindable> managerUpdated;
private IBindable> managerRemoved;
[BackgroundDependencyLoader]
private void load()
{
- managerAdded = beatmaps.ItemAdded.GetBoundCopy();
- managerAdded.BindValueChanged(beatmapAdded);
+ managerUpdated = beatmaps.ItemUpdated.GetBoundCopy();
+ managerUpdated.BindValueChanged(beatmapUpdated);
managerRemoved = beatmaps.ItemRemoved.GetBoundCopy();
managerRemoved.BindValueChanged(beatmapRemoved);
@@ -98,14 +98,14 @@ namespace osu.Game.Overlays
///
public bool IsPlaying => current?.Track.IsRunning ?? false;
- private void beatmapAdded(ValueChangedEvent> weakSet)
+ private void beatmapUpdated(ValueChangedEvent> weakSet)
{
if (weakSet.NewValue.TryGetTarget(out var set))
{
Schedule(() =>
{
- if (!beatmapSets.Contains(set))
- beatmapSets.Add(set);
+ beatmapSets.Remove(set);
+ beatmapSets.Add(set);
});
}
}
diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
index 94080f5592..04390a1193 100644
--- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs
@@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
+using osu.Framework.Logging;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Skinning;
@@ -31,16 +32,18 @@ namespace osu.Game.Overlays.Settings.Sections
[Resolved]
private SkinManager skins { get; set; }
- private IBindable> managerAdded;
+ private IBindable> managerUpdated;
private IBindable> managerRemoved;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
FlowContent.Spacing = new Vector2(0, 5);
+
Children = new Drawable[]
{
skinDropdown = new SkinSettingsDropdown(),
+ new ExportSkinButton(),
new SettingsSlider
{
LabelText = "Menu cursor size",
@@ -70,8 +73,8 @@ namespace osu.Game.Overlays.Settings.Sections
},
};
- managerAdded = skins.ItemAdded.GetBoundCopy();
- managerAdded.BindValueChanged(itemAdded);
+ managerUpdated = skins.ItemUpdated.GetBoundCopy();
+ managerUpdated.BindValueChanged(itemUpdated);
managerRemoved = skins.ItemRemoved.GetBoundCopy();
managerRemoved.BindValueChanged(itemRemoved);
@@ -89,10 +92,10 @@ namespace osu.Game.Overlays.Settings.Sections
dropdownBindable.BindValueChanged(skin => configBindable.Value = skin.NewValue.ID);
}
- private void itemAdded(ValueChangedEvent> weakItem)
+ private void itemUpdated(ValueChangedEvent> weakItem)
{
if (weakItem.NewValue.TryGetTarget(out var item))
- Schedule(() => skinDropdown.Items = skinDropdown.Items.Append(item).ToArray());
+ Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => !i.Equals(item)).Append(item).ToArray());
}
private void itemRemoved(ValueChangedEvent> weakItem)
@@ -117,5 +120,35 @@ namespace osu.Game.Overlays.Settings.Sections
protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = 200);
}
}
+
+ private class ExportSkinButton : SettingsButton
+ {
+ [Resolved]
+ private SkinManager skins { get; set; }
+
+ private Bindable currentSkin;
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Text = "Export selected skin";
+ Action = export;
+
+ currentSkin = skins.CurrentSkin.GetBoundCopy();
+ currentSkin.BindValueChanged(skin => Enabled.Value = skin.NewValue.SkinInfo.ID > 0, true);
+ }
+
+ private void export()
+ {
+ try
+ {
+ skins.Export(currentSkin.Value.SkinInfo);
+ }
+ catch (Exception e)
+ {
+ Logger.Log($"Could not export current skin: {e.Message}", level: LogLevel.Error);
+ }
+ }
+ }
}
}
diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
index 67216b019d..c25fb03fd0 100644
--- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs
+++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs
@@ -3,15 +3,14 @@
using System;
using System.Collections.Generic;
+using System.Collections.Specialized;
using System.Linq;
-using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Logging;
-using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Configuration;
@@ -20,6 +19,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
+using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osu.Game.Screens.Edit.Compose;
@@ -38,23 +38,19 @@ namespace osu.Game.Rulesets.Edit
protected readonly Ruleset Ruleset;
[Resolved]
- protected IFrameBasedClock EditorClock { get; private set; }
+ protected EditorClock EditorClock { get; private set; }
[Resolved]
protected EditorBeatmap EditorBeatmap { get; private set; }
[Resolved]
- private IAdjustableClock adjustableClock { get; set; }
-
- [Resolved]
- private IBeatSnapProvider beatSnapProvider { get; set; }
+ protected IBeatSnapProvider BeatSnapProvider { get; private set; }
protected ComposeBlueprintContainer BlueprintContainer { get; private set; }
private DrawableEditRulesetWrapper drawableRulesetWrapper;
- private Container distanceSnapGridContainer;
- private DistanceSnapGrid distanceSnapGrid;
- private readonly List layerContainers = new List();
+
+ protected readonly Container LayerBelowRuleset = new Container { RelativeSizeAxes = Axes.Both };
private InputManager inputManager;
@@ -63,11 +59,10 @@ namespace osu.Game.Rulesets.Edit
protected HitObjectComposer(Ruleset ruleset)
{
Ruleset = ruleset;
- RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
- private void load(IFrameBasedClock framedClock)
+ private void load()
{
Config = Dependencies.Get().GetConfigFor(Ruleset);
@@ -75,7 +70,7 @@ namespace osu.Game.Rulesets.Edit
{
drawableRulesetWrapper = new DrawableEditRulesetWrapper(CreateDrawableRuleset(Ruleset, EditorBeatmap.PlayableBeatmap))
{
- Clock = framedClock,
+ Clock = EditorClock,
ProcessCustomClock = false
};
}
@@ -85,17 +80,6 @@ namespace osu.Game.Rulesets.Edit
return;
}
- var layerBelowRuleset = drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChildren(new Drawable[]
- {
- distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both },
- new EditorPlayfieldBorder { RelativeSizeAxes = Axes.Both }
- });
-
- var layerAboveRuleset = drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChild(BlueprintContainer = CreateBlueprintContainer());
-
- layerContainers.Add(layerBelowRuleset);
- layerContainers.Add(layerAboveRuleset);
-
InternalChild = new GridContainer
{
RelativeSizeAxes = Axes.Both,
@@ -117,11 +101,19 @@ namespace osu.Game.Rulesets.Edit
{
Name = "Content",
RelativeSizeAxes = Axes.Both,
+ Masking = true,
Children = new Drawable[]
{
- layerBelowRuleset,
+ // layers below playfield
+ drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChildren(new Drawable[]
+ {
+ LayerBelowRuleset,
+ new EditorPlayfieldBorder { RelativeSizeAxes = Axes.Both }
+ }),
drawableRulesetWrapper,
- layerAboveRuleset
+ // layers above playfield
+ drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer()
+ .WithChild(BlueprintContainer = CreateBlueprintContainer(HitObjects))
}
}
},
@@ -139,9 +131,54 @@ namespace osu.Game.Rulesets.Edit
setSelectTool();
- BlueprintContainer.SelectionChanged += selectionChanged;
+ EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged;
}
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ inputManager = GetContainingInputManager();
+ }
+
+ public override Playfield Playfield => drawableRulesetWrapper.Playfield;
+
+ public override IEnumerable HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects;
+
+ public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position);
+
+ ///
+ /// Defines all available composition tools, listed on the left side of the editor screen as button controls.
+ /// This should usually define one tool for each type used in the target ruleset.
+ ///
+ ///
+ /// A "select" tool is automatically added as the first tool.
+ ///
+ protected abstract IReadOnlyList CompositionTools { get; }
+
+ ///
+ /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic.
+ ///
+ /// A live collection of all s in the editor beatmap.
+ protected virtual ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects)
+ => new ComposeBlueprintContainer(hitObjects);
+
+ ///
+ /// Construct a drawable ruleset for the provided ruleset.
+ ///
+ ///
+ /// Can be overridden to add editor-specific logical changes to a 's standard .
+ /// For example, hit animations or judgement logic may be changed to give a better editor user experience.
+ ///
+ /// The ruleset used to construct its drawable counterpart.
+ /// The loaded beatmap.
+ /// The mods to be applied.
+ /// An editor-relevant .
+ protected virtual DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null)
+ => (DrawableRuleset)ruleset.CreateDrawableRulesetWith(beatmap, mods);
+
+ #region Tool selection logic
+
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Key >= Key.Number1 && e.Key <= Key.Number9)
@@ -158,49 +195,13 @@ namespace osu.Game.Rulesets.Edit
return base.OnKeyDown(e);
}
- protected override void LoadComplete()
+ private void selectionChanged(object sender, NotifyCollectionChangedEventArgs changedArgs)
{
- base.LoadComplete();
-
- inputManager = GetContainingInputManager();
- }
-
- private double lastGridUpdateTime;
-
- protected override void Update()
- {
- base.Update();
-
- if (EditorClock.CurrentTime != lastGridUpdateTime && !(BlueprintContainer.CurrentTool is SelectTool))
- showGridFor(Enumerable.Empty());
- }
-
- protected override void UpdateAfterChildren()
- {
- base.UpdateAfterChildren();
-
- layerContainers.ForEach(l =>
- {
- l.Anchor = drawableRulesetWrapper.Playfield.Anchor;
- l.Origin = drawableRulesetWrapper.Playfield.Origin;
- l.Position = drawableRulesetWrapper.Playfield.Position;
- l.Size = drawableRulesetWrapper.Playfield.Size;
- });
- }
-
- private void selectionChanged(IEnumerable selectedHitObjects)
- {
- var hitObjects = selectedHitObjects.ToArray();
-
- if (hitObjects.Any())
+ if (EditorBeatmap.SelectedHitObjects.Any())
{
// ensure in selection mode if a selection is made.
setSelectTool();
-
- showGridFor(hitObjects);
}
- else
- distanceSnapGridContainer.Hide();
}
private void setSelectTool() => toolboxCollection.Items.First().Select();
@@ -209,44 +210,17 @@ namespace osu.Game.Rulesets.Edit
{
BlueprintContainer.CurrentTool = tool;
- if (tool is SelectTool)
- distanceSnapGridContainer.Hide();
- else
- {
+ if (!(tool is SelectTool))
EditorBeatmap.SelectedHitObjects.Clear();
- showGridFor(Enumerable.Empty());
- }
}
- private void showGridFor(IEnumerable selectedHitObjects)
- {
- distanceSnapGridContainer.Clear();
- distanceSnapGrid = CreateDistanceSnapGrid(selectedHitObjects);
+ #endregion
- if (distanceSnapGrid != null)
- {
- distanceSnapGridContainer.Child = distanceSnapGrid;
- distanceSnapGridContainer.Show();
- }
-
- lastGridUpdateTime = EditorClock.CurrentTime;
- }
-
- public override IEnumerable HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects;
- public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position);
-
- protected abstract IReadOnlyList CompositionTools { get; }
-
- protected abstract ComposeBlueprintContainer CreateBlueprintContainer();
-
- protected abstract DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null);
+ #region IPlacementHandler
public void BeginPlacement(HitObject hitObject)
{
EditorBeatmap.PlacementObject.Value = hitObject;
-
- if (distanceSnapGrid != null)
- hitObject.StartTime = GetSnappedPosition(distanceSnapGrid.ToLocalSpace(inputManager.CurrentState.Mouse.Position), hitObject.StartTime).time;
}
public void EndPlacement(HitObject hitObject, bool commit)
@@ -257,57 +231,96 @@ namespace osu.Game.Rulesets.Edit
{
EditorBeatmap.Add(hitObject);
- if (adjustableClock.CurrentTime < hitObject.StartTime)
- adjustableClock.Seek(hitObject.StartTime);
+ if (EditorClock.CurrentTime < hitObject.StartTime)
+ EditorClock.SeekTo(hitObject.StartTime);
}
-
- showGridFor(Enumerable.Empty());
}
public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject);
- public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) => distanceSnapGrid?.GetSnappedPosition(position) ?? (position, time);
+ #endregion
+
+ #region IPositionSnapProvider
+
+ ///
+ /// Retrieve the relevant at a specified screen-space position.
+ /// In cases where a ruleset doesn't require custom logic (due to nested playfields, for example)
+ /// this will return the ruleset's main playfield.
+ ///
+ /// The screen-space position to query.
+ /// The most relevant .
+ protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield;
+
+ public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
+ {
+ var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
+ double? targetTime = null;
+
+ if (playfield is ScrollingPlayfield scrollingPlayfield)
+ {
+ targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition);
+
+ // apply beat snapping
+ targetTime = BeatSnapProvider.SnapTime(targetTime.Value);
+
+ // convert back to screen space
+ screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value);
+ }
+
+ return new SnapResult(screenSpacePosition, targetTime, playfield);
+ }
public override float GetBeatSnapDistanceAt(double referenceTime)
{
DifficultyControlPoint difficultyPoint = EditorBeatmap.ControlPointInfo.DifficultyPointAt(referenceTime);
- return (float)(100 * EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / beatSnapProvider.BeatDivisor);
+ return (float)(100 * EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / BeatSnapProvider.BeatDivisor);
}
public override float DurationToDistance(double referenceTime, double duration)
{
- double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceTime);
+ double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime);
return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceTime));
}
public override double DistanceToDuration(double referenceTime, float distance)
{
- double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceTime);
+ double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime);
return distance / GetBeatSnapDistanceAt(referenceTime) * beatLength;
}
public override double GetSnappedDurationFromDistance(double referenceTime, float distance)
- => beatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime) - referenceTime;
+ => BeatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime) - referenceTime;
public override float GetSnappedDistanceFromDistance(double referenceTime, float distance)
{
- var snappedEndTime = beatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime);
+ var snappedEndTime = BeatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime);
return DurationToDistance(referenceTime, snappedEndTime - referenceTime);
}
+
+ #endregion
}
+ ///
+ /// A non-generic definition of a HitObject composer class.
+ /// Generally used to access certain methods without requiring a generic type for .
+ ///
[Cached(typeof(HitObjectComposer))]
- [Cached(typeof(IDistanceSnapProvider))]
- public abstract class HitObjectComposer : CompositeDrawable, IDistanceSnapProvider
+ [Cached(typeof(IPositionSnapProvider))]
+ public abstract class HitObjectComposer : CompositeDrawable, IPositionSnapProvider
{
- internal HitObjectComposer()
+ protected HitObjectComposer()
{
RelativeSizeAxes = Axes.Both;
}
///
- /// All the s.
+ /// The target ruleset's playfield.
+ ///
+ public abstract Playfield Playfield { get; }
+
+ ///
+ /// All s in currently loaded beatmap.
///
public abstract IEnumerable HitObjects { get; }
@@ -316,15 +329,9 @@ namespace osu.Game.Rulesets.Edit
///
public abstract bool CursorInPlacementArea { get; }
- ///
- /// Creates the applicable for a selection.
- ///
- /// The selection.
- /// The for . If empty, a grid is returned for the current point in time.
- [CanBeNull]
- protected virtual DistanceSnapGrid CreateDistanceSnapGrid([NotNull] IEnumerable selectedHitObjects) => null;
+ #region IPositionSnapProvider
- public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time);
+ public abstract SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition);
public abstract float GetBeatSnapDistanceAt(double referenceTime);
@@ -335,5 +342,7 @@ namespace osu.Game.Rulesets.Edit
public abstract double GetSnappedDurationFromDistance(double referenceTime, float distance);
public abstract float GetSnappedDistanceFromDistance(double referenceTime, float distance);
+
+ #endregion
}
}
diff --git a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs
similarity index 87%
rename from osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs
rename to osu.Game/Rulesets/Edit/IPositionSnapProvider.cs
index c6e61f68da..c854c06031 100644
--- a/osu.Game/Rulesets/Edit/IDistanceSnapProvider.cs
+++ b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs
@@ -5,9 +5,14 @@ using osuTK;
namespace osu.Game.Rulesets.Edit
{
- public interface IDistanceSnapProvider
+ public interface IPositionSnapProvider
{
- (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time);
+ ///
+ /// Given a position, find a valid time snap.
+ ///
+ /// The screen-space position to be snapped.
+ /// The time and position post-snapping.
+ SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition);
///
/// Retrieves the distance between two points within a timing point that are one beat length apart.
diff --git a/osu.Game/Rulesets/Edit/OverlaySelectionBlueprint.cs b/osu.Game/Rulesets/Edit/OverlaySelectionBlueprint.cs
index b4ae3f3fba..75200e3027 100644
--- a/osu.Game/Rulesets/Edit/OverlaySelectionBlueprint.cs
+++ b/osu.Game/Rulesets/Edit/OverlaySelectionBlueprint.cs
@@ -15,7 +15,12 @@ namespace osu.Game.Rulesets.Edit
///
public readonly DrawableHitObject DrawableObject;
- protected override bool ShouldBeAlive => (DrawableObject.IsAlive && DrawableObject.IsPresent) || State == SelectionState.Selected;
+ ///
+ /// Whether the blueprint should be shown even when the is not alive.
+ ///
+ protected virtual bool AlwaysShowWhenSelected => false;
+
+ protected override bool ShouldBeAlive => (DrawableObject.IsAlive && DrawableObject.IsPresent) || (AlwaysShowWhenSelected && State == SelectionState.Selected);
protected OverlaySelectionBlueprint(DrawableHitObject drawableObject)
: base(drawableObject.HitObject)
@@ -25,7 +30,7 @@ namespace osu.Game.Rulesets.Edit
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.ReceivePositionalInputAt(screenSpacePos);
- public override Vector2 SelectionPoint => DrawableObject.ScreenSpaceDrawQuad.Centre;
+ public override Vector2 ScreenSpaceSelectionPoint => DrawableObject.ScreenSpaceDrawQuad.Centre;
public override Quad SelectionQuad => DrawableObject.ScreenSpaceDrawQuad;
diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
index fb1eb7adbf..02d5955ae6 100644
--- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
+++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs
@@ -1,15 +1,16 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
-using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects;
+using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose;
using osuTK;
@@ -30,10 +31,13 @@ namespace osu.Game.Rulesets.Edit
///
protected readonly HitObject HitObject;
- protected IClock EditorClock { get; private set; }
+ [Resolved(canBeNull: true)]
+ protected EditorClock EditorClock { get; private set; }
private readonly IBindable beatmap = new Bindable();
+ private Bindable startTimeBindable;
+
[Resolved]
private IPlacementHandler placementHandler { get; set; }
@@ -49,23 +53,20 @@ namespace osu.Game.Rulesets.Edit
}
[BackgroundDependencyLoader]
- private void load(IBindable beatmap, IAdjustableClock clock)
+ private void load(IBindable beatmap)
{
this.beatmap.BindTo(beatmap);
- EditorClock = clock;
-
- ApplyDefaultsToHitObject();
+ startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy();
+ startTimeBindable.BindValueChanged(_ => ApplyDefaultsToHitObject(), true);
}
///
/// Signals that the placement of has started.
///
- /// The start time of at the placement point. If null, the current clock time is used.
/// Whether this call is committing a value for HitObject.StartTime and continuing with further adjustments.
- protected void BeginPlacement(double? startTime = null, bool commitStart = false)
+ protected void BeginPlacement(bool commitStart = false)
{
- HitObject.StartTime = startTime ?? EditorClock.CurrentTime;
placementHandler.BeginPlacement(HitObject);
PlacementActive |= commitStart;
}
@@ -86,11 +87,15 @@ namespace osu.Game.Rulesets.Edit
///
/// Updates the position of this to a new screen-space position.
///
- /// The screen-space position.
- public abstract void UpdatePosition(Vector2 screenSpacePosition);
+ /// The snap result information.
+ public virtual void UpdatePosition(SnapResult result)
+ {
+ if (!PlacementActive)
+ HitObject.StartTime = result.Time ?? EditorClock?.CurrentTime ?? Time.Current;
+ }
///
- /// Invokes ,
+ /// Invokes ,
/// refreshing and parameters for the .
///
protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.Value.Beatmap.ControlPointInfo, beatmap.Value.Beatmap.BeatmapInfo.BaseDifficulty);
diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
index e6a63eae4f..71256093d5 100644
--- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
+++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs
@@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Edit
///
/// The screen-space point that causes this to be selected.
///
- public virtual Vector2 SelectionPoint => ScreenSpaceDrawQuad.Centre;
+ public virtual Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.Centre;
///
/// The screen-space quad that outlines this for selections.
diff --git a/osu.Game/Rulesets/Edit/SnapResult.cs b/osu.Game/Rulesets/Edit/SnapResult.cs
new file mode 100644
index 0000000000..31dd2b9496
--- /dev/null
+++ b/osu.Game/Rulesets/Edit/SnapResult.cs
@@ -0,0 +1,33 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Game.Rulesets.UI;
+using osuTK;
+
+namespace osu.Game.Rulesets.Edit
+{
+ ///
+ /// The result of a position/time snapping process.
+ ///
+ public class SnapResult
+ {
+ ///
+ /// The screen space position, potentially altered for snapping.
+ ///
+ public Vector2 ScreenSpacePosition;
+
+ ///
+ /// The resultant time for snapping, if a value could be attained.
+ ///
+ public double? Time;
+
+ public readonly Playfield Playfield;
+
+ public SnapResult(Vector2 screenSpacePosition, double? time, Playfield playfield = null)
+ {
+ ScreenSpacePosition = screenSpacePosition;
+ Time = time;
+ Playfield = playfield;
+ }
+ }
+}
diff --git a/osu.Game/Rulesets/Objects/BarLineGenerator.cs b/osu.Game/Rulesets/Objects/BarLineGenerator.cs
index 5588e9c0b7..9556b52735 100644
--- a/osu.Game/Rulesets/Objects/BarLineGenerator.cs
+++ b/osu.Game/Rulesets/Objects/BarLineGenerator.cs
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd . 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.Linq;
using osu.Framework.Utils;
@@ -46,6 +47,16 @@ namespace osu.Game.Rulesets.Objects
for (double t = currentTimingPoint.Time; Precision.DefinitelyBigger(endTime, t); t += barLength, currentBeat++)
{
+ var roundedTime = Math.Round(t, MidpointRounding.AwayFromZero);
+
+ // in the case of some bar lengths, rounding errors can cause t to be slightly less than
+ // the expected whole number value due to floating point inaccuracies.
+ // if this is the case, apply rounding.
+ if (Precision.AlmostEquals(t, roundedTime))
+ {
+ t = roundedTime;
+ }
+
BarLines.Add(new TBarLine
{
StartTime = t,
diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
index d594909cda..44afb7a227 100644
--- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
+++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs
@@ -257,7 +257,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
}
}
- if (state.Value != ArmedState.Idle && LifetimeEnd == double.MaxValue || HitObject.HitWindows == null)
+ if (LifetimeEnd == double.MaxValue && (state.Value != ArmedState.Idle || HitObject.HitWindows == null))
Expire();
// apply any custom state overrides
diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs
index cffbdbae08..e2cc98813a 100644
--- a/osu.Game/Rulesets/Objects/HitObject.cs
+++ b/osu.Game/Rulesets/Objects/HitObject.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Threading;
using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Framework.Bindables;
@@ -99,7 +100,8 @@ namespace osu.Game.Rulesets.Objects
///
/// The control points.
/// The difficulty settings to use.
- public void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
+ /// The cancellation token.
+ public void ApplyDefaults(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty, CancellationToken cancellationToken = default)
{
ApplyDefaultsToSelf(controlPointInfo, difficulty);
@@ -108,7 +110,7 @@ namespace osu.Game.Rulesets.Objects
nestedHitObjects.Clear();
- CreateNestedHitObjects();
+ CreateNestedHitObjects(cancellationToken);
if (this is IHasComboInformation hasCombo)
{
@@ -122,7 +124,7 @@ namespace osu.Game.Rulesets.Objects
nestedHitObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
foreach (var h in nestedHitObjects)
- h.ApplyDefaults(controlPointInfo, difficulty);
+ h.ApplyDefaults(controlPointInfo, difficulty, cancellationToken);
DefaultsApplied?.Invoke(this);
}
@@ -136,6 +138,14 @@ namespace osu.Game.Rulesets.Objects
HitWindows?.SetDifficulty(difficulty.OverallDifficulty);
}
+ protected virtual void CreateNestedHitObjects(CancellationToken cancellationToken)
+ {
+ // ReSharper disable once MethodSupportsCancellation (https://youtrack.jetbrains.com/issue/RIDER-44520)
+#pragma warning disable 618
+ CreateNestedHitObjects();
+#pragma warning restore 618
+ }
+
protected virtual void CreateNestedHitObjects()
{
}
@@ -165,10 +175,10 @@ namespace osu.Game.Rulesets.Objects
/// Returns the end time of this object.
///
///
- /// This returns the where available, falling back to otherwise.
+ /// This returns the where available, falling back to otherwise.
///
/// The object.
/// The end time of this object.
- public static double GetEndTime(this HitObject hitObject) => (hitObject as IHasEndTime)?.EndTime ?? hitObject.StartTime;
+ public static double GetEndTime(this HitObject hitObject) => (hitObject as IHasDuration)?.EndTime ?? hitObject.StartTime;
}
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs
index 43e8d01297..c10c8dc30f 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertHitObjectParser.cs
@@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
};
}
- protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime)
+ protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration)
{
// Convert spinners don't create the new combo themselves, but force the next non-spinner hitobject to create a new combo
// Their combo offset is still added to that next hitobject's combo index
@@ -65,11 +65,11 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
return new ConvertSpinner
{
- EndTime = endTime
+ Duration = duration
};
}
- protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime)
+ protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration)
{
return null;
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs
index 9de311c9d7..014494ec54 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Catch/ConvertSpinner.cs
@@ -8,11 +8,11 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
///
/// Legacy osu!catch Spinner-type, used for parsing Beatmaps.
///
- internal sealed class ConvertSpinner : ConvertHitObject, IHasEndTime, IHasXPosition, IHasCombo
+ internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasXPosition, IHasCombo
{
- public double EndTime { get; set; }
+ public double EndTime => StartTime + Duration;
- public double Duration => EndTime - StartTime;
+ public double Duration { get; set; }
public float X => 256; // Required for CatchBeatmapConverter
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
index 9a60a0a75c..9e936c7717 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs
@@ -189,9 +189,9 @@ namespace osu.Game.Rulesets.Objects.Legacy
}
else if (type.HasFlag(LegacyHitObjectType.Spinner))
{
- double endTime = Math.Max(startTime, Parsing.ParseDouble(split[5]) + Offset);
+ double duration = Math.Max(0, Parsing.ParseDouble(split[5]) + Offset - startTime);
- result = CreateSpinner(new Vector2(512, 384) / 2, combo, comboOffset, endTime);
+ result = CreateSpinner(new Vector2(512, 384) / 2, combo, comboOffset, duration);
if (split.Length > 6)
readCustomSampleBanks(split[6], bankInfo);
@@ -209,7 +209,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
readCustomSampleBanks(string.Join(":", ss.Skip(1)), bankInfo);
}
- result = CreateHold(pos, combo, comboOffset, endTime + Offset);
+ result = CreateHold(pos, combo, comboOffset, endTime + Offset - startTime);
}
if (result == null)
@@ -321,9 +321,9 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// The position of the hit object.
/// Whether the hit object creates a new combo.
/// When starting a new combo, the offset of the new combo relative to the current one.
- /// The spinner end time.
+ /// The spinner duration.
/// The hit object.
- protected abstract HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime);
+ protected abstract HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration);
///
/// Creates a legacy Hold-type hit object.
@@ -331,8 +331,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// The position of the hit object.
/// Whether the hit object creates a new combo.
/// When starting a new combo, the offset of the new combo relative to the current one.
- /// The hold end time.
- protected abstract HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime);
+ /// The hold duration.
+ protected abstract HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration);
private List convertSoundType(LegacyHitSoundType type, SampleBankInfo bankInfo)
{
diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs
index 924182b265..c522dc623c 100644
--- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs
@@ -9,7 +9,10 @@ using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Objects.Legacy
{
- internal abstract class ConvertSlider : ConvertHitObject, IHasCurve, IHasLegacyLastTickOffset
+ internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasLegacyLastTickOffset,
+#pragma warning disable 618
+ IHasCurve
+#pragma warning restore 618
{
///
/// Scoring distance with a speed-adjusted beat length of 1 second.
@@ -26,13 +29,13 @@ namespace osu.Game.Rulesets.Objects.Legacy
public List> NodeSamples { get; set; }
public int RepeatCount { get; set; }
- public double EndTime
+ public double Duration
{
- get => StartTime + this.SpanCount() * Distance / Velocity;
+ get => this.SpanCount() * Distance / Velocity;
set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
}
- public double Duration => EndTime - StartTime;
+ public double EndTime => StartTime + Duration;
public double Velocity = 1;
diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs
index f94c4aaa75..bc64518f40 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHitObjectParser.cs
@@ -37,21 +37,21 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
};
}
- protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime)
+ protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration)
{
return new ConvertSpinner
{
X = position.X,
- EndTime = endTime
+ Duration = duration
};
}
- protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime)
+ protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration)
{
return new ConvertHold
{
X = position.X,
- EndTime = endTime
+ Duration = duration
};
}
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs
index 1d92d638dd..2fa4766c1d 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertHold.cs
@@ -5,12 +5,12 @@ using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Objects.Legacy.Mania
{
- internal sealed class ConvertHold : ConvertHitObject, IHasXPosition, IHasEndTime
+ internal sealed class ConvertHold : ConvertHitObject, IHasXPosition, IHasDuration
{
public float X { get; set; }
- public double EndTime { get; set; }
+ public double Duration { get; set; }
- public double Duration => EndTime - StartTime;
+ public double EndTime => StartTime + Duration;
}
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs
index 7dc13e27cd..c05aaceb9c 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Mania/ConvertSpinner.cs
@@ -8,11 +8,11 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
///
/// Legacy osu!mania Spinner-type, used for parsing Beatmaps.
///
- internal sealed class ConvertSpinner : ConvertHitObject, IHasEndTime, IHasXPosition
+ internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasXPosition
{
- public double EndTime { get; set; }
+ public double Duration { get; set; }
- public double Duration => EndTime - StartTime;
+ public double EndTime => StartTime + Duration;
public float X { get; set; }
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs
index b95ec703b6..75ecab0b8f 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertHitObjectParser.cs
@@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
};
}
- protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime)
+ protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration)
{
// Convert spinners don't create the new combo themselves, but force the next non-spinner hitobject to create a new combo
// Their combo offset is still added to that next hitobject's combo index
@@ -66,11 +66,11 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
return new ConvertSpinner
{
Position = position,
- EndTime = endTime
+ Duration = duration
};
}
- protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime)
+ protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration)
{
return null;
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs
index 8b21aab411..e9e5ca8c94 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Osu/ConvertSpinner.cs
@@ -9,11 +9,11 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
///
/// Legacy osu! Spinner-type, used for parsing Beatmaps.
///
- internal sealed class ConvertSpinner : ConvertHitObject, IHasEndTime, IHasPosition, IHasCombo
+ internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration, IHasPosition, IHasCombo
{
- public double EndTime { get; set; }
+ public double Duration { get; set; }
- public double Duration => EndTime - StartTime;
+ public double EndTime => StartTime + Duration;
public Vector2 Position { get; set; }
diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs
index db65a61c90..13e3e84c6a 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertHitObjectParser.cs
@@ -33,15 +33,15 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko
};
}
- protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime)
+ protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double duration)
{
return new ConvertSpinner
{
- EndTime = endTime
+ Duration = duration
};
}
- protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime)
+ protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double duration)
{
return null;
}
diff --git a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs
index 8e28487f2f..1d5ecb1ef3 100644
--- a/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs
+++ b/osu.Game/Rulesets/Objects/Legacy/Taiko/ConvertSpinner.cs
@@ -8,10 +8,10 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko
///
/// Legacy osu!taiko Spinner-type, used for parsing Beatmaps.
///
- internal sealed class ConvertSpinner : ConvertHitObject, IHasEndTime
+ internal sealed class ConvertSpinner : ConvertHitObject, IHasDuration
{
- public double EndTime { get; set; }
+ public double Duration { get; set; }
- public double Duration => EndTime - StartTime;
+ public double EndTime => StartTime + Duration;
}
}
diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs
index e9ee3833b7..d8c6da86f9 100644
--- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs
+++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs
@@ -4,13 +4,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Threading;
namespace osu.Game.Rulesets.Objects
{
public static class SliderEventGenerator
{
+ [Obsolete("Use the overload with cancellation support instead.")] // can be removed 20201115
+ // ReSharper disable once RedundantOverload.Global
public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount,
double? legacyLastTickOffset)
+ {
+ return Generate(startTime, spanDuration, velocity, tickDistance, totalDistance, spanCount, legacyLastTickOffset, default);
+ }
+
+ // ReSharper disable once MethodOverloadWithOptionalParameter
+ public static IEnumerable Generate(double startTime, double spanDuration, double velocity, double tickDistance, double totalDistance, int spanCount,
+ double? legacyLastTickOffset, CancellationToken cancellationToken = default)
{
// A very lenient maximum length of a slider for ticks to be generated.
// This exists for edge cases such as /b/1573664 where the beatmap has been edited by the user, and should never be reached in normal usage.
@@ -37,7 +47,7 @@ namespace osu.Game.Rulesets.Objects
var spanStartTime = startTime + span * spanDuration;
var reversed = span % 2 == 1;
- var ticks = generateTicks(span, spanStartTime, spanDuration, reversed, length, tickDistance, minDistanceFromEnd);
+ var ticks = generateTicks(span, spanStartTime, spanDuration, reversed, length, tickDistance, minDistanceFromEnd, cancellationToken);
if (reversed)
{
@@ -108,12 +118,15 @@ namespace osu.Game.Rulesets.Objects
/// The length of the path.
/// The distance between each tick.
/// The distance from the end of the path at which ticks are not allowed to be added.
+ /// The cancellation token.
/// A for each tick. If is true, the ticks will be returned in reverse-StartTime order.
private static IEnumerable generateTicks(int spanIndex, double spanStartTime, double spanDuration, bool reversed, double length, double tickDistance,
- double minDistanceFromEnd)
+ double minDistanceFromEnd, CancellationToken cancellationToken = default)
{
for (var d = tickDistance; d <= length; d += tickDistance)
{
+ cancellationToken.ThrowIfCancellationRequested();
+
if (d >= length - minDistanceFromEnd)
break;
diff --git a/osu.Game/Rulesets/Objects/Types/IHasCurve.cs b/osu.Game/Rulesets/Objects/Types/IHasCurve.cs
index e98a888bd7..26f50ffa31 100644
--- a/osu.Game/Rulesets/Objects/Types/IHasCurve.cs
+++ b/osu.Game/Rulesets/Objects/Types/IHasCurve.cs
@@ -1,13 +1,12 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using osuTK;
namespace osu.Game.Rulesets.Objects.Types
{
- ///
- /// A HitObject that has a curve.
- ///
+ [Obsolete("Use IHasPathWithRepeats instead.")] // can be removed 20201126
public interface IHasCurve : IHasDistance, IHasRepeats
{
///
@@ -16,6 +15,8 @@ namespace osu.Game.Rulesets.Objects.Types
SliderPath Path { get; }
}
+#pragma warning disable 618
+ [Obsolete("Use IHasPathWithRepeats instead.")] // can be removed 20201126
public static class HasCurveExtensions
{
///
@@ -50,4 +51,5 @@ namespace osu.Game.Rulesets.Objects.Types
public static int SpanAt(this IHasCurve obj, double progress)
=> (int)(progress * obj.SpanCount());
}
+#pragma warning restore 618
}
diff --git a/osu.Game/Rulesets/Objects/Types/IHasDistance.cs b/osu.Game/Rulesets/Objects/Types/IHasDistance.cs
index e7f552115e..b497ca5da3 100644
--- a/osu.Game/Rulesets/Objects/Types/IHasDistance.cs
+++ b/osu.Game/Rulesets/Objects/Types/IHasDistance.cs
@@ -6,7 +6,7 @@ namespace osu.Game.Rulesets.Objects.Types
///
/// A HitObject that has a positional length.
///
- public interface IHasDistance : IHasEndTime
+ public interface IHasDistance : IHasDuration
{
///
/// The positional length of the HitObject.
diff --git a/osu.Game/Rulesets/Objects/Types/IHasDuration.cs b/osu.Game/Rulesets/Objects/Types/IHasDuration.cs
new file mode 100644
index 0000000000..185fd5977b
--- /dev/null
+++ b/osu.Game/Rulesets/Objects/Types/IHasDuration.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using Newtonsoft.Json;
+
+namespace osu.Game.Rulesets.Objects.Types
+{
+ ///
+ /// A HitObject that ends at a different time than its start time.
+ ///
+#pragma warning disable 618
+ public interface IHasDuration : IHasEndTime
+#pragma warning restore 618
+ {
+ double IHasEndTime.EndTime
+ {
+ get => EndTime;
+ set => Duration = (Duration - EndTime) + value;
+ }
+
+ double IHasEndTime.Duration => Duration;
+
+ ///
+ /// The time at which the HitObject ends.
+ ///
+ new double EndTime { get; }
+
+ ///
+ /// The duration of the HitObject.
+ ///
+ [JsonIgnore]
+ new double Duration { get; set; }
+ }
+}
diff --git a/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs b/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs
index bc7103c60d..c3769c5909 100644
--- a/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs
+++ b/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs
@@ -1,6 +1,7 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System;
using Newtonsoft.Json;
namespace osu.Game.Rulesets.Objects.Types
@@ -8,6 +9,7 @@ namespace osu.Game.Rulesets.Objects.Types
///
/// A HitObject that ends at a different time than its start time.
///
+ [Obsolete("Use IHasDuration instead.")] // can be removed 20201126
public interface IHasEndTime
{
///
diff --git a/osu.Game/Rulesets/Objects/Types/IHasPath.cs b/osu.Game/Rulesets/Objects/Types/IHasPath.cs
new file mode 100644
index 0000000000..567c24a4a2
--- /dev/null
+++ b/osu.Game/Rulesets/Objects/Types/IHasPath.cs
@@ -0,0 +1,13 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+namespace osu.Game.Rulesets.Objects.Types
+{
+ public interface IHasPath : IHasDistance
+ {
+ ///
+ /// The curve.
+ ///
+ SliderPath Path { get; }
+ }
+}
diff --git a/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs b/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs
new file mode 100644
index 0000000000..279946b44e
--- /dev/null
+++ b/osu.Game/Rulesets/Objects/Types/IHasPathWithRepeats.cs
@@ -0,0 +1,50 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osuTK;
+
+namespace osu.Game.Rulesets.Objects.Types
+{
+ ///
+ /// A HitObject that has a curve.
+ ///
+ // ReSharper disable once RedundantExtendsListEntry
+ public interface IHasPathWithRepeats : IHasPath, IHasRepeats
+ {
+ }
+
+ public static class HasPathWithRepeatsExtensions
+ {
+ ///
+ /// Computes the position on the curve relative to how much of the has been completed.
+ ///
+ /// The curve.
+ /// [0, 1] where 0 is the start time of the and 1 is the end time of the .
+ /// The position on the curve.
+ public static Vector2 CurvePositionAt(this IHasPathWithRepeats obj, double progress)
+ => obj.Path.PositionAt(obj.ProgressAt(progress));
+
+ ///
+ /// Computes the progress along the curve relative to how much of the has been completed.
+ ///
+ /// The curve.
+ /// [0, 1] where 0 is the start time of the and 1 is the end time of the .
+ /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve.
+ public static double ProgressAt(this IHasPathWithRepeats obj, double progress)
+ {
+ double p = progress * obj.SpanCount() % 1;
+ if (obj.SpanAt(progress) % 2 == 1)
+ p = 1 - p;
+ return p;
+ }
+
+ ///
+ /// Determines which span of the curve the progress point is on.
+ ///
+ /// The curve.
+ /// [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve.
+ /// [0, SpanCount) where 0 is the first run.
+ public static int SpanAt(this IHasPathWithRepeats obj, double progress)
+ => (int)(progress * obj.SpanCount());
+ }
+}
diff --git a/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs b/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs
index 256b1f3963..7a3fb16196 100644
--- a/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs
+++ b/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs
@@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Objects.Types
///
/// A HitObject that spans some length.
///
- public interface IHasRepeats : IHasEndTime
+ public interface IHasRepeats : IHasDuration
{
///
/// The amount of times the HitObject repeats.
diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs
index bee11accca..4f28607733 100644
--- a/osu.Game/Rulesets/Ruleset.cs
+++ b/osu.Game/Rulesets/Ruleset.cs
@@ -22,6 +22,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Skinning;
using osu.Game.Users;
+using JetBrains.Annotations;
namespace osu.Game.Rulesets
{
@@ -100,7 +101,8 @@ namespace osu.Game.Rulesets
return value;
}
- public ModAutoplay GetAutoplayMod() => GetAllMods().OfType().First();
+ [CanBeNull]
+ public ModAutoplay GetAutoplayMod() => GetAllMods().OfType().FirstOrDefault();
public virtual ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => null;
diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs
index b3026bf2b7..58a2ba056e 100644
--- a/osu.Game/Rulesets/RulesetStore.cs
+++ b/osu.Game/Rulesets/RulesetStore.cs
@@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
+using osu.Framework;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Database;
@@ -153,14 +154,14 @@ namespace osu.Game.Rulesets
{
try
{
- string[] files = Directory.GetFiles(Environment.CurrentDirectory, $"{ruleset_library_prefix}.*.dll");
+ var files = Directory.GetFiles(RuntimeInfo.StartupDirectory, $"{ruleset_library_prefix}.*.dll");
foreach (string file in files.Where(f => !Path.GetFileName(f).Contains("Tests")))
loadRulesetFromFile(file);
}
catch (Exception e)
{
- Logger.Error(e, $"Could not load rulesets from directory {Environment.CurrentDirectory}");
+ Logger.Error(e, $"Could not load rulesets from directory {RuntimeInfo.StartupDirectory}");
}
}
diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs
index fffcbb3c9f..982f527517 100644
--- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs
@@ -3,9 +3,11 @@
using System;
using System.Collections.Generic;
+using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
+using osu.Game.Utils;
namespace osu.Game.Rulesets.Scoring
{
@@ -47,6 +49,8 @@ namespace osu.Game.Rulesets.Scoring
private double targetMinimumHealth;
private double drainRate = 1;
+ private PeriodTracker noDrainPeriodTracker;
+
///
/// Creates a new .
///
@@ -60,14 +64,14 @@ namespace osu.Game.Rulesets.Scoring
{
base.Update();
- if (!IsBreakTime.Value)
- {
- // When jumping in and out of gameplay time within a single frame, health should only be drained for the period within the gameplay time
- double lastGameplayTime = Math.Clamp(Time.Current - Time.Elapsed, drainStartTime, gameplayEndTime);
- double currentGameplayTime = Math.Clamp(Time.Current, drainStartTime, gameplayEndTime);
+ if (noDrainPeriodTracker?.IsInAny(Time.Current) == true)
+ return;
- Health.Value -= drainRate * (currentGameplayTime - lastGameplayTime);
- }
+ // When jumping in and out of gameplay time within a single frame, health should only be drained for the period within the gameplay time
+ double lastGameplayTime = Math.Clamp(Time.Current - Time.Elapsed, drainStartTime, gameplayEndTime);
+ double currentGameplayTime = Math.Clamp(Time.Current, drainStartTime, gameplayEndTime);
+
+ Health.Value -= drainRate * (currentGameplayTime - lastGameplayTime);
}
public override void ApplyBeatmap(IBeatmap beatmap)
@@ -77,6 +81,19 @@ namespace osu.Game.Rulesets.Scoring
if (beatmap.HitObjects.Count > 0)
gameplayEndTime = beatmap.HitObjects[^1].GetEndTime();
+ noDrainPeriodTracker = new PeriodTracker(beatmap.Breaks.Select(breakPeriod => new Period(
+ beatmap.HitObjects
+ .Select(hitObject => hitObject.GetEndTime())
+ .Where(endTime => endTime <= breakPeriod.StartTime)
+ .DefaultIfEmpty(double.MinValue)
+ .Last(),
+ beatmap.HitObjects
+ .Select(hitObject => hitObject.StartTime)
+ .Where(startTime => startTime >= breakPeriod.EndTime)
+ .DefaultIfEmpty(double.MaxValue)
+ .First()
+ )));
+
targetMinimumHealth = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.DrainRate, min_health_target, mid_health_target, max_health_target);
base.ApplyBeatmap(beatmap);
diff --git a/osu.Game/Rulesets/Scoring/HealthProcessor.cs b/osu.Game/Rulesets/Scoring/HealthProcessor.cs
index 45edc0f4a3..1535fe4d00 100644
--- a/osu.Game/Rulesets/Scoring/HealthProcessor.cs
+++ b/osu.Game/Rulesets/Scoring/HealthProcessor.cs
@@ -26,11 +26,6 @@ namespace osu.Game.Rulesets.Scoring
///
public readonly BindableDouble Health = new BindableDouble(1) { MinValue = 0, MaxValue = 1 };
- ///
- /// Whether gameplay is currently in a break.
- ///
- public readonly IBindable IsBreakTime = new Bindable();
-
///
/// Whether this ScoreProcessor has already triggered the failed state.
///
diff --git a/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs b/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs
index 41f9ebdb82..0052c877f6 100644
--- a/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/Algorithms/SequentialScrollAlgorithm.cs
@@ -22,8 +22,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength)
{
- double adjustedTime = TimeAt(-offset, originTime, timeRange, scrollLength);
- return adjustedTime - timeRange - 1000;
+ return TimeAt(-(scrollLength + offset), originTime, timeRange, scrollLength);
}
public float GetLength(double startTime, double endTime, double timeRange, float scrollLength)
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
index 15e625872d..0dc3324559 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
@@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Layout;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
+using osuTK;
namespace osu.Game.Rulesets.UI.Scrolling
{
@@ -78,6 +79,98 @@ namespace osu.Game.Rulesets.UI.Scrolling
hitObjectInitialStateCache.Clear();
}
+ ///
+ /// Given a position in screen space, return the time within this column.
+ ///
+ public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition)
+ {
+ // convert to local space of column so we can snap and fetch correct location.
+ Vector2 localPosition = ToLocalSpace(screenSpacePosition);
+
+ float position = 0;
+
+ switch (scrollingInfo.Direction.Value)
+ {
+ case ScrollingDirection.Up:
+ case ScrollingDirection.Down:
+ position = localPosition.Y;
+ break;
+
+ case ScrollingDirection.Right:
+ case ScrollingDirection.Left:
+ position = localPosition.X;
+ break;
+ }
+
+ flipPositionIfRequired(ref position);
+
+ return scrollingInfo.Algorithm.TimeAt(position, Time.Current, scrollingInfo.TimeRange.Value, getLength());
+ }
+
+ ///
+ /// Given a time, return the screen space position within this column.
+ ///
+ public Vector2 ScreenSpacePositionAtTime(double time)
+ {
+ var pos = scrollingInfo.Algorithm.PositionAt(time, Time.Current, scrollingInfo.TimeRange.Value, getLength());
+
+ flipPositionIfRequired(ref pos);
+
+ switch (scrollingInfo.Direction.Value)
+ {
+ case ScrollingDirection.Up:
+ case ScrollingDirection.Down:
+ return ToScreenSpace(new Vector2(getBreadth() / 2, pos));
+
+ default:
+ return ToScreenSpace(new Vector2(pos, getBreadth() / 2));
+ }
+ }
+
+ private float getLength()
+ {
+ switch (scrollingInfo.Direction.Value)
+ {
+ case ScrollingDirection.Left:
+ case ScrollingDirection.Right:
+ return DrawWidth;
+
+ default:
+ return DrawHeight;
+ }
+ }
+
+ private float getBreadth()
+ {
+ switch (scrollingInfo.Direction.Value)
+ {
+ case ScrollingDirection.Up:
+ case ScrollingDirection.Down:
+ return DrawWidth;
+
+ default:
+ return DrawHeight;
+ }
+ }
+
+ private void flipPositionIfRequired(ref float position)
+ {
+ // We're dealing with screen coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time.
+ // The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position,
+ // so when scrolling downwards the coordinates need to be flipped.
+
+ switch (scrollingInfo.Direction.Value)
+ {
+ case ScrollingDirection.Down:
+ position = DrawHeight - position;
+ break;
+
+ case ScrollingDirection.Right:
+ position = DrawWidth - position;
+ break;
+ }
+ }
+
private void onDefaultsApplied(DrawableHitObject drawableObject)
{
// The cache may not exist if the hitobject state hasn't been computed yet (e.g. if the hitobject was added + defaults applied in the same frame).
@@ -177,7 +270,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
// Cant use AddOnce() since the delegate is re-constructed every invocation
private void computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() =>
{
- if (hitObject.HitObject is IHasEndTime e)
+ if (hitObject.HitObject is IHasDuration e)
{
switch (direction.Value)
{
diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs
index bf2203e176..9dac3f4de1 100644
--- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs
+++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs
@@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Drawables;
+using osuTK;
namespace osu.Game.Rulesets.UI.Scrolling
{
@@ -15,14 +16,26 @@ namespace osu.Game.Rulesets.UI.Scrolling
protected readonly IBindable Direction = new Bindable();
[Resolved]
- private IScrollingInfo scrollingInfo { get; set; }
+ protected IScrollingInfo ScrollingInfo { get; private set; }
[BackgroundDependencyLoader]
private void load()
{
- Direction.BindTo(scrollingInfo.Direction);
+ Direction.BindTo(ScrollingInfo.Direction);
}
+ ///
+ /// Given a position in screen space, return the time within this column.
+ ///
+ public virtual double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) =>
+ ((ScrollingHitObjectContainer)HitObjectContainer).TimeAtScreenSpacePosition(screenSpacePosition);
+
+ ///
+ /// Given a time, return the screen space position within this column.
+ ///
+ public virtual Vector2 ScreenSpacePositionAtTime(double time)
+ => ((ScrollingHitObjectContainer)HitObjectContainer).ScreenSpacePositionAtTime(time);
+
protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer();
}
}
diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs
index 897c6ec531..59b3d1c565 100644
--- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs
+++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs
@@ -13,7 +13,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
-using osu.Framework.Timing;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
@@ -26,7 +25,7 @@ namespace osu.Game.Screens.Edit.Components
private IconButton playButton;
[Resolved]
- private IAdjustableClock adjustableClock { get; set; }
+ private EditorClock editorClock { get; set; }
private readonly BindableNumber tempo = new BindableDouble(1);
@@ -87,17 +86,17 @@ namespace osu.Game.Screens.Edit.Components
private void togglePause()
{
- if (adjustableClock.IsRunning)
- adjustableClock.Stop();
+ if (editorClock.IsRunning)
+ editorClock.Stop();
else
- adjustableClock.Start();
+ editorClock.Start();
}
protected override void Update()
{
base.Update();
- playButton.Icon = adjustableClock.IsRunning ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle;
+ playButton.Icon = editorClock.IsRunning ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle;
}
private class PlaybackTabControl : OsuTabControl
diff --git a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs
index 4bf21d240a..c1f54d7938 100644
--- a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs
+++ b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs
@@ -5,7 +5,6 @@ using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites;
using System;
using osu.Framework.Allocation;
-using osu.Framework.Timing;
using osu.Game.Graphics;
namespace osu.Game.Screens.Edit.Components
@@ -15,7 +14,7 @@ namespace osu.Game.Screens.Edit.Components
private readonly OsuSpriteText trackTimer;
[Resolved]
- private IAdjustableClock adjustableClock { get; set; }
+ private EditorClock editorClock { get; set; }
public TimeInfoContainer()
{
@@ -35,7 +34,7 @@ namespace osu.Game.Screens.Edit.Components
{
base.Update();
- trackTimer.Text = TimeSpan.FromMilliseconds(adjustableClock.CurrentTime).ToString(@"mm\:ss\:fff");
+ trackTimer.Text = TimeSpan.FromMilliseconds(editorClock.CurrentTime).ToString(@"mm\:ss\:fff");
}
}
}
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs
index 5d638d7919..9e9ac93d23 100644
--- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs
@@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
-using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
@@ -20,14 +19,14 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
///
public class MarkerPart : TimelinePart
{
- private readonly Drawable marker;
+ private Drawable marker;
- private readonly IAdjustableClock adjustableClock;
+ [Resolved]
+ private EditorClock editorClock { get; set; }
- public MarkerPart(IAdjustableClock adjustableClock)
+ [BackgroundDependencyLoader]
+ private void load()
{
- this.adjustableClock = adjustableClock;
-
Add(marker = new MarkerVisualisation());
}
@@ -59,14 +58,14 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
return;
float markerPos = Math.Clamp(ToLocalSpace(screenPosition).X, 0, DrawWidth);
- adjustableClock.Seek(markerPos / DrawWidth * Beatmap.Value.Track.Length);
+ editorClock.SeekTo(markerPos / DrawWidth * editorClock.TrackLength);
});
}
protected override void Update()
{
base.Update();
- marker.X = (float)adjustableClock.CurrentTime;
+ marker.X = (float)editorClock.CurrentTime;
}
protected override void LoadBeatmap(WorkingBeatmap beatmap)
diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs
index 20db2cac21..02cd4bccb4 100644
--- a/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs
+++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/SummaryTimeline.cs
@@ -6,7 +6,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Timing;
using osu.Game.Graphics;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
@@ -18,11 +17,11 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary
public class SummaryTimeline : BottomBarContainer
{
[BackgroundDependencyLoader]
- private void load(OsuColour colours, IAdjustableClock adjustableClock)
+ private void load(OsuColour colours)
{
Children = new Drawable[]
{
- new MarkerPart(adjustableClock) { RelativeSizeAxes = Axes.Both },
+ new MarkerPart { RelativeSizeAxes = Axes.Both },
new ControlPointPart
{
Anchor = Anchor.Centre,
diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index 8910684463..d07cffff0c 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
-using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Linq;
@@ -14,7 +13,6 @@ using osu.Framework.Graphics.Primitives;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
-using osu.Framework.Timing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
@@ -29,8 +27,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
///
public abstract class BlueprintContainer : CompositeDrawable, IKeyBindingHandler
{
- public event Action> SelectionChanged;
-
protected DragBox DragBox { get; private set; }
protected Container SelectionBlueprints { get; private set; }
@@ -41,15 +37,17 @@ namespace osu.Game.Screens.Edit.Compose.Components
private IEditorChangeHandler changeHandler { get; set; }
[Resolved]
- private IAdjustableClock adjustableClock { get; set; }
+ private EditorClock editorClock { get; set; }
[Resolved]
private EditorBeatmap beatmap { get; set; }
private readonly BindableList selectedHitObjects = new BindableList();
+ public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
+
[Resolved(canBeNull: true)]
- private IDistanceSnapProvider snapProvider { get; set; }
+ private IPositionSnapProvider snapProvider { get; set; }
protected BlueprintContainer()
{
@@ -88,8 +86,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
SelectionBlueprints.FirstOrDefault(b => b.HitObject == o)?.Deselect();
break;
}
-
- SelectionChanged?.Invoke(selectedHitObjects);
};
}
@@ -149,7 +145,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (clickedBlueprint == null)
return false;
- adjustableClock?.Seek(clickedBlueprint.HitObject.StartTime);
+ editorClock?.SeekTo(clickedBlueprint.HitObject.StartTime);
return true;
}
@@ -326,7 +322,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
foreach (var blueprint in SelectionBlueprints)
{
- if (blueprint.IsAlive && blueprint.IsPresent && rect.Contains(blueprint.SelectionPoint))
+ if (blueprint.IsAlive && blueprint.IsPresent && rect.Contains(blueprint.ScreenSpaceSelectionPoint))
blueprint.Select();
else
blueprint.Deselect();
@@ -384,7 +380,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
// Movement is tracked from the blueprint of the earliest hitobject, since it only makes sense to distance snap from that hitobject
movementBlueprint = selectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).First();
- movementBlueprintOriginalPosition = movementBlueprint.SelectionPoint; // todo: unsure if correct
+ movementBlueprintOriginalPosition = movementBlueprint.ScreenSpaceSelectionPoint; // todo: unsure if correct
}
///
@@ -405,16 +401,19 @@ namespace osu.Game.Screens.Edit.Compose.Components
Vector2 movePosition = movementBlueprintOriginalPosition.Value + e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition;
// Retrieve a snapped position.
- (Vector2 snappedPosition, double snappedTime) = snapProvider.GetSnappedPosition(ToLocalSpace(movePosition), draggedObject.StartTime);
+ var result = snapProvider.SnapScreenSpacePositionToValidTime(movePosition);
// Move the hitobjects.
- if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, ToScreenSpace(snappedPosition))))
+ if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, result.ScreenSpacePosition)))
return true;
- // Apply the start time at the newly snapped-to position
- double offset = snappedTime - draggedObject.StartTime;
- foreach (HitObject obj in selectionHandler.SelectedHitObjects)
- obj.StartTime += offset;
+ if (result.Time.HasValue)
+ {
+ // Apply the start time at the newly snapped-to position
+ double offset = result.Time.Value - draggedObject.StartTime;
+ foreach (HitObject obj in selectionHandler.SelectedHitObjects)
+ obj.StartTime += offset;
+ }
return true;
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
index 0eb77a8561..0b5d8262fd 100644
--- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs
@@ -11,7 +11,6 @@ using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
-using osuTK;
namespace osu.Game.Screens.Edit.Compose.Components
{
@@ -65,12 +64,11 @@ namespace osu.Game.Screens.Edit.Compose.Components
createPlacement();
}
- private void updatePlacementPosition(Vector2 screenSpacePosition)
+ private void updatePlacementPosition()
{
- Vector2 snappedGridPosition = composer.GetSnappedPosition(ToLocalSpace(screenSpacePosition), 0).position;
- Vector2 snappedScreenSpacePosition = ToScreenSpace(snappedGridPosition);
+ var snapResult = composer.SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position);
- currentPlacement.UpdatePosition(snappedScreenSpacePosition);
+ currentPlacement.UpdatePosition(snapResult);
}
#endregion
@@ -85,7 +83,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
removePlacement();
if (currentPlacement != null)
- updatePlacementPosition(inputManager.CurrentState.Mouse.Position);
+ updatePlacementPosition();
}
protected sealed override SelectionBlueprint CreateBlueprintFor(HitObject hitObject)
@@ -117,7 +115,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
placementBlueprintContainer.Child = currentPlacement = blueprint;
// Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame
- updatePlacementPosition(inputManager.CurrentState.Mouse.Position);
+ updatePlacementPosition();
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
index 3a42938fc1..8a92a2011d 100644
--- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
@@ -43,7 +43,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected OsuColour Colours { get; private set; }
[Resolved]
- protected IDistanceSnapProvider SnapProvider { get; private set; }
+ protected IPositionSnapProvider SnapProvider { get; private set; }
[Resolved]
private EditorBeatmap beatmap { get; set; }
diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
index 764eae1056..38893f90a8 100644
--- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs
@@ -38,7 +38,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private Drawable outline;
[Resolved(CanBeNull = true)]
- private EditorBeatmap editorBeatmap { get; set; }
+ protected EditorBeatmap EditorBeatmap { get; private set; }
[Resolved(CanBeNull = true)]
private IEditorChangeHandler changeHandler { get; set; }
@@ -74,9 +74,16 @@ namespace osu.Game.Screens.Edit.Compose.Components
///
/// Handles the selected s being moved.
///
+ ///
+ /// Just returning true is enough to allow updates to take place.
+ /// Custom implementation is only required if other attributes are to be considered, like changing columns.
+ ///
/// The move event.
- /// Whether any s were moved.
- public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => false;
+ ///
+ /// Whether any s could be moved.
+ /// Returning true will also propagate StartTime changes provided by the closest .
+ ///
+ public virtual bool HandleMovement(MoveSelectionEvent moveEvent) => true;
public bool OnPressed(PlatformAction action)
{
@@ -110,7 +117,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
internal void HandleSelected(SelectionBlueprint blueprint)
{
selectedBlueprints.Add(blueprint);
- editorBeatmap.SelectedHitObjects.Add(blueprint.HitObject);
+ EditorBeatmap.SelectedHitObjects.Add(blueprint.HitObject);
UpdateVisibility();
}
@@ -122,7 +129,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
internal void HandleDeselected(SelectionBlueprint blueprint)
{
selectedBlueprints.Remove(blueprint);
- editorBeatmap.SelectedHitObjects.Remove(blueprint.HitObject);
+ EditorBeatmap.SelectedHitObjects.Remove(blueprint.HitObject);
// We don't want to update visibility if > 0, since we may be deselecting blueprints during drag-selection
if (selectedBlueprints.Count == 0)
@@ -158,7 +165,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
changeHandler?.BeginChange();
foreach (var h in selectedBlueprints.ToList())
- editorBeatmap?.Remove(h.HitObject);
+ EditorBeatmap?.Remove(h.HitObject);
changeHandler?.EndChange();
}
@@ -244,14 +251,21 @@ namespace osu.Game.Screens.Edit.Compose.Components
#region Context Menu
- public virtual MenuItem[] ContextMenuItems
+ public MenuItem[] ContextMenuItems
{
get
{
if (!selectedBlueprints.Any(b => b.IsHovered))
return Array.Empty