diff --git a/osu.Android.props b/osu.Android.props
index c57fc342ba..46fd5424df 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -62,6 +62,6 @@
-
+
diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs
index 761f52f961..7725ee6451 100644
--- a/osu.Desktop/OsuGameDesktop.cs
+++ b/osu.Desktop/OsuGameDesktop.cs
@@ -17,6 +17,7 @@ using osu.Framework.Logging;
using osu.Framework.Platform.Windows;
using osu.Framework.Screens;
using osu.Game.Screens.Menu;
+using osu.Game.Updater;
namespace osu.Desktop
{
diff --git a/osu.Desktop/Overlays/VersionManager.cs b/osu.Desktop/Overlays/VersionManager.cs
index 51e801c185..6eed46867a 100644
--- a/osu.Desktop/Overlays/VersionManager.cs
+++ b/osu.Desktop/Overlays/VersionManager.cs
@@ -8,11 +8,8 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game;
-using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
-using osu.Game.Overlays;
-using osu.Game.Overlays.Notifications;
using osuTK;
using osuTK.Graphics;
@@ -20,17 +17,9 @@ namespace osu.Desktop.Overlays
{
public class VersionManager : OverlayContainer
{
- private OsuConfigManager config;
- private OsuGameBase game;
- private NotificationOverlay notificationOverlay;
-
[BackgroundDependencyLoader]
- private void load(NotificationOverlay notification, OsuColour colours, TextureStore textures, OsuGameBase game, OsuConfigManager config)
+ private void load(OsuColour colours, TextureStore textures, OsuGameBase game)
{
- notificationOverlay = notification;
- this.config = config;
- this.game = game;
-
AutoSizeAxes = Axes.Both;
Anchor = Anchor.BottomCentre;
Origin = Anchor.BottomCentre;
@@ -85,48 +74,6 @@ namespace osu.Desktop.Overlays
};
}
- protected override void LoadComplete()
- {
- base.LoadComplete();
-
- var version = game.Version;
- var lastVersion = config.Get(OsuSetting.Version);
-
- if (game.IsDeployedBuild && version != lastVersion)
- {
- config.Set(OsuSetting.Version, version);
-
- // only show a notification if we've previously saved a version to the config file (ie. not the first run).
- if (!string.IsNullOrEmpty(lastVersion))
- notificationOverlay.Post(new UpdateCompleteNotification(version));
- }
- }
-
- private class UpdateCompleteNotification : SimpleNotification
- {
- private readonly string version;
-
- public UpdateCompleteNotification(string version)
- {
- this.version = version;
- Text = $"You are now running osu!lazer {version}.\nClick to see what's new!";
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours, ChangelogOverlay changelog, NotificationOverlay notificationOverlay)
- {
- Icon = FontAwesome.Solid.CheckSquare;
- IconBackgound.Colour = colours.BlueDark;
-
- Activated = delegate
- {
- notificationOverlay.Hide();
- changelog.ShowBuild(OsuGameBase.CLIENT_STREAM_NAME, version);
- return true;
- };
- }
- }
-
protected override void PopIn()
{
this.FadeIn(1400, Easing.OutQuint);
diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs
index fa41c061b5..60b47a8b3a 100644
--- a/osu.Desktop/Updater/SquirrelUpdateManager.cs
+++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs
@@ -20,7 +20,7 @@ using LogLevel = Splat.LogLevel;
namespace osu.Desktop.Updater
{
- public class SquirrelUpdateManager : Component
+ public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager
{
private UpdateManager updateManager;
private NotificationOverlay notificationOverlay;
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index 538aaf2d7a..2461351110 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -23,10 +23,10 @@
-
+
-
+
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
index e7fd601abe..d5fd2808b8 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneStage.cs
@@ -15,7 +15,6 @@ using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
using osu.Game.Rulesets.Mods;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
using osuTK;
@@ -67,6 +66,8 @@ namespace osu.Game.Rulesets.Mania.Tests
AddAssert("check note anchors", () => notesInStageAreAnchored(stages[0], Anchor.TopCentre));
AddAssert("check note anchors", () => notesInStageAreAnchored(stages[1], Anchor.BottomCentre));
+ AddAssert("check bar anchors", () => barsInStageAreAnchored(stages[0], Anchor.TopCentre));
+ AddAssert("check bar anchors", () => barsInStageAreAnchored(stages[1], Anchor.BottomCentre));
AddStep("flip direction", () =>
{
@@ -76,10 +77,14 @@ namespace osu.Game.Rulesets.Mania.Tests
AddAssert("check note anchors", () => notesInStageAreAnchored(stages[0], Anchor.BottomCentre));
AddAssert("check note anchors", () => notesInStageAreAnchored(stages[1], Anchor.TopCentre));
+ AddAssert("check bar anchors", () => barsInStageAreAnchored(stages[0], Anchor.BottomCentre));
+ AddAssert("check bar anchors", () => barsInStageAreAnchored(stages[1], Anchor.TopCentre));
}
private bool notesInStageAreAnchored(ManiaStage stage, Anchor anchor) => stage.Columns.SelectMany(c => c.AllHitObjects).All(o => o.Anchor == anchor);
+ private bool barsInStageAreAnchored(ManiaStage stage, Anchor anchor) => stage.AllHitObjects.Where(obj => obj is DrawableBarLine).All(o => o.Anchor == anchor);
+
private void createNote()
{
foreach (var stage in stages)
diff --git a/osu.Game.Rulesets.Mania/Objects/BarLine.cs b/osu.Game.Rulesets.Mania/Objects/BarLine.cs
new file mode 100644
index 0000000000..0981b028b2
--- /dev/null
+++ b/osu.Game.Rulesets.Mania/Objects/BarLine.cs
@@ -0,0 +1,12 @@
+// 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.Objects;
+
+namespace osu.Game.Rulesets.Mania.Objects
+{
+ public class BarLine : ManiaHitObject, IBarLine
+ {
+ public bool Major { get; set; }
+ }
+}
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
index be21610525..56bc797c7f 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs
@@ -4,7 +4,6 @@
using osuTK;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK.Graphics;
@@ -14,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// Visualises a . Although this derives DrawableManiaHitObject,
/// this does not handle input/sound like a normal hit object.
///
- public class DrawableBarLine : DrawableHitObject
+ public class DrawableBarLine : DrawableManiaHitObject
{
///
/// Height of major bar line triangles.
diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
index 29863fba2e..d371c1f7a8 100644
--- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
+++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs
@@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mania.UI
public DrawableManiaRuleset(Ruleset ruleset, IWorkingBeatmap beatmap, IReadOnlyList mods)
: base(ruleset, beatmap, mods)
{
- BarLines = new BarLineGenerator(Beatmap).BarLines;
+ BarLines = new BarLineGenerator(Beatmap).BarLines;
}
[BackgroundDependencyLoader]
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
index 12faa499ad..5ab07416a6 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs
@@ -8,7 +8,6 @@ using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osuTK;
diff --git a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs
index 98a4b7d0b6..a28de7ea58 100644
--- a/osu.Game.Rulesets.Mania/UI/ManiaStage.cs
+++ b/osu.Game.Rulesets.Mania/UI/ManiaStage.cs
@@ -12,7 +12,6 @@ using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Objects.Drawables;
-using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
index b32dfd483f..80291c002e 100644
--- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
+++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs
@@ -40,9 +40,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
for (int i = 0; i < max_sprites; i++)
{
- // InvalidationID 1 forces an update of each part of the cursor trail the first time ApplyState is run on the draw node
- // This is to prevent garbage data from being sent to the vertex shader, resulting in visual issues on some platforms
- parts[i].InvalidationID = 1;
+ // -1 signals that the part is unusable, and should not be drawn
+ parts[i].InvalidationID = -1;
}
}
@@ -112,7 +111,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
for (int i = 0; i < parts.Length; ++i)
{
parts[i].Time -= time;
- ++parts[i].InvalidationID;
+
+ if (parts[i].InvalidationID != -1)
+ ++parts[i].InvalidationID;
}
time = 0;
@@ -205,8 +206,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
public TrailDrawNode(CursorTrail source)
: base(source)
{
- for (int i = 0; i < max_sprites; i++)
- parts[i].InvalidationID = 0;
}
public override void ApplyState()
@@ -218,11 +217,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
size = Source.partSize;
time = Source.time;
- for (int i = 0; i < Source.parts.Length; ++i)
- {
- if (Source.parts[i].InvalidationID > parts[i].InvalidationID)
- parts[i] = Source.parts[i];
- }
+ Source.parts.CopyTo(parts, 0);
}
public override void Draw(Action vertexAction)
@@ -234,6 +229,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
for (int i = 0; i < parts.Length; ++i)
{
+ if (parts[i].InvalidationID == -1)
+ continue;
+
vertexBatch.DrawTime = parts[i].Time;
Vector2 pos = parts[i].Position;
diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs
index 3aa461e779..cbbf5b0c09 100644
--- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs
+++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoPlayfield.cs
@@ -53,6 +53,11 @@ namespace osu.Game.Rulesets.Taiko.Tests
AddStep("Strong Rim", () => addRimHit(true));
AddStep("Add bar line", () => addBarLine(false));
AddStep("Add major bar line", () => addBarLine(true));
+ AddStep("Add centre w/ bar line", () =>
+ {
+ addCentreHit(false);
+ addBarLine(true);
+ });
AddStep("Height test 1", () => changePlayfieldSize(1));
AddStep("Height test 2", () => changePlayfieldSize(2));
AddStep("Height test 3", () => changePlayfieldSize(3));
diff --git a/osu.Game.Rulesets.Taiko/Objects/BarLine.cs b/osu.Game.Rulesets.Taiko/Objects/BarLine.cs
new file mode 100644
index 0000000000..2afbbc737c
--- /dev/null
+++ b/osu.Game.Rulesets.Taiko/Objects/BarLine.cs
@@ -0,0 +1,12 @@
+// 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.Objects;
+
+namespace osu.Game.Rulesets.Taiko.Objects
+{
+ public class BarLine : TaikoHitObject, IBarLine
+ {
+ public bool Major { get; set; }
+ }
+}
diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs
index f5b75a781b..4d3a1a3f8a 100644
--- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs
+++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs
@@ -5,7 +5,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osuTK;
using osu.Framework.Graphics.Shapes;
-using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
index 5caa9e4626..fc109bf6a6 100644
--- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
+++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.UI
[BackgroundDependencyLoader]
private void load()
{
- new BarLineGenerator(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar.Major ? new DrawableBarLineMajor(bar) : new DrawableBarLine(bar)));
+ new BarLineGenerator(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar.Major ? new DrawableBarLineMajor(bar) : new DrawableBarLine(bar)));
}
public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(this);
diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs
new file mode 100644
index 0000000000..30686cb947
--- /dev/null
+++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs
@@ -0,0 +1,201 @@
+// 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.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Screens.Select;
+using osu.Game.Screens.Select.Carousel;
+
+namespace osu.Game.Tests.NonVisual.Filtering
+{
+ [TestFixture]
+ public class FilterMatchingTest
+ {
+ private BeatmapInfo getExampleBeatmap() => new BeatmapInfo
+ {
+ Ruleset = new RulesetInfo { ID = 5 },
+ StarDifficulty = 4.0d,
+ BaseDifficulty = new BeatmapDifficulty
+ {
+ ApproachRate = 5.0f,
+ DrainRate = 3.0f,
+ CircleSize = 2.0f,
+ },
+ Metadata = new BeatmapMetadata
+ {
+ Artist = "The Artist",
+ ArtistUnicode = "check unicode too",
+ Title = "Title goes here",
+ TitleUnicode = "Title goes here",
+ AuthorString = "The Author",
+ Source = "unit tests",
+ Tags = "look for tags too",
+ },
+ Version = "version as well",
+ Length = 2500,
+ BPM = 160,
+ BeatDivisor = 12,
+ Status = BeatmapSetOnlineStatus.Loved
+ };
+
+ [Test]
+ public void TestCriteriaMatchingNoRuleset()
+ {
+ var exampleBeatmapInfo = getExampleBeatmap();
+ var criteria = new FilterCriteria();
+ var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
+ carouselItem.Filter(criteria);
+ Assert.IsFalse(carouselItem.Filtered.Value);
+ }
+
+ [Test]
+ public void TestCriteriaMatchingSpecificRuleset()
+ {
+ var exampleBeatmapInfo = getExampleBeatmap();
+ var criteria = new FilterCriteria
+ {
+ Ruleset = new RulesetInfo { ID = 6 }
+ };
+ var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
+ carouselItem.Filter(criteria);
+ Assert.IsTrue(carouselItem.Filtered.Value);
+ }
+
+ [Test]
+ public void TestCriteriaMatchingConvertedBeatmaps()
+ {
+ var exampleBeatmapInfo = getExampleBeatmap();
+ var criteria = new FilterCriteria
+ {
+ Ruleset = new RulesetInfo { ID = 6 },
+ AllowConvertedBeatmaps = true
+ };
+ var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
+ carouselItem.Filter(criteria);
+ Assert.IsFalse(carouselItem.Filtered.Value);
+ }
+
+ [Test]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void TestCriteriaMatchingRangeMin(bool inclusive)
+ {
+ var exampleBeatmapInfo = getExampleBeatmap();
+ var criteria = new FilterCriteria
+ {
+ Ruleset = new RulesetInfo { ID = 6 },
+ AllowConvertedBeatmaps = true,
+ ApproachRate = new FilterCriteria.OptionalRange
+ {
+ IsLowerInclusive = inclusive,
+ Min = 5.0f
+ }
+ };
+ var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
+ carouselItem.Filter(criteria);
+ Assert.AreEqual(!inclusive, carouselItem.Filtered.Value);
+ }
+
+ [Test]
+ [TestCase(true)]
+ [TestCase(false)]
+ public void TestCriteriaMatchingRangeMax(bool inclusive)
+ {
+ var exampleBeatmapInfo = getExampleBeatmap();
+ var criteria = new FilterCriteria
+ {
+ Ruleset = new RulesetInfo { ID = 6 },
+ AllowConvertedBeatmaps = true,
+ BPM = new FilterCriteria.OptionalRange
+ {
+ IsUpperInclusive = inclusive,
+ Max = 160d
+ }
+ };
+ var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
+ carouselItem.Filter(criteria);
+ Assert.AreEqual(!inclusive, carouselItem.Filtered.Value);
+ }
+
+ [Test]
+ [TestCase("artist", false)]
+ [TestCase("artist title author", false)]
+ [TestCase("an artist", true)]
+ [TestCase("tags too", false)]
+ [TestCase("version", false)]
+ [TestCase("an auteur", true)]
+ public void TestCriteriaMatchingTerms(string terms, bool filtered)
+ {
+ var exampleBeatmapInfo = getExampleBeatmap();
+ var criteria = new FilterCriteria
+ {
+ Ruleset = new RulesetInfo { ID = 6 },
+ AllowConvertedBeatmaps = true,
+ SearchText = terms
+ };
+ var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
+ carouselItem.Filter(criteria);
+ Assert.AreEqual(filtered, carouselItem.Filtered.Value);
+ }
+
+ [Test]
+ [TestCase("", false)]
+ [TestCase("The", false)]
+ [TestCase("THE", false)]
+ [TestCase("author", false)]
+ [TestCase("the author", false)]
+ [TestCase("the author AND then something else", true)]
+ [TestCase("unknown", true)]
+ public void TestCriteriaMatchingCreator(string creatorName, bool filtered)
+ {
+ var exampleBeatmapInfo = getExampleBeatmap();
+ var criteria = new FilterCriteria
+ {
+ Creator = new FilterCriteria.OptionalTextFilter { SearchTerm = creatorName }
+ };
+ var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
+ carouselItem.Filter(criteria);
+ Assert.AreEqual(filtered, carouselItem.Filtered.Value);
+ }
+
+ [Test]
+ [TestCase("", false)]
+ [TestCase("The", false)]
+ [TestCase("THE", false)]
+ [TestCase("artist", false)]
+ [TestCase("the artist", false)]
+ [TestCase("the artist AND then something else", true)]
+ [TestCase("unicode too", false)]
+ [TestCase("unknown", true)]
+ public void TestCriteriaMatchingArtist(string artistName, bool filtered)
+ {
+ var exampleBeatmapInfo = getExampleBeatmap();
+ var criteria = new FilterCriteria
+ {
+ Artist = new FilterCriteria.OptionalTextFilter { SearchTerm = artistName }
+ };
+ var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
+ carouselItem.Filter(criteria);
+ Assert.AreEqual(filtered, carouselItem.Filtered.Value);
+ }
+
+ [Test]
+ [TestCase("", false)]
+ [TestCase("artist", false)]
+ [TestCase("unknown", true)]
+ public void TestCriteriaMatchingArtistWithNullUnicodeName(string artistName, bool filtered)
+ {
+ var exampleBeatmapInfo = getExampleBeatmap();
+ exampleBeatmapInfo.Metadata.ArtistUnicode = null;
+
+ var criteria = new FilterCriteria
+ {
+ Artist = new FilterCriteria.OptionalTextFilter { SearchTerm = artistName }
+ };
+ var carouselItem = new CarouselBeatmap(exampleBeatmapInfo);
+ carouselItem.Filter(criteria);
+ Assert.AreEqual(filtered, carouselItem.Filtered.Value);
+ }
+ }
+}
diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
new file mode 100644
index 0000000000..9869ddde41
--- /dev/null
+++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs
@@ -0,0 +1,184 @@
+// 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.Game.Beatmaps;
+using osu.Game.Screens.Select;
+
+namespace osu.Game.Tests.NonVisual.Filtering
+{
+ [TestFixture]
+ public class FilterQueryParserTest
+ {
+ [Test]
+ public void TestApplyQueriesBareWords()
+ {
+ const string query = "looking for a beatmap";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("looking for a beatmap", filterCriteria.SearchText);
+ Assert.AreEqual(4, filterCriteria.SearchTerms.Length);
+ }
+
+ /*
+ * The following tests have been written a bit strangely (they don't check exact
+ * bound equality with what the filter says).
+ * This is to account for floating-point arithmetic issues.
+ * For example, specifying a bpm<140 filter would previously match beatmaps with BPM
+ * of 139.99999, which would be displayed in the UI as 140.
+ * Due to this the tests check the last tick inside the range and the first tick
+ * outside of the range.
+ */
+
+ [Test]
+ public void TestApplyStarQueries()
+ {
+ const string query = "stars<4 easy";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("easy", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
+ Assert.IsNotNull(filterCriteria.StarDifficulty.Max);
+ Assert.Greater(filterCriteria.StarDifficulty.Max, 3.99d);
+ Assert.Less(filterCriteria.StarDifficulty.Max, 4.00d);
+ Assert.IsNull(filterCriteria.StarDifficulty.Min);
+ }
+
+ [Test]
+ public void TestApplyApproachRateQueries()
+ {
+ const string query = "ar>=9 difficult";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("difficult", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
+ Assert.IsNotNull(filterCriteria.ApproachRate.Min);
+ Assert.Greater(filterCriteria.ApproachRate.Min, 8.9f);
+ Assert.Less(filterCriteria.ApproachRate.Min, 9.0f);
+ Assert.IsNull(filterCriteria.ApproachRate.Max);
+ }
+
+ [Test]
+ public void TestApplyDrainRateQueries()
+ {
+ const string query = "dr>2 quite specific dr<:6";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("quite specific", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(2, filterCriteria.SearchTerms.Length);
+ Assert.Greater(filterCriteria.DrainRate.Min, 2.0f);
+ Assert.Less(filterCriteria.DrainRate.Min, 2.1f);
+ Assert.Greater(filterCriteria.DrainRate.Max, 6.0f);
+ Assert.Less(filterCriteria.DrainRate.Min, 6.1f);
+ }
+
+ [Test]
+ public void TestApplyBPMQueries()
+ {
+ const string query = "bpm>:200 gotta go fast";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("gotta go fast", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
+ Assert.IsNotNull(filterCriteria.BPM.Min);
+ Assert.Greater(filterCriteria.BPM.Min, 199.99d);
+ Assert.Less(filterCriteria.BPM.Min, 200.00d);
+ Assert.IsNull(filterCriteria.BPM.Max);
+ }
+
+ private static object[] lengthQueryExamples =
+ {
+ new object[] { "6ms", TimeSpan.FromMilliseconds(6), TimeSpan.FromMilliseconds(1) },
+ new object[] { "23s", TimeSpan.FromSeconds(23), TimeSpan.FromSeconds(1) },
+ new object[] { "9m", TimeSpan.FromMinutes(9), TimeSpan.FromMinutes(1) },
+ new object[] { "0.25h", TimeSpan.FromHours(0.25), TimeSpan.FromHours(1) },
+ new object[] { "70", TimeSpan.FromSeconds(70), TimeSpan.FromSeconds(1) },
+ };
+
+ [Test]
+ [TestCaseSource(nameof(lengthQueryExamples))]
+ public void TestApplyLengthQueries(string lengthQuery, TimeSpan expectedLength, TimeSpan scale)
+ {
+ string query = $"length={lengthQuery} time";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("time", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
+ Assert.AreEqual(expectedLength.TotalMilliseconds - scale.TotalMilliseconds / 2.0, filterCriteria.Length.Min);
+ Assert.AreEqual(expectedLength.TotalMilliseconds + scale.TotalMilliseconds / 2.0, filterCriteria.Length.Max);
+ }
+
+ [Test]
+ public void TestApplyDivisorQueries()
+ {
+ const string query = "that's a time signature alright! divisor:12";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("that's a time signature alright!", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
+ Assert.AreEqual(12, filterCriteria.BeatDivisor.Min);
+ Assert.IsTrue(filterCriteria.BeatDivisor.IsLowerInclusive);
+ Assert.AreEqual(12, filterCriteria.BeatDivisor.Max);
+ Assert.IsTrue(filterCriteria.BeatDivisor.IsUpperInclusive);
+ }
+
+ [Test]
+ public void TestApplyStatusQueries()
+ {
+ const string query = "I want the pp status=ranked";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("I want the pp", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(4, filterCriteria.SearchTerms.Length);
+ Assert.AreEqual(BeatmapSetOnlineStatus.Ranked, filterCriteria.OnlineStatus.Min);
+ Assert.IsTrue(filterCriteria.OnlineStatus.IsLowerInclusive);
+ Assert.AreEqual(BeatmapSetOnlineStatus.Ranked, filterCriteria.OnlineStatus.Max);
+ Assert.IsTrue(filterCriteria.OnlineStatus.IsUpperInclusive);
+ }
+
+ [Test]
+ public void TestApplyCreatorQueries()
+ {
+ const string query = "beatmap specifically by creator=my_fav";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("beatmap specifically by", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
+ Assert.AreEqual("my_fav", filterCriteria.Creator.SearchTerm);
+ }
+
+ [Test]
+ public void TestApplyArtistQueries()
+ {
+ const string query = "find me songs by artist=singer please";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("find me songs by please", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(5, filterCriteria.SearchTerms.Length);
+ Assert.AreEqual("singer", filterCriteria.Artist.SearchTerm);
+ }
+
+ [Test]
+ public void TestApplyArtistQueriesWithSpaces()
+ {
+ const string query = "really like artist=\"name with space\" yes";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("really like yes", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(3, filterCriteria.SearchTerms.Length);
+ Assert.AreEqual("name with space", filterCriteria.Artist.SearchTerm);
+ }
+
+ [Test]
+ public void TestApplyArtistQueriesOneDoubleQuote()
+ {
+ const string query = "weird artist=double\"quote";
+ var filterCriteria = new FilterCriteria();
+ FilterQueryParser.ApplyQueries(filterCriteria, query);
+ Assert.AreEqual("weird", filterCriteria.SearchText.Trim());
+ Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
+ Assert.AreEqual("double\"quote", filterCriteria.Artist.SearchTerm);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
index bbcc4140a9..578030748b 100644
--- a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
+++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
@@ -9,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
+using osu.Framework.Testing;
using osu.Game.Audio;
using osu.Game.Skinning;
using osu.Game.Tests.Visual;
@@ -17,6 +18,7 @@ using osuTK.Graphics;
namespace osu.Game.Tests.Skins
{
[TestFixture]
+ [HeadlessTest]
public class TestSceneSkinConfigurationLookup : OsuTestScene
{
private LegacySkin source1;
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
index 6669ec7da3..f12a613bf1 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs
@@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private readonly Stack selectedSets = new Stack();
private readonly HashSet eagerSelectedIDs = new HashSet();
- private BeatmapInfo currentSelection;
+ private BeatmapInfo currentSelection => carousel.SelectedBeatmap;
private const int set_count = 5;
@@ -56,37 +56,26 @@ namespace osu.Game.Tests.Visual.SongSelect
{
RelativeSizeAxes = Axes.Both,
});
-
- List beatmapSets = new List();
-
- for (int i = 1; i <= set_count; i++)
- beatmapSets.Add(createTestBeatmapSet(i));
-
- carousel.SelectionChanged = s => currentSelection = s;
-
- loadBeatmaps(beatmapSets);
-
- testTraversal();
- testFiltering();
- testRandom();
- testAddRemove();
- testSorting();
-
- testRemoveAll();
- testEmptyTraversal();
- testHiding();
- testSelectingFilteredRuleset();
- testCarouselRootIsRandom();
}
- private void loadBeatmaps(List beatmapSets)
+ private void loadBeatmaps(List beatmapSets = null)
{
+ if (beatmapSets == null)
+ {
+ beatmapSets = new List();
+
+ for (int i = 1; i <= set_count; i++)
+ beatmapSets.Add(createTestBeatmapSet(i));
+ }
+
bool changed = false;
AddStep($"Load {beatmapSets.Count} Beatmaps", () =>
{
+ carousel.Filter(new FilterCriteria());
carousel.BeatmapSetsChanged = () => changed = true;
carousel.BeatmapSets = beatmapSets;
});
+
AddUntilStep("Wait for load", () => changed);
}
@@ -173,8 +162,11 @@ namespace osu.Game.Tests.Visual.SongSelect
///
/// Test keyboard traversal
///
- private void testTraversal()
+ [Test]
+ public void TestTraversal()
{
+ loadBeatmaps();
+
advanceSelection(direction: 1, diff: false);
checkSelected(1, 1);
@@ -199,8 +191,11 @@ namespace osu.Game.Tests.Visual.SongSelect
///
/// Test filtering
///
- private void testFiltering()
+ [Test]
+ public void TestFiltering()
{
+ loadBeatmaps();
+
// basic filtering
setSelected(1, 1);
@@ -242,13 +237,31 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
AddAssert("Selection is non-null", () => currentSelection != null);
+
+ setSelected(1, 3);
+ AddStep("Apply a range filter", () => carousel.Filter(new FilterCriteria
+ {
+ SearchText = "#3",
+ StarDifficulty = new FilterCriteria.OptionalRange
+ {
+ Min = 2,
+ Max = 5.5,
+ IsLowerInclusive = true
+ }
+ }, false));
+ checkSelected(3, 2);
+
+ AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
}
///
/// Test random non-repeating algorithm
///
- private void testRandom()
+ [Test]
+ public void TestRandom()
{
+ loadBeatmaps();
+
setSelected(1, 1);
nextRandom();
@@ -284,8 +297,11 @@ namespace osu.Game.Tests.Visual.SongSelect
///
/// Test adding and removing beatmap sets
///
- private void testAddRemove()
+ [Test]
+ public void TestAddRemove()
{
+ loadBeatmaps();
+
AddStep("Add new set", () => carousel.UpdateBeatmapSet(createTestBeatmapSet(set_count + 1)));
AddStep("Add new set", () => carousel.UpdateBeatmapSet(createTestBeatmapSet(set_count + 2)));
@@ -307,16 +323,22 @@ namespace osu.Game.Tests.Visual.SongSelect
///
/// Test sorting
///
- private void testSorting()
+ [Test]
+ public void TestSorting()
{
+ loadBeatmaps();
+
AddStep("Sort by author", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Author }, false));
AddAssert("Check zzzzz is at bottom", () => carousel.BeatmapSets.Last().Metadata.AuthorString == "zzzzz");
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
AddAssert($"Check #{set_count} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Title.EndsWith($"#{set_count}!"));
}
- private void testRemoveAll()
+ [Test]
+ public void TestRemoveAll()
{
+ loadBeatmaps();
+
setSelected(2, 1);
AddAssert("Selection is non-null", () => currentSelection != null);
@@ -338,8 +360,11 @@ namespace osu.Game.Tests.Visual.SongSelect
checkNoSelection();
}
- private void testEmptyTraversal()
+ [Test]
+ public void TestEmptyTraversal()
{
+ loadBeatmaps(new List());
+
advanceSelection(direction: 1, diff: false);
checkNoSelection();
@@ -353,11 +378,14 @@ namespace osu.Game.Tests.Visual.SongSelect
checkNoSelection();
}
- private void testHiding()
+ [Test]
+ public void TestHiding()
{
- var hidingSet = createTestBeatmapSet(1);
+ BeatmapSetInfo hidingSet = createTestBeatmapSet(1);
hidingSet.Beatmaps[1].Hidden = true;
- AddStep("Add set with diff 2 hidden", () => carousel.UpdateBeatmapSet(hidingSet));
+
+ loadBeatmaps(new List { hidingSet });
+
setSelected(1, 1);
checkVisibleItemCount(true, 2);
@@ -387,7 +415,8 @@ namespace osu.Game.Tests.Visual.SongSelect
}
}
- private void testSelectingFilteredRuleset()
+ [Test]
+ public void TestSelectingFilteredRuleset()
{
var testMixed = createTestBeatmapSet(set_count + 1);
AddStep("add mixed ruleset beatmapset", () =>
@@ -422,14 +451,16 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("remove single ruleset set", () => carousel.RemoveBeatmapSet(testSingle));
}
- private void testCarouselRootIsRandom()
+ [Test]
+ public void TestCarouselRootIsRandom()
{
- List beatmapSets = new List();
+ List manySets = new List();
for (int i = 1; i <= 50; i++)
- beatmapSets.Add(createTestBeatmapSet(i));
+ manySets.Add(createTestBeatmapSet(i));
+
+ loadBeatmaps(manySets);
- loadBeatmaps(beatmapSets);
advanceSelection(direction: 1, diff: false);
checkNonmatchingFilter();
checkNonmatchingFilter();
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledComponent.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledComponent.cs
index 73e0191adb..700adad9cb 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledComponent.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledComponent.cs
@@ -6,7 +6,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Sprites;
-using osu.Game.Screens.Edit.Setup.Components.LabelledComponents;
+using osu.Game.Graphics.UserInterfaceV2;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.UserInterface
@@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep("create component", () =>
{
- LabelledComponent component;
+ LabelledComponent component;
Child = new Container
{
@@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Origin = Anchor.Centre,
Width = 500,
AutoSizeAxes = Axes.Y,
- Child = component = padded ? (LabelledComponent)new PaddedLabelledComponent() : new NonPaddedLabelledComponent(),
+ Child = component = padded ? (LabelledComponent)new PaddedLabelledComponent() : new NonPaddedLabelledComponent(),
};
component.Label = "a sample component";
@@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.UserInterface
});
}
- private class PaddedLabelledComponent : LabelledComponent
+ private class PaddedLabelledComponent : LabelledComponent
{
public PaddedLabelledComponent()
: base(true)
@@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.UserInterface
};
}
- private class NonPaddedLabelledComponent : LabelledComponent
+ private class NonPaddedLabelledComponent : LabelledComponent
{
public NonPaddedLabelledComponent()
: base(false)
diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs
index 395905a30d..53a2bfabbc 100644
--- a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs
+++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledTextBox.cs
@@ -7,7 +7,8 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
-using osu.Game.Screens.Edit.Setup.Components.LabelledComponents;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Tests.Visual.UserInterface
{
@@ -19,6 +20,36 @@ namespace osu.Game.Tests.Visual.UserInterface
typeof(LabelledTextBox),
};
+ [TestCase(false)]
+ [TestCase(true)]
+ public void TestTextBox(bool hasDescription) => createTextBox(hasDescription);
+
+ private void createTextBox(bool hasDescription = false)
+ {
+ AddStep("create component", () =>
+ {
+ LabelledComponent component;
+
+ Child = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Width = 500,
+ AutoSizeAxes = Axes.Y,
+ Child = component = new LabelledTextBox
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Label = "Testing text",
+ PlaceholderText = "This is definitely working as intended",
+ }
+ };
+
+ component.Label = "a sample component";
+ component.Description = hasDescription ? "this text describes the component" : string.Empty;
+ });
+ }
+
[BackgroundDependencyLoader]
private void load()
{
@@ -32,7 +63,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
- LabelText = "Testing text",
+ Label = "Testing text",
PlaceholderText = "This is definitely working as intended",
}
};
diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneSetupScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneSetupScreen.cs
new file mode 100644
index 0000000000..650b4c5412
--- /dev/null
+++ b/osu.Game.Tournament.Tests/Screens/TestSceneSetupScreen.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 osu.Framework.Allocation;
+using osu.Game.Tournament.Screens;
+
+namespace osu.Game.Tournament.Tests.Screens
+{
+ public class TestSceneSetupScreen : TournamentTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Add(new SetupScreen());
+ }
+ }
+}
diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs
index 4fd858bd12..e05d96e098 100644
--- a/osu.Game.Tournament/IPC/FileBasedIPC.cs
+++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs
@@ -9,6 +9,7 @@ using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Platform.Windows;
+using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Online.API;
@@ -26,103 +27,120 @@ namespace osu.Game.Tournament.IPC
[Resolved]
protected RulesetStore Rulesets { get; private set; }
+ [Resolved]
+ private GameHost host { get; set; }
+
+ [Resolved]
+ private LadderInfo ladder { get; set; }
+
private int lastBeatmapId;
+ private ScheduledDelegate scheduled;
+
+ public Storage Storage { get; private set; }
[BackgroundDependencyLoader]
- private void load(LadderInfo ladder, GameHost host)
+ private void load()
{
- StableStorage stable;
+ LocateStableStorage();
+ }
+
+ public Storage LocateStableStorage()
+ {
+ scheduled?.Cancel();
+
+ Storage = null;
try
{
- stable = new StableStorage(host as DesktopGameHost);
+ Storage = new StableStorage(host as DesktopGameHost);
+
+ const string file_ipc_filename = "ipc.txt";
+ const string file_ipc_state_filename = "ipc-state.txt";
+ const string file_ipc_scores_filename = "ipc-scores.txt";
+ const string file_ipc_channel_filename = "ipc-channel.txt";
+
+ if (Storage.Exists(file_ipc_filename))
+ scheduled = Scheduler.AddDelayed(delegate
+ {
+ try
+ {
+ using (var stream = Storage.GetStream(file_ipc_filename))
+ using (var sr = new StreamReader(stream))
+ {
+ var beatmapId = int.Parse(sr.ReadLine());
+ var mods = int.Parse(sr.ReadLine());
+
+ if (lastBeatmapId != beatmapId)
+ {
+ lastBeatmapId = beatmapId;
+
+ var existing = ladder.CurrentMatch.Value?.Round.Value?.Beatmaps.FirstOrDefault(b => b.ID == beatmapId && b.BeatmapInfo != null);
+
+ if (existing != null)
+ Beatmap.Value = existing.BeatmapInfo;
+ else
+ {
+ var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = beatmapId });
+ req.Success += b => Beatmap.Value = b.ToBeatmap(Rulesets);
+ API.Queue(req);
+ }
+ }
+
+ Mods.Value = (LegacyMods)mods;
+ }
+ }
+ catch
+ {
+ // file might be in use.
+ }
+
+ try
+ {
+ using (var stream = Storage.GetStream(file_ipc_channel_filename))
+ using (var sr = new StreamReader(stream))
+ {
+ ChatChannel.Value = sr.ReadLine();
+ }
+ }
+ catch (Exception)
+ {
+ // file might be in use.
+ }
+
+ try
+ {
+ using (var stream = Storage.GetStream(file_ipc_state_filename))
+ using (var sr = new StreamReader(stream))
+ {
+ State.Value = (TourneyState)Enum.Parse(typeof(TourneyState), sr.ReadLine());
+ }
+ }
+ catch (Exception)
+ {
+ // file might be in use.
+ }
+
+ try
+ {
+ using (var stream = Storage.GetStream(file_ipc_scores_filename))
+ using (var sr = new StreamReader(stream))
+ {
+ Score1.Value = int.Parse(sr.ReadLine());
+ Score2.Value = int.Parse(sr.ReadLine());
+ }
+ }
+ catch (Exception)
+ {
+ // file might be in use.
+ }
+ }, 250, true);
}
catch (Exception e)
{
Logger.Error(e, "Stable installation could not be found; disabling file based IPC");
- return;
}
- const string file_ipc_filename = "ipc.txt";
- const string file_ipc_state_filename = "ipc-state.txt";
- const string file_ipc_scores_filename = "ipc-scores.txt";
- const string file_ipc_channel_filename = "ipc-channel.txt";
-
- if (stable.Exists(file_ipc_filename))
- Scheduler.AddDelayed(delegate
- {
- try
- {
- using (var stream = stable.GetStream(file_ipc_filename))
- using (var sr = new StreamReader(stream))
- {
- var beatmapId = int.Parse(sr.ReadLine());
- var mods = int.Parse(sr.ReadLine());
-
- if (lastBeatmapId != beatmapId)
- {
- lastBeatmapId = beatmapId;
-
- var existing = ladder.CurrentMatch.Value?.Round.Value?.Beatmaps.FirstOrDefault(b => b.ID == beatmapId && b.BeatmapInfo != null);
-
- if (existing != null)
- Beatmap.Value = existing.BeatmapInfo;
- else
- {
- var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = beatmapId });
- req.Success += b => Beatmap.Value = b.ToBeatmap(Rulesets);
- API.Queue(req);
- }
- }
-
- Mods.Value = (LegacyMods)mods;
- }
- }
- catch
- {
- // file might be in use.
- }
-
- try
- {
- using (var stream = stable.GetStream(file_ipc_channel_filename))
- using (var sr = new StreamReader(stream))
- {
- ChatChannel.Value = sr.ReadLine();
- }
- }
- catch (Exception)
- {
- // file might be in use.
- }
-
- try
- {
- using (var stream = stable.GetStream(file_ipc_state_filename))
- using (var sr = new StreamReader(stream))
- {
- State.Value = (TourneyState)Enum.Parse(typeof(TourneyState), sr.ReadLine());
- }
- }
- catch (Exception)
- {
- // file might be in use.
- }
-
- try
- {
- using (var stream = stable.GetStream(file_ipc_scores_filename))
- using (var sr = new StreamReader(stream))
- {
- Score1.Value = int.Parse(sr.ReadLine());
- Score2.Value = int.Parse(sr.ReadLine());
- }
- }
- catch (Exception)
- {
- // file might be in use.
- }
- }, 250, true);
+ return Storage;
}
///
diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs
new file mode 100644
index 0000000000..091a837745
--- /dev/null
+++ b/osu.Game.Tournament/Screens/SetupScreen.cs
@@ -0,0 +1,142 @@
+// 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.Allocation;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Game.Graphics.Sprites;
+using osu.Game.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterfaceV2;
+using osu.Game.Online.API;
+using osu.Game.Overlays;
+using osu.Game.Tournament.IPC;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Tournament.Screens
+{
+ public class SetupScreen : TournamentScreen, IProvideVideo
+ {
+ private FillFlowContainer fillFlow;
+
+ private LoginOverlay loginOverlay;
+
+ [Resolved]
+ private MatchIPCInfo ipc { get; set; }
+
+ [Resolved]
+ private IAPIProvider api { get; set; }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChild = fillFlow = new FillFlowContainer
+ {
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Direction = FillDirection.Vertical,
+ Padding = new MarginPadding(10),
+ Spacing = new Vector2(10),
+ };
+
+ api.LocalUser.BindValueChanged(_ => Schedule(reload));
+ reload();
+ }
+
+ private void reload()
+ {
+ var fileBasedIpc = ipc as FileBasedIPC;
+
+ fillFlow.Children = new Drawable[]
+ {
+ new ActionableInfo
+ {
+ Label = "Current IPC source",
+ ButtonText = "Refresh",
+ Action = () =>
+ {
+ fileBasedIpc?.LocateStableStorage();
+ reload();
+ },
+ Value = fileBasedIpc?.Storage?.GetFullPath(string.Empty) ?? "Not found",
+ Failing = fileBasedIpc?.Storage == null,
+ Description = "The osu!stable installation which is currently being used as a data source. If a source is not found, make sure you have created an empty ipc.txt in your stable cutting-edge installation, and that it is registered as the default osu! install."
+ },
+ new ActionableInfo
+ {
+ Label = "Current User",
+ ButtonText = "Change Login",
+ Action = () =>
+ {
+ api.Logout();
+
+ if (loginOverlay == null)
+ {
+ AddInternal(loginOverlay = new LoginOverlay
+ {
+ Anchor = Anchor.TopRight,
+ Origin = Anchor.TopRight,
+ });
+ }
+
+ loginOverlay.State.Value = Visibility.Visible;
+ },
+ Value = api?.LocalUser.Value.Username,
+ Failing = api?.IsLoggedIn != true,
+ Description = "In order to access the API and display metadata, a login is required."
+ }
+ };
+ }
+
+ private class ActionableInfo : LabelledComponent
+ {
+ private OsuButton button;
+
+ public ActionableInfo()
+ : base(true)
+ {
+ }
+
+ public string ButtonText
+ {
+ set => button.Text = value;
+ }
+
+ public string Value
+ {
+ set => valueText.Text = value;
+ }
+
+ public bool Failing
+ {
+ set => valueText.Colour = value ? Color4.Red : Color4.White;
+ }
+
+ public Action Action;
+
+ private OsuSpriteText valueText;
+
+ protected override Drawable CreateComponent() => new Container
+ {
+ AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.X,
+ Children = new Drawable[]
+ {
+ valueText = new OsuSpriteText
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreLeft,
+ },
+ button = new TriangleButton
+ {
+ Anchor = Anchor.CentreRight,
+ Origin = Anchor.CentreRight,
+ Size = new Vector2(100, 30),
+ Action = () => Action?.Invoke()
+ },
+ }
+ };
+ }
+ }
+}
diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs
index 4c255be463..02ee1c8603 100644
--- a/osu.Game.Tournament/TournamentSceneManager.cs
+++ b/osu.Game.Tournament/TournamentSceneManager.cs
@@ -69,6 +69,7 @@ namespace osu.Game.Tournament
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
+ new SetupScreen(),
new ScheduleScreen(),
new LadderScreen(),
new LadderEditorScreen(),
@@ -106,6 +107,8 @@ namespace osu.Game.Tournament
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
+ new OsuButton { RelativeSizeAxes = Axes.X, Text = "Setup", Action = () => SetScreen(typeof(SetupScreen)) },
+ new Container { RelativeSizeAxes = Axes.X, Height = 50 },
new OsuButton { RelativeSizeAxes = Axes.X, Text = "Team Editor", Action = () => SetScreen(typeof(TeamEditorScreen)) },
new OsuButton { RelativeSizeAxes = Axes.X, Text = "Rounds Editor", Action = () => SetScreen(typeof(RoundEditorScreen)) },
new OsuButton { RelativeSizeAxes = Axes.X, Text = "Bracket Editor", Action = () => SetScreen(typeof(LadderEditorScreen)) },
@@ -127,7 +130,7 @@ namespace osu.Game.Tournament
},
};
- SetScreen(typeof(ScheduleScreen));
+ SetScreen(typeof(SetupScreen));
}
public void SetScreen(Type screenType)
diff --git a/osu.Game.Tournament/osu.Game.Tournament.csproj b/osu.Game.Tournament/osu.Game.Tournament.csproj
index 4790fcbcde..bddaff0a80 100644
--- a/osu.Game.Tournament/osu.Game.Tournament.csproj
+++ b/osu.Game.Tournament/osu.Game.Tournament.csproj
@@ -11,6 +11,6 @@
-
+
\ No newline at end of file
diff --git a/osu.Game/Screens/Edit/Setup/Components/LabelledComponents/LabelledComponent.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledComponent.cs
similarity index 93%
rename from osu.Game/Screens/Edit/Setup/Components/LabelledComponents/LabelledComponent.cs
rename to osu.Game/Graphics/UserInterfaceV2/LabelledComponent.cs
index 19e9c329d6..2e659825b7 100644
--- a/osu.Game/Screens/Edit/Setup/Components/LabelledComponents/LabelledComponent.cs
+++ b/osu.Game/Graphics/UserInterfaceV2/LabelledComponent.cs
@@ -5,13 +5,13 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osuTK;
-namespace osu.Game.Screens.Edit.Setup.Components.LabelledComponents
+namespace osu.Game.Graphics.UserInterfaceV2
{
- public abstract class LabelledComponent : CompositeDrawable
+ public abstract class LabelledComponent : CompositeDrawable
+ where T : Drawable
{
protected const float CONTENT_PADDING_VERTICAL = 10;
protected const float CONTENT_PADDING_HORIZONTAL = 15;
@@ -20,15 +20,15 @@ namespace osu.Game.Screens.Edit.Setup.Components.LabelledComponents
///
/// The component that is being displayed.
///
- protected readonly Drawable Component;
+ protected readonly T Component;
private readonly OsuTextFlowContainer labelText;
private readonly OsuTextFlowContainer descriptionText;
///
- /// Creates a new .
+ /// Creates a new .
///
- /// Whether the component should be padded or should be expanded to the bounds of this .
+ /// Whether the component should be padded or should be expanded to the bounds of this .
protected LabelledComponent(bool padded)
{
RelativeSizeAxes = Axes.X;
@@ -127,6 +127,6 @@ namespace osu.Game.Screens.Edit.Setup.Components.LabelledComponents
/// Creates the component that should be displayed.
///
/// The component.
- protected abstract Drawable CreateComponent();
+ protected abstract T CreateComponent();
}
}
diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs
new file mode 100644
index 0000000000..50d2a14482
--- /dev/null
+++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs
@@ -0,0 +1,49 @@
+// 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.Graphics.UserInterface;
+using osu.Game.Graphics.UserInterface;
+
+namespace osu.Game.Graphics.UserInterfaceV2
+{
+ public class LabelledTextBox : LabelledComponent
+ {
+ public event TextBox.OnCommitHandler OnCommit;
+
+ public LabelledTextBox()
+ : base(false)
+ {
+ }
+
+ public bool ReadOnly
+ {
+ set => Component.ReadOnly = value;
+ }
+
+ public string PlaceholderText
+ {
+ set => Component.PlaceholderText = value;
+ }
+
+ public string Text
+ {
+ set => Component.Text = value;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours)
+ {
+ Component.BorderColour = colours.Blue;
+ }
+
+ protected override OsuTextBox CreateComponent() => new OsuTextBox
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ RelativeSizeAxes = Axes.X,
+ CornerRadius = CORNER_RADIUS,
+ }.With(t => t.OnCommit += (sender, newText) => OnCommit?.Invoke(sender, newText));
+ }
+}
diff --git a/osu.Game/Overlays/Changelog/ChangelogBuild.cs b/osu.Game/Overlays/Changelog/ChangelogBuild.cs
index 11dc2049fd..bce1be5941 100644
--- a/osu.Game/Overlays/Changelog/ChangelogBuild.cs
+++ b/osu.Game/Overlays/Changelog/ChangelogBuild.cs
@@ -15,6 +15,7 @@ using osu.Game.Users;
using osuTK.Graphics;
using osu.Framework.Allocation;
using System.Net;
+using osuTK;
namespace osu.Game.Overlays.Changelog
{
@@ -67,22 +68,34 @@ namespace osu.Game.Overlays.Changelog
foreach (APIChangelogEntry entry in categoryEntries)
{
- LinkFlowContainer title = new LinkFlowContainer
- {
- Direction = FillDirection.Full,
- RelativeSizeAxes = Axes.X,
- AutoSizeAxes = Axes.Y,
- Margin = new MarginPadding { Vertical = 5 },
- };
-
var entryColour = entry.Major ? colours.YellowLight : Color4.White;
- title.AddIcon(entry.Type == ChangelogEntryType.Fix ? FontAwesome.Solid.Check : FontAwesome.Solid.Plus, t =>
+ LinkFlowContainer title;
+
+ Container titleContainer = new Container
{
- t.Font = fontSmall;
- t.Colour = entryColour;
- t.Padding = new MarginPadding { Left = -17, Right = 5 };
- });
+ AutoSizeAxes = Axes.Y,
+ RelativeSizeAxes = Axes.X,
+ Margin = new MarginPadding { Vertical = 5 },
+ Children = new Drawable[]
+ {
+ new SpriteIcon
+ {
+ Anchor = Anchor.CentreLeft,
+ Origin = Anchor.CentreRight,
+ Size = new Vector2(fontSmall.Size),
+ Icon = entry.Type == ChangelogEntryType.Fix ? FontAwesome.Solid.Check : FontAwesome.Solid.Plus,
+ Colour = entryColour,
+ Margin = new MarginPadding { Right = 5 },
+ },
+ title = new LinkFlowContainer
+ {
+ Direction = FillDirection.Full,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ }
+ }
+ };
title.AddText(entry.Title, t =>
{
@@ -139,7 +152,7 @@ namespace osu.Game.Overlays.Changelog
t.Colour = entryColour;
});
- ChangelogEntries.Add(title);
+ ChangelogEntries.Add(titleContainer);
if (!string.IsNullOrEmpty(entry.MessageHtml))
{
diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs
index 66fec1ecf9..b02b1a5489 100644
--- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs
+++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs
@@ -67,7 +67,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
api?.Register(this);
}
- public void APIStateChanged(IAPIProvider api, APIState state)
+ public void APIStateChanged(IAPIProvider api, APIState state) => Schedule(() =>
{
form = null;
@@ -184,7 +184,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
}
if (form != null) GetContainingInputManager()?.ChangeFocus(form);
- }
+ });
public override bool AcceptsFocus => true;
diff --git a/osu.Game/Rulesets/Objects/BarLine.cs b/osu.Game/Rulesets/Objects/BarLine.cs
deleted file mode 100644
index a5c716e127..0000000000
--- a/osu.Game/Rulesets/Objects/BarLine.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-// 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
-{
- ///
- /// A hit object representing the end of a bar.
- ///
- public class BarLine : HitObject
- {
- ///
- /// Whether this barline is a prominent beat (based on time signature of beatmap).
- ///
- public bool Major;
- }
-}
diff --git a/osu.Game/Rulesets/Objects/BarLineGenerator.cs b/osu.Game/Rulesets/Objects/BarLineGenerator.cs
index ce571d7b17..4f9395435e 100644
--- a/osu.Game/Rulesets/Objects/BarLineGenerator.cs
+++ b/osu.Game/Rulesets/Objects/BarLineGenerator.cs
@@ -10,12 +10,13 @@ using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Objects
{
- public class BarLineGenerator
+ public class BarLineGenerator
+ where TBarLine : class, IBarLine, new()
{
///
/// The generated bar lines.
///
- public readonly List BarLines = new List();
+ public readonly List BarLines = new List();
///
/// Constructs and generates bar lines for provided beatmap.
@@ -46,7 +47,7 @@ namespace osu.Game.Rulesets.Objects
for (double t = currentTimingPoint.Time; Precision.DefinitelyBigger(endTime, t); t += barLength, currentBeat++)
{
- BarLines.Add(new BarLine
+ BarLines.Add(new TBarLine
{
StartTime = t,
Major = currentBeat % (int)currentTimingPoint.TimeSignature == 0
diff --git a/osu.Game/Rulesets/Objects/IBarLine.cs b/osu.Game/Rulesets/Objects/IBarLine.cs
new file mode 100644
index 0000000000..14df80e3b9
--- /dev/null
+++ b/osu.Game/Rulesets/Objects/IBarLine.cs
@@ -0,0 +1,22 @@
+// 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
+{
+ ///
+ /// Interface for bar line hitobjects.
+ /// Used to decouple bar line generation from ruleset-specific rendering/drawing hierarchies.
+ ///
+ public interface IBarLine
+ {
+ ///
+ /// The time position of the bar.
+ ///
+ double StartTime { set; }
+
+ ///
+ /// Whether this bar line is a prominent beat (based on time signature of beatmap).
+ ///
+ bool Major { set; }
+ }
+}
diff --git a/osu.Game/Screens/Edit/Setup/Components/LabelledComponents/LabelledTextBox.cs b/osu.Game/Screens/Edit/Setup/Components/LabelledComponents/LabelledTextBox.cs
deleted file mode 100644
index 1c53fc7088..0000000000
--- a/osu.Game/Screens/Edit/Setup/Components/LabelledComponents/LabelledTextBox.cs
+++ /dev/null
@@ -1,129 +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.Framework.Allocation;
-using osu.Framework.Graphics;
-using osu.Framework.Graphics.Containers;
-using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.UserInterface;
-using osu.Game.Graphics;
-using osu.Game.Graphics.Sprites;
-using osu.Game.Graphics.UserInterface;
-using osuTK.Graphics;
-
-namespace osu.Game.Screens.Edit.Setup.Components.LabelledComponents
-{
- public class LabelledTextBox : CompositeDrawable
- {
- private const float label_container_width = 150;
- private const float corner_radius = 15;
- private const float default_height = 40;
- private const float default_label_left_padding = 15;
- private const float default_label_top_padding = 12;
- private const float default_label_text_size = 16;
-
- public event TextBox.OnCommitHandler OnCommit;
-
- public bool ReadOnly
- {
- get => textBox.ReadOnly;
- set => textBox.ReadOnly = value;
- }
-
- public string LabelText
- {
- get => label.Text;
- set => label.Text = value;
- }
-
- public float LabelTextSize
- {
- get => label.Font.Size;
- set => label.Font = label.Font.With(size: value);
- }
-
- public string PlaceholderText
- {
- get => textBox.PlaceholderText;
- set => textBox.PlaceholderText = value;
- }
-
- public string Text
- {
- get => textBox.Text;
- set => textBox.Text = value;
- }
-
- public Color4 LabelTextColour
- {
- get => label.Colour;
- set => label.Colour = value;
- }
-
- private readonly OsuTextBox textBox;
- private readonly OsuSpriteText label;
-
- public LabelledTextBox()
- {
- RelativeSizeAxes = Axes.X;
- Height = default_height;
- CornerRadius = corner_radius;
- Masking = true;
-
- InternalChild = new Container
- {
- RelativeSizeAxes = Axes.Both,
- CornerRadius = corner_radius,
- Masking = true,
- Children = new Drawable[]
- {
- new Box
- {
- RelativeSizeAxes = Axes.Both,
- Colour = OsuColour.FromHex("1c2125"),
- },
- new GridContainer
- {
- RelativeSizeAxes = Axes.X,
- Height = default_height,
- Content = new[]
- {
- new Drawable[]
- {
- label = new OsuSpriteText
- {
- Anchor = Anchor.TopLeft,
- Origin = Anchor.TopLeft,
- Padding = new MarginPadding { Left = default_label_left_padding, Top = default_label_top_padding },
- Colour = Color4.White,
- Font = OsuFont.GetFont(size: default_label_text_size, weight: FontWeight.Bold),
- },
- textBox = new OsuTextBox
- {
- Anchor = Anchor.TopLeft,
- Origin = Anchor.TopLeft,
- RelativeSizeAxes = Axes.Both,
- Height = 1,
- CornerRadius = corner_radius,
- },
- },
- },
- ColumnDimensions = new[]
- {
- new Dimension(GridSizeMode.Absolute, label_container_width),
- new Dimension()
- }
- }
- }
- };
-
- textBox.OnCommit += OnCommit;
- }
-
- [BackgroundDependencyLoader]
- private void load(OsuColour colours)
- {
- textBox.BorderColour = colours.Blue;
- }
- }
-}
diff --git a/osu.Game/Screens/Menu/Button.cs b/osu.Game/Screens/Menu/Button.cs
index 1bf25a2504..ffeadb96c7 100644
--- a/osu.Game/Screens/Menu/Button.cs
+++ b/osu.Game/Screens/Menu/Button.cs
@@ -31,6 +31,8 @@ namespace osu.Game.Screens.Menu
{
public event Action StateChanged;
+ public readonly Key TriggerKey;
+
private readonly Container iconText;
private readonly Container box;
private readonly Box boxHoverLayer;
@@ -43,7 +45,6 @@ namespace osu.Game.Screens.Menu
public ButtonSystemState VisibleState = ButtonSystemState.TopLevel;
private readonly Action clickAction;
- private readonly Key triggerKey;
private SampleChannel sampleClick;
private SampleChannel sampleHover;
@@ -53,7 +54,7 @@ namespace osu.Game.Screens.Menu
{
this.sampleName = sampleName;
this.clickAction = clickAction;
- this.triggerKey = triggerKey;
+ TriggerKey = triggerKey;
AutoSizeAxes = Axes.Both;
Alpha = 0;
@@ -210,7 +211,7 @@ namespace osu.Game.Screens.Menu
if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed)
return false;
- if (triggerKey == e.Key && triggerKey != Key.Unknown)
+ if (TriggerKey == e.Key && TriggerKey != Key.Unknown)
{
trigger();
return true;
diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs
index 1a3e1213b4..ed8e4c70f9 100644
--- a/osu.Game/Screens/Menu/ButtonSystem.cs
+++ b/osu.Game/Screens/Menu/ButtonSystem.cs
@@ -14,6 +14,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
+using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Threading;
@@ -180,6 +181,20 @@ namespace osu.Game.Screens.Menu
State = ButtonSystemState.Initial;
}
+ protected override bool OnKeyDown(KeyDownEvent e)
+ {
+ if (State == ButtonSystemState.Initial)
+ {
+ if (buttonsTopLevel.Any(b => e.Key == b.TriggerKey))
+ {
+ logo?.Click();
+ return true;
+ }
+ }
+
+ return base.OnKeyDown(e);
+ }
+
public bool OnPressed(GlobalAction action)
{
switch (action)
diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs
index 23c581c6f9..c3436ffd45 100644
--- a/osu.Game/Screens/Select/BeatmapCarousel.cs
+++ b/osu.Game/Screens/Select/BeatmapCarousel.cs
@@ -82,6 +82,9 @@ namespace osu.Game.Screens.Select
var _ = newRoot.Drawables;
root = newRoot;
+ if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet))
+ selectedBeatmapSet = null;
+
scrollableContent.Clear(false);
itemsCache.Invalidate();
scrollPositionCache.Invalidate();
diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
index 9cc84c8bdd..6c3c9d20f3 100644
--- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
+++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs
@@ -39,6 +39,10 @@ namespace osu.Game.Screens.Select.Carousel
match &= criteria.BeatDivisor.IsInRange(Beatmap.BeatDivisor);
match &= criteria.OnlineStatus.IsInRange(Beatmap.Status);
+ match &= criteria.Creator.Matches(Beatmap.Metadata.AuthorString);
+ match &= criteria.Artist.Matches(Beatmap.Metadata.Artist) ||
+ criteria.Artist.Matches(Beatmap.Metadata.ArtistUnicode);
+
if (match)
foreach (var criteriaTerm in criteria.SearchTerms)
match &=
diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs
index e3c23f7e22..91f1ca0307 100644
--- a/osu.Game/Screens/Select/FilterControl.cs
+++ b/osu.Game/Screens/Select/FilterControl.cs
@@ -16,8 +16,6 @@ using Container = osu.Framework.Graphics.Containers.Container;
using osu.Framework.Graphics.Shapes;
using osu.Game.Configuration;
using osu.Game.Rulesets;
-using System.Text.RegularExpressions;
-using osu.Game.Beatmaps;
namespace osu.Game.Screens.Select
{
@@ -47,10 +45,7 @@ namespace osu.Game.Screens.Select
Ruleset = ruleset.Value
};
- applyQueries(criteria, ref query);
-
- criteria.SearchText = query;
-
+ FilterQueryParser.ApplyQueries(criteria, query);
return criteria;
}
@@ -181,129 +176,5 @@ namespace osu.Game.Screens.Select
}
private void updateCriteria() => FilterChanged?.Invoke(CreateCriteria());
-
- private static readonly Regex query_syntax_regex = new Regex(
- @"\b(?stars|ar|dr|cs|divisor|length|objects|bpm|status)(?[=:><]+)(?\S*)",
- RegexOptions.Compiled | RegexOptions.IgnoreCase);
-
- private void applyQueries(FilterCriteria criteria, ref string query)
- {
- foreach (Match match in query_syntax_regex.Matches(query))
- {
- var key = match.Groups["key"].Value.ToLower();
- var op = match.Groups["op"].Value;
- var value = match.Groups["value"].Value;
-
- switch (key)
- {
- case "stars" when float.TryParse(value, out var stars):
- updateCriteriaRange(ref criteria.StarDifficulty, op, stars);
- break;
-
- case "ar" when float.TryParse(value, out var ar):
- updateCriteriaRange(ref criteria.ApproachRate, op, ar);
- break;
-
- case "dr" when float.TryParse(value, out var dr):
- updateCriteriaRange(ref criteria.DrainRate, op, dr);
- break;
-
- case "cs" when float.TryParse(value, out var cs):
- updateCriteriaRange(ref criteria.CircleSize, op, cs);
- break;
-
- case "bpm" when double.TryParse(value, out var bpm):
- updateCriteriaRange(ref criteria.BPM, op, bpm);
- break;
-
- case "length" when double.TryParse(value.TrimEnd('m', 's', 'h'), out var length):
- var scale =
- value.EndsWith("ms") ? 1 :
- value.EndsWith("s") ? 1000 :
- value.EndsWith("m") ? 60000 :
- value.EndsWith("h") ? 3600000 : 1000;
-
- updateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0);
- break;
-
- case "divisor" when int.TryParse(value, out var divisor):
- updateCriteriaRange(ref criteria.BeatDivisor, op, divisor);
- break;
-
- case "status" when Enum.TryParse(value, true, out var statusValue):
- updateCriteriaRange(ref criteria.OnlineStatus, op, statusValue);
- break;
- }
-
- query = query.Replace(match.ToString(), "");
- }
- }
-
- private void updateCriteriaRange(ref FilterCriteria.OptionalRange range, string op, float value, float tolerance = 0.05f)
- {
- updateCriteriaRange(ref range, op, value);
-
- switch (op)
- {
- case "=":
- case ":":
- range.Min = value - tolerance;
- range.Max = value + tolerance;
- break;
- }
- }
-
- private void updateCriteriaRange(ref FilterCriteria.OptionalRange range, string op, double value, double tolerance = 0.05)
- {
- updateCriteriaRange(ref range, op, value);
-
- switch (op)
- {
- case "=":
- case ":":
- range.Min = value - tolerance;
- range.Max = value + tolerance;
- break;
- }
- }
-
- private void updateCriteriaRange(ref FilterCriteria.OptionalRange range, string op, T value)
- where T : struct, IComparable
- {
- switch (op)
- {
- default:
- return;
-
- case "=":
- case ":":
- range.IsInclusive = true;
- range.Min = value;
- range.Max = value;
- break;
-
- case ">":
- range.IsInclusive = false;
- range.Min = value;
- break;
-
- case ">=":
- case ">:":
- range.IsInclusive = true;
- range.Min = value;
- break;
-
- case "<":
- range.IsInclusive = false;
- range.Max = value;
- break;
-
- case "<=":
- case "<:":
- range.IsInclusive = true;
- range.Max = value;
- break;
- }
- }
}
}
diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs
index a3fa1b10ca..c2cbac905e 100644
--- a/osu.Game/Screens/Select/FilterCriteria.cs
+++ b/osu.Game/Screens/Select/FilterCriteria.cs
@@ -23,6 +23,8 @@ namespace osu.Game.Screens.Select
public OptionalRange BPM;
public OptionalRange BeatDivisor;
public OptionalRange OnlineStatus;
+ public OptionalTextFilter Creator;
+ public OptionalTextFilter Artist;
public string[] SearchTerms = Array.Empty();
@@ -53,7 +55,7 @@ namespace osu.Game.Screens.Select
if (comparison < 0)
return false;
- if (comparison == 0 && !IsInclusive)
+ if (comparison == 0 && !IsLowerInclusive)
return false;
}
@@ -64,7 +66,7 @@ namespace osu.Game.Screens.Select
if (comparison > 0)
return false;
- if (comparison == 0 && !IsInclusive)
+ if (comparison == 0 && !IsUpperInclusive)
return false;
}
@@ -73,12 +75,33 @@ namespace osu.Game.Screens.Select
public T? Min;
public T? Max;
- public bool IsInclusive;
+ public bool IsLowerInclusive;
+ public bool IsUpperInclusive;
public bool Equals(OptionalRange other)
=> Min.Equals(other.Min)
&& Max.Equals(other.Max)
- && IsInclusive.Equals(other.IsInclusive);
+ && IsLowerInclusive.Equals(other.IsLowerInclusive)
+ && IsUpperInclusive.Equals(other.IsUpperInclusive);
+ }
+
+ public struct OptionalTextFilter : IEquatable
+ {
+ public bool Matches(string value)
+ {
+ if (string.IsNullOrEmpty(SearchTerm))
+ return true;
+
+ // search term is guaranteed to be non-empty, so if the string we're comparing is empty, it's not matching
+ if (string.IsNullOrEmpty(value))
+ return false;
+
+ return value.IndexOf(SearchTerm, StringComparison.InvariantCultureIgnoreCase) >= 0;
+ }
+
+ public string SearchTerm;
+
+ public bool Equals(OptionalTextFilter other) => SearchTerm?.Equals(other.SearchTerm) ?? true;
}
}
}
diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs
new file mode 100644
index 0000000000..ffe1258168
--- /dev/null
+++ b/osu.Game/Screens/Select/FilterQueryParser.cs
@@ -0,0 +1,211 @@
+// 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.Globalization;
+using System.Text.RegularExpressions;
+using osu.Game.Beatmaps;
+
+namespace osu.Game.Screens.Select
+{
+ internal static class FilterQueryParser
+ {
+ private static readonly Regex query_syntax_regex = new Regex(
+ @"\b(?stars|ar|dr|cs|divisor|length|objects|bpm|status|creator|artist)(?[=:><]+)(?("".*"")|(\S*))",
+ RegexOptions.Compiled | RegexOptions.IgnoreCase);
+
+ internal static void ApplyQueries(FilterCriteria criteria, string query)
+ {
+ foreach (Match match in query_syntax_regex.Matches(query))
+ {
+ var key = match.Groups["key"].Value.ToLower();
+ var op = match.Groups["op"].Value;
+ var value = match.Groups["value"].Value;
+
+ parseKeywordCriteria(criteria, key, value, op);
+
+ query = query.Replace(match.ToString(), "");
+ }
+
+ criteria.SearchText = query;
+ }
+
+ private static void parseKeywordCriteria(FilterCriteria criteria, string key, string value, string op)
+ {
+ switch (key)
+ {
+ case "stars" when parseFloatWithPoint(value, out var stars):
+ updateCriteriaRange(ref criteria.StarDifficulty, op, stars, 0.01f / 2);
+ break;
+
+ case "ar" when parseFloatWithPoint(value, out var ar):
+ updateCriteriaRange(ref criteria.ApproachRate, op, ar, 0.1f / 2);
+ break;
+
+ case "dr" when parseFloatWithPoint(value, out var dr):
+ updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2);
+ break;
+
+ case "cs" when parseFloatWithPoint(value, out var cs):
+ updateCriteriaRange(ref criteria.CircleSize, op, cs, 0.1f / 2);
+ break;
+
+ case "bpm" when parseDoubleWithPoint(value, out var bpm):
+ updateCriteriaRange(ref criteria.BPM, op, bpm, 0.01d / 2);
+ break;
+
+ case "length" when parseDoubleWithPoint(value.TrimEnd('m', 's', 'h'), out var length):
+ var scale = getLengthScale(value);
+ updateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0);
+ break;
+
+ case "divisor" when parseInt(value, out var divisor):
+ updateCriteriaRange(ref criteria.BeatDivisor, op, divisor);
+ break;
+
+ case "status" when Enum.TryParse(value, true, out var statusValue):
+ updateCriteriaRange(ref criteria.OnlineStatus, op, statusValue);
+ break;
+
+ case "creator":
+ updateCriteriaText(ref criteria.Creator, op, value);
+ break;
+
+ case "artist":
+ updateCriteriaText(ref criteria.Artist, op, value);
+ break;
+ }
+ }
+
+ private static int getLengthScale(string value) =>
+ value.EndsWith("ms") ? 1 :
+ value.EndsWith("s") ? 1000 :
+ value.EndsWith("m") ? 60000 :
+ value.EndsWith("h") ? 3600000 : 1000;
+
+ private static bool parseFloatWithPoint(string value, out float result) =>
+ float.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result);
+
+ private static bool parseDoubleWithPoint(string value, out double result) =>
+ double.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result);
+
+ private static bool parseInt(string value, out int result) =>
+ int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result);
+
+ private static void updateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, string op, string value)
+ {
+ switch (op)
+ {
+ case "=":
+ case ":":
+ textFilter.SearchTerm = value.Trim('"');
+ break;
+ }
+ }
+
+ private static void updateCriteriaRange(ref FilterCriteria.OptionalRange range, string op, float value, float tolerance = 0.05f)
+ {
+ switch (op)
+ {
+ default:
+ return;
+
+ case "=":
+ case ":":
+ range.Min = value - tolerance;
+ range.Max = value + tolerance;
+ break;
+
+ case ">":
+ range.Min = value + tolerance;
+ break;
+
+ case ">=":
+ case ">:":
+ range.Min = value - tolerance;
+ break;
+
+ case "<":
+ range.Max = value - tolerance;
+ break;
+
+ case "<=":
+ case "<:":
+ range.Max = value + tolerance;
+ break;
+ }
+ }
+
+ private static void updateCriteriaRange(ref FilterCriteria.OptionalRange range, string op, double value, double tolerance = 0.05)
+ {
+ switch (op)
+ {
+ default:
+ return;
+
+ case "=":
+ case ":":
+ range.Min = value - tolerance;
+ range.Max = value + tolerance;
+ break;
+
+ case ">":
+ range.Min = value + tolerance;
+ break;
+
+ case ">=":
+ case ">:":
+ range.Min = value - tolerance;
+ break;
+
+ case "<":
+ range.Max = value - tolerance;
+ break;
+
+ case "<=":
+ case "<:":
+ range.Max = value + tolerance;
+ break;
+ }
+ }
+
+ private static void updateCriteriaRange(ref FilterCriteria.OptionalRange range, string op, T value)
+ where T : struct, IComparable
+ {
+ switch (op)
+ {
+ default:
+ return;
+
+ case "=":
+ case ":":
+ range.IsLowerInclusive = range.IsUpperInclusive = true;
+ range.Min = value;
+ range.Max = value;
+ break;
+
+ case ">":
+ range.IsLowerInclusive = false;
+ range.Min = value;
+ break;
+
+ case ">=":
+ case ">:":
+ range.IsLowerInclusive = true;
+ range.Min = value;
+ break;
+
+ case "<":
+ range.IsUpperInclusive = false;
+ range.Max = value;
+ break;
+
+ case "<=":
+ case "<:":
+ range.IsUpperInclusive = true;
+ range.Max = value;
+ break;
+ }
+ }
+ }
+}
diff --git a/osu.Desktop/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs
similarity index 87%
rename from osu.Desktop/Updater/SimpleUpdateManager.cs
rename to osu.Game/Updater/SimpleUpdateManager.cs
index 5184791de1..eec27d3325 100644
--- a/osu.Desktop/Updater/SimpleUpdateManager.cs
+++ b/osu.Game/Updater/SimpleUpdateManager.cs
@@ -6,31 +6,25 @@ using System.Threading.Tasks;
using Newtonsoft.Json;
using osu.Framework;
using osu.Framework.Allocation;
-using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.IO.Network;
using osu.Framework.Platform;
-using osu.Game;
-using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
-namespace osu.Desktop.Updater
+namespace osu.Game.Updater
{
///
/// An update manager that shows notifications if a newer release is detected.
/// Installation is left up to the user.
///
- internal class SimpleUpdateManager : CompositeDrawable
+ public class SimpleUpdateManager : UpdateManager
{
- private NotificationOverlay notificationOverlay;
private string version;
private GameHost host;
[BackgroundDependencyLoader]
- private void load(NotificationOverlay notification, OsuGameBase game, GameHost host)
+ private void load(OsuGameBase game, GameHost host)
{
- notificationOverlay = notification;
-
this.host = host;
version = game.Version;
@@ -50,7 +44,7 @@ namespace osu.Desktop.Updater
if (latest.TagName != version)
{
- notificationOverlay.Post(new SimpleNotification
+ Notifications.Post(new SimpleNotification
{
Text = $"A newer release of osu! has been found ({version} → {latest.TagName}).\n\n"
+ "Click here to download the new version, which can be installed over the top of your existing installation",
@@ -82,6 +76,10 @@ namespace osu.Desktop.Updater
case RuntimeInfo.Platform.MacOsx:
bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".app.zip"));
break;
+
+ case RuntimeInfo.Platform.Android:
+ bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".apk"));
+ break;
}
return bestAsset?.BrowserDownloadUrl ?? release.HtmlUrl;
diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs
new file mode 100644
index 0000000000..e256cdbe45
--- /dev/null
+++ b/osu.Game/Updater/UpdateManager.cs
@@ -0,0 +1,67 @@
+// 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.Containers;
+using osu.Framework.Graphics.Sprites;
+using osu.Game.Configuration;
+using osu.Game.Graphics;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Notifications;
+
+namespace osu.Game.Updater
+{
+ public abstract class UpdateManager : CompositeDrawable
+ {
+ [Resolved]
+ private OsuConfigManager config { get; set; }
+
+ [Resolved]
+ private OsuGameBase game { get; set; }
+
+ [Resolved]
+ protected NotificationOverlay Notifications { get; private set; }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ var version = game.Version;
+ var lastVersion = config.Get(OsuSetting.Version);
+
+ if (game.IsDeployedBuild && version != lastVersion)
+ {
+ config.Set(OsuSetting.Version, version);
+
+ // only show a notification if we've previously saved a version to the config file (ie. not the first run).
+ if (!string.IsNullOrEmpty(lastVersion))
+ Notifications.Post(new UpdateCompleteNotification(version));
+ }
+ }
+
+ private class UpdateCompleteNotification : SimpleNotification
+ {
+ private readonly string version;
+
+ public UpdateCompleteNotification(string version)
+ {
+ this.version = version;
+ Text = $"You are now running osu!lazer {version}.\nClick to see what's new!";
+ }
+
+ [BackgroundDependencyLoader]
+ private void load(OsuColour colours, ChangelogOverlay changelog, NotificationOverlay notificationOverlay)
+ {
+ Icon = FontAwesome.Solid.CheckSquare;
+ IconBackgound.Colour = colours.BlueDark;
+
+ Activated = delegate
+ {
+ notificationOverlay.Hide();
+ changelog.ShowBuild(OsuGameBase.CLIENT_STREAM_NAME, version);
+ return true;
+ };
+ }
+ }
+ }
+}
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index a27a94b8f9..83632f3d41 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -26,10 +26,10 @@
-
+
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index a6516e6d1b..30f1da362d 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -118,12 +118,12 @@
-
-
+
+
-
+