1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-28 08:43:20 +08:00

Merge branch 'master' into introduce-session-statics

This commit is contained in:
Salman Ahmed 2019-09-25 16:14:39 +03:00 committed by GitHub
commit 3aa9a172d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
51 changed files with 1414 additions and 549 deletions

View File

@ -1,11 +1,11 @@
GEM
remote: https://rubygems.org/
specs:
CFPropertyList (3.0.0)
addressable (2.6.0)
public_suffix (>= 2.0.2, < 4.0)
CFPropertyList (3.0.1)
addressable (2.7.0)
public_suffix (>= 2.0.2, < 5.0)
atomos (0.1.3)
babosa (1.0.2)
babosa (1.0.3)
claide (1.0.3)
colored (1.2)
colored2 (3.1.2)
@ -26,8 +26,8 @@ GEM
http-cookie (~> 1.0.0)
faraday_middleware (0.13.1)
faraday (>= 0.7.4, < 1.0)
fastimage (2.1.5)
fastlane (2.129.0)
fastimage (2.1.7)
fastlane (2.131.0)
CFPropertyList (>= 2.3, < 4.0.0)
addressable (>= 2.3, < 3.0.0)
babosa (>= 1.0.2, < 2.0.0)
@ -77,9 +77,9 @@ GEM
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
signet (~> 0.9)
google-cloud-core (1.3.0)
google-cloud-core (1.3.1)
google-cloud-env (~> 1.0)
google-cloud-env (1.2.0)
google-cloud-env (1.2.1)
faraday (~> 0.11)
google-cloud-storage (1.16.0)
digest-crc (~> 0.4)
@ -100,9 +100,9 @@ GEM
json (2.2.0)
jwt (2.1.0)
memoist (0.16.0)
mime-types (3.2.2)
mime-types (3.3)
mime-types-data (~> 3.2015)
mime-types-data (3.2019.0331)
mime-types-data (3.2019.0904)
mini_magick (4.9.5)
mini_portile2 (2.4.0)
multi_json (1.13.1)
@ -121,14 +121,14 @@ GEM
uber (< 0.2.0)
retriable (3.1.2)
rouge (2.0.7)
rubyzip (1.2.3)
rubyzip (1.2.4)
security (0.1.3)
signet (0.11.0)
addressable (~> 2.3)
faraday (~> 0.9)
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
simctl (1.6.5)
simctl (1.6.6)
CFPropertyList
naturally
slack-notifier (2.3.2)

View File

@ -31,12 +31,10 @@ If you are not interested in developing the game, you can still consume our [bin
**Latest build:**
| [Windows (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) |
| ------------- | ------------- |
| [Windows (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [iOS(iOS 10+)](https://testflight.apple.com/join/2tLcjWlF) | [Android (5+)](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
| ------------- | ------------- | ------------- | ------------- |
- **Linux** users are recommended to self-compile until we have official deployment in place.
- **iOS** users can join the [TestFlight beta program](https://testflight.apple.com/join/2tLcjWlF) (note that due to high demand this is regularly full).
- **Android** users can self-compile, and expect a public beta soon.
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.

View File

@ -1,5 +1,83 @@
update_fastlane
platform :android do
desc 'Deploy to play store'
lane :beta do |options|
update_version(
version: options[:version],
build: options[:build],
)
build(options)
supply(
apk: './osu.Android/bin/Release/sh.ppy.osulazer-Signed.apk',
package_name: 'sh.ppy.osulazer',
track: 'alpha', # upload to alpha, we can promote it later
json_key: options[:json_key],
)
end
desc 'Deploy to github release'
lane :build_github do |options|
update_version(
version: options[:version],
build: options[:build],
)
build(options)
client = HTTPClient.new
changelog = client.get_content 'https://gist.githubusercontent.com/peppy/aaa2ec1a323554b619671cac6dbbb776/raw'
changelog.gsub!('$BUILD_ID', options[:build])
set_github_release(
repository_name: "ppy/osu",
api_token: ENV["GITHUB_TOKEN"],
name: options[:build],
tag_name: options[:build],
is_draft: true,
description: changelog,
commitish: "master",
upload_assets: ["osu.Android/bin/Release/sh.ppy.osulazer.apk"]
)
end
desc 'Compile the project'
lane :build do |options|
nuget_restore(
project_path: 'osu.Android.sln'
)
souyuz(
build_configuration: 'Release',
solution_path: 'osu.Android.sln',
platform: "android",
output_path: "osu.Android/bin/Release/",
keystore_path: options[:keystore_path],
keystore_alias: options[:keystore_alias],
keystore_password: ENV["KEYSTORE_PASSWORD"]
)
end
lane :update_version do |options|
split = options[:build].split('.')
split[1] = split[1].to_s.rjust(4, '0')
android_build = split.join('')
app_version(
solution_path: 'osu.Android.sln',
version: options[:version],
build: android_build,
)
end
end
platform :ios do
desc 'Deploy to testflight'
lane :beta do |options|

View File

@ -15,6 +15,30 @@ Install _fastlane_ using
or alternatively using `brew cask install fastlane`
# Available Actions
## Android
### android beta
```
fastlane android beta
```
Deploy to play store
### android build_github
```
fastlane android build_github
```
Deploy to github release
### android build
```
fastlane android build
```
Compile the project
### android update_version
```
fastlane android update_version
```
----
## iOS
### ios beta
```

View File

@ -62,6 +62,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.913.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2019.921.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2019.924.0" />
</ItemGroup>
</Project>

View File

@ -4,11 +4,37 @@
using System;
using Android.App;
using osu.Game;
using osu.Game.Updater;
namespace osu.Android
{
public class OsuGameAndroid : OsuGame
{
public override Version AssemblyVersion => new Version(Application.Context.ApplicationContext.PackageManager.GetPackageInfo(Application.Context.ApplicationContext.PackageName, 0).VersionName);
public override Version AssemblyVersion
{
get
{
var packageInfo = Application.Context.ApplicationContext.PackageManager.GetPackageInfo(Application.Context.ApplicationContext.PackageName, 0);
try
{
string versionName = packageInfo.VersionCode.ToString();
// undo play store version garbling
return new Version(int.Parse(versionName.Substring(0, 4)), int.Parse(versionName.Substring(4, 4)), int.Parse(versionName.Substring(8, 1)));
}
catch
{
}
return new Version(packageInfo.VersionName);
}
}
protected override void LoadComplete()
{
base.LoadComplete();
Add(new SimpleUpdateManager());
}
}
}
}

View File

@ -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
{

View File

@ -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<string>(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);

View File

@ -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;

View File

@ -23,10 +23,10 @@
<ProjectReference Include="..\osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.5.0" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.6.0" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="System.IO.Packaging" Version="4.5.0" />
<PackageReference Include="System.IO.Packaging" Version="4.6.0" />
<PackageReference Include="ppy.squirrel.windows" Version="1.9.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.2.6" />

View File

@ -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)

View File

@ -0,0 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Objects
{
public class BarLine : ManiaHitObject, IBarLine
{
public bool Major { get; set; }
}
}

View File

@ -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 <see cref="BarLine"/>. Although this derives DrawableManiaHitObject,
/// this does not handle input/sound like a normal hit object.
/// </summary>
public class DrawableBarLine : DrawableHitObject<BarLine>
public class DrawableBarLine : DrawableManiaHitObject<BarLine>
{
/// <summary>
/// Height of major bar line triangles.

View File

@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mania.UI
public DrawableManiaRuleset(Ruleset ruleset, IWorkingBeatmap beatmap, IReadOnlyList<Mod> mods)
: base(ruleset, beatmap, mods)
{
BarLines = new BarLineGenerator(Beatmap).BarLines;
BarLines = new BarLineGenerator<BarLine>(Beatmap).BarLines;
}
[BackgroundDependencyLoader]

View File

@ -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;

View File

@ -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;

View File

@ -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<TexturedVertex2D> 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;

View File

@ -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));

View File

@ -0,0 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Taiko.Objects
{
public class BarLine : TaikoHitObject, IBarLine
{
public bool Major { get; set; }
}
}

View File

@ -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
{

View File

@ -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<BarLine>(Beatmap).BarLines.ForEach(bar => Playfield.Add(bar.Major ? new DrawableBarLineMajor(bar) : new DrawableBarLine(bar)));
}
public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(this);

View File

@ -0,0 +1,201 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.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<float>
{
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<double>
{
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);
}
}
}

View File

@ -0,0 +1,184 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using 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);
}
}
}

View File

@ -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;

View File

@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.SongSelect
private readonly Stack<BeatmapSetInfo> selectedSets = new Stack<BeatmapSetInfo>();
private readonly HashSet<int> eagerSelectedIDs = new HashSet<int>();
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<BeatmapSetInfo> beatmapSets = new List<BeatmapSetInfo>();
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<BeatmapSetInfo> beatmapSets)
private void loadBeatmaps(List<BeatmapSetInfo> beatmapSets = null)
{
if (beatmapSets == null)
{
beatmapSets = new List<BeatmapSetInfo>();
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
/// <summary>
/// Test keyboard traversal
/// </summary>
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
/// <summary>
/// Test filtering
/// </summary>
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<double>
{
Min = 2,
Max = 5.5,
IsLowerInclusive = true
}
}, false));
checkSelected(3, 2);
AddStep("Un-filter", () => carousel.Filter(new FilterCriteria(), false));
}
/// <summary>
/// Test random non-repeating algorithm
/// </summary>
private void testRandom()
[Test]
public void TestRandom()
{
loadBeatmaps();
setSelected(1, 1);
nextRandom();
@ -284,8 +297,11 @@ namespace osu.Game.Tests.Visual.SongSelect
/// <summary>
/// Test adding and removing beatmap sets
/// </summary>
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
/// <summary>
/// Test sorting
/// </summary>
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<BeatmapSetInfo>());
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<BeatmapSetInfo> { 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<BeatmapSetInfo> beatmapSets = new List<BeatmapSetInfo>();
List<BeatmapSetInfo> manySets = new List<BeatmapSetInfo>();
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();

View File

@ -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<Drawable> 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<Drawable>)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<Drawable>
{
public PaddedLabelledComponent()
: base(true)
@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.UserInterface
};
}
private class NonPaddedLabelledComponent : LabelledComponent
private class NonPaddedLabelledComponent : LabelledComponent<Drawable>
{
public NonPaddedLabelledComponent()
: base(false)

View File

@ -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<OsuTextBox> 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",
}
};

View File

@ -0,0 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Game.Tournament.Screens;
namespace osu.Game.Tournament.Tests.Screens
{
public class TestSceneSetupScreen : TournamentTestScene
{
[BackgroundDependencyLoader]
private void load()
{
Add(new SetupScreen());
}
}
}

View File

@ -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;
}
/// <summary>

View File

@ -0,0 +1,142 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.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<Drawable>
{
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()
},
}
};
}
}
}

View File

@ -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)

View File

@ -11,6 +11,6 @@
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.Win32.Registry" Version="4.5.0" />
<PackageReference Include="Microsoft.Win32.Registry" Version="4.6.0" />
</ItemGroup>
</Project>

View File

@ -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<T> : 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
/// <summary>
/// The component that is being displayed.
/// </summary>
protected readonly Drawable Component;
protected readonly T Component;
private readonly OsuTextFlowContainer labelText;
private readonly OsuTextFlowContainer descriptionText;
/// <summary>
/// Creates a new <see cref="LabelledComponent"/>.
/// Creates a new <see cref="LabelledComponent{T}"/>.
/// </summary>
/// <param name="padded">Whether the component should be padded or should be expanded to the bounds of this <see cref="LabelledComponent"/>.</param>
/// <param name="padded">Whether the component should be padded or should be expanded to the bounds of this <see cref="LabelledComponent{T}"/>.</param>
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.
/// </summary>
/// <returns>The component.</returns>
protected abstract Drawable CreateComponent();
protected abstract T CreateComponent();
}
}

View File

@ -0,0 +1,49 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Graphics.UserInterfaceV2
{
public class LabelledTextBox : LabelledComponent<OsuTextBox>
{
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));
}
}

View File

@ -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))
{

View File

@ -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;

View File

@ -1,16 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Rulesets.Objects
{
/// <summary>
/// A hit object representing the end of a bar.
/// </summary>
public class BarLine : HitObject
{
/// <summary>
/// Whether this barline is a prominent beat (based on time signature of beatmap).
/// </summary>
public bool Major;
}
}

View File

@ -10,12 +10,13 @@ using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Objects
{
public class BarLineGenerator
public class BarLineGenerator<TBarLine>
where TBarLine : class, IBarLine, new()
{
/// <summary>
/// The generated bar lines.
/// </summary>
public readonly List<BarLine> BarLines = new List<BarLine>();
public readonly List<TBarLine> BarLines = new List<TBarLine>();
/// <summary>
/// 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

View File

@ -0,0 +1,22 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Rulesets.Objects
{
/// <summary>
/// Interface for bar line hitobjects.
/// Used to decouple bar line generation from ruleset-specific rendering/drawing hierarchies.
/// </summary>
public interface IBarLine
{
/// <summary>
/// The time position of the bar.
/// </summary>
double StartTime { set; }
/// <summary>
/// Whether this bar line is a prominent beat (based on time signature of beatmap).
/// </summary>
bool Major { set; }
}
}

View File

@ -1,129 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.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;
}
}
}

View File

@ -31,6 +31,8 @@ namespace osu.Game.Screens.Menu
{
public event Action<ButtonState> 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;

View File

@ -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)

View File

@ -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();

View File

@ -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 &=

View File

@ -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(?<key>stars|ar|dr|cs|divisor|length|objects|bpm|status)(?<op>[=:><]+)(?<value>\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<BeatmapSetOnlineStatus>(value, true, out var statusValue):
updateCriteriaRange(ref criteria.OnlineStatus, op, statusValue);
break;
}
query = query.Replace(match.ToString(), "");
}
}
private void updateCriteriaRange(ref FilterCriteria.OptionalRange<float> 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<double> 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<T>(ref FilterCriteria.OptionalRange<T> 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;
}
}
}
}

View File

@ -23,6 +23,8 @@ namespace osu.Game.Screens.Select
public OptionalRange<double> BPM;
public OptionalRange<int> BeatDivisor;
public OptionalRange<BeatmapSetOnlineStatus> OnlineStatus;
public OptionalTextFilter Creator;
public OptionalTextFilter Artist;
public string[] SearchTerms = Array.Empty<string>();
@ -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<T> 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<OptionalTextFilter>
{
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;
}
}
}

View File

@ -0,0 +1,211 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.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(?<key>stars|ar|dr|cs|divisor|length|objects|bpm|status|creator|artist)(?<op>[=:><]+)(?<value>("".*"")|(\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<BeatmapSetOnlineStatus>(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<float> 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<double> 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<T>(ref FilterCriteria.OptionalRange<T> 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;
}
}
}
}

View File

@ -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
{
/// <summary>
/// An update manager that shows notifications if a newer release is detected.
/// Installation is left up to the user.
/// </summary>
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,11 @@ namespace osu.Desktop.Updater
case RuntimeInfo.Platform.MacOsx:
bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".app.zip"));
break;
case RuntimeInfo.Platform.Android:
// on our testing device this causes the download to magically disappear.
//bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".apk"));
break;
}
return bestAsset?.BrowserDownloadUrl ?? release.HtmlUrl;

View File

@ -0,0 +1,67 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.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<string>(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;
};
}
}
}
}

View File

@ -26,10 +26,10 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.913.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.921.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.924.0" />
<PackageReference Include="SharpCompress" Version="0.24.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.6.0" />
</ItemGroup>
</Project>

View File

@ -118,12 +118,12 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.913.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.921.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2019.921.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.924.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2019.924.0" />
<PackageReference Include="SharpCompress" Version="0.24.0" />
<PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.6.0" />
<PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2019.813.0" ExcludeAssets="all" />
</ItemGroup>
</Project>