Merge branch 'master' into speedpp

This commit is contained in:
smoogipoo 2021-09-16 14:18:47 +09:00
commit 2dd9d457e8
138 changed files with 2820 additions and 998 deletions

View File

@ -23,9 +23,9 @@ jobs:
continue-on-error: true continue-on-error: true
if: | if: |
${{ github.event.issue.pull_request }} && github.event.issue.pull_request &&
contains(github.event.comment.body, '!pp check') && contains(github.event.comment.body, '!pp check') &&
${{ github.event.comment.author_association == 'MEMBER' }} (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER')
strategy: strategy:
fail-fast: false fail-fast: false

View File

@ -1,8 +1,8 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Benchmarks" type="DotNetProject" factoryName=".NET Project"> <configuration default="false" name="Benchmarks" type="DotNetProject" factoryName=".NET Project">
<option name="EXE_PATH" value="$PROJECT_DIR$/osu.Game.Benchmarks/bin/Release/net5.0/osu.Game.Benchmarks.dll" /> <option name="EXE_PATH" value="$PROJECT_DIR$/osu.Game.Benchmarks/bin/Debug/net5.0/osu.Game.Benchmarks.dll" />
<option name="PROGRAM_PARAMETERS" value="--filter *" /> <option name="PROGRAM_PARAMETERS" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Game.Benchmarks/bin/Release/net5.0" /> <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/osu.Game.Benchmarks/bin/Debug/net5.0" />
<option name="PASS_PARENT_ENVS" value="1" /> <option name="PASS_PARENT_ENVS" value="1" />
<option name="USE_EXTERNAL_CONSOLE" value="0" /> <option name="USE_EXTERNAL_CONSOLE" value="0" />
<option name="USE_MONO" value="0" /> <option name="USE_MONO" value="0" />
@ -14,7 +14,7 @@
<option name="PROJECT_KIND" value="DotNetCore" /> <option name="PROJECT_KIND" value="DotNetCore" />
<option name="PROJECT_TFM" value="net5.0" /> <option name="PROJECT_TFM" value="net5.0" />
<method v="2"> <method v="2">
<option name="Build" enabled="true" /> <option name="Build" />
</method> </method>
</configuration> </configuration>
</component> </component>

View File

@ -31,12 +31,11 @@ If you are looking to install or test osu! without setting up a development envi
**Latest build:** **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) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS(iOS 10+)](https://osu.ppy.sh/home/testflight) | [Android (5+)](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk) | [Windows 8.1+ (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) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS 10+](https://osu.ppy.sh/home/testflight) | [Android 5+](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
| ------------- | ------------- | ------------- | ------------- | ------------- | | ------------- | ------------- | ------------- | ------------- | ------------- |
- The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets. - The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets.
- When running on Windows 7 or 8.1, *[additional prerequisites](https://docs.microsoft.com/en-us/dotnet/core/install/windows?tabs=net50&pivots=os-windows#dependencies)** may be required to correctly run .NET 5 applications if your operating system is not up-to-date with the latest service packs.
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below. If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.
## Developing a custom ruleset ## Developing a custom ruleset

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.907.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.913.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.830.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.907.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -14,9 +14,9 @@ namespace osu.Game.Benchmarks
[Params(1, 10, 100)] [Params(1, 10, 100)]
public int Times { get; set; } public int Times { get; set; }
[GlobalSetup] public override void SetUp()
public void GlobalSetup()
{ {
base.SetUp();
mod = new OsuModDoubleTime(); mod = new OsuModDoubleTime();
} }

View File

@ -0,0 +1,62 @@
// 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 BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Engines;
using osu.Game.Online.API;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
namespace osu.Game.Benchmarks
{
public class BenchmarkRuleset : BenchmarkTest
{
private OsuRuleset ruleset;
private APIMod apiModDoubleTime;
private APIMod apiModDifficultyAdjust;
public override void SetUp()
{
base.SetUp();
ruleset = new OsuRuleset();
apiModDoubleTime = new APIMod { Acronym = "DT" };
apiModDifficultyAdjust = new APIMod { Acronym = "DA" };
}
[Benchmark]
public void BenchmarkToModDoubleTime()
{
apiModDoubleTime.ToMod(ruleset);
}
[Benchmark]
public void BenchmarkToModDifficultyAdjust()
{
apiModDifficultyAdjust.ToMod(ruleset);
}
[Benchmark]
public void BenchmarkGetAllMods()
{
ruleset.CreateAllMods().Consume(new Consumer());
}
[Benchmark]
public void BenchmarkGetAllModsForReference()
{
ruleset.AllMods.Consume(new Consumer());
}
[Benchmark]
public void BenchmarkGetForAcronym()
{
ruleset.CreateModFromAcronym("DT");
}
[Benchmark]
public void BenchmarkGetForType()
{
ruleset.CreateMod<ModDoubleTime>();
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running; using BenchmarkDotNet.Running;
namespace osu.Game.Benchmarks namespace osu.Game.Benchmarks
@ -11,7 +12,7 @@ namespace osu.Game.Benchmarks
{ {
BenchmarkSwitcher BenchmarkSwitcher
.FromAssembly(typeof(Program).Assembly) .FromAssembly(typeof(Program).Assembly)
.Run(args); .Run(args, DefaultConfig.Instance.WithOption(ConfigOptions.DisableOptimizationsValidator, true));
} }
} }
} }

View File

@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{ {
c.Add(CreateHitObject().With(h => c.Add(CreateHitObject().With(h =>
{ {
h.HitObject.StartTime = START_TIME; h.HitObject.StartTime = Time.Current + 5000;
h.AccentColour.Value = Color4.Orange; h.AccentColour.Value = Color4.Orange;
})); }));
}) })
@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
{ {
c.Add(CreateHitObject().With(h => c.Add(CreateHitObject().With(h =>
{ {
h.HitObject.StartTime = START_TIME; h.HitObject.StartTime = Time.Current + 5000;
h.AccentColour.Value = Color4.Orange; h.AccentColour.Value = Color4.Orange;
})); }));
}) })

View File

@ -19,8 +19,6 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
/// </summary> /// </summary>
public abstract class ManiaSkinnableTestScene : SkinnableTestScene public abstract class ManiaSkinnableTestScene : SkinnableTestScene
{ {
protected const double START_TIME = 1000000000;
[Cached(Type = typeof(IScrollingInfo))] [Cached(Type = typeof(IScrollingInfo))]
private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo(); private readonly TestScrollingInfo scrollingInfo = new TestScrollingInfo();
@ -55,27 +53,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning
public readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>(); public readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
IBindable<ScrollingDirection> IScrollingInfo.Direction => Direction; IBindable<ScrollingDirection> IScrollingInfo.Direction => Direction;
IBindable<double> IScrollingInfo.TimeRange { get; } = new Bindable<double>(1000); IBindable<double> IScrollingInfo.TimeRange { get; } = new Bindable<double>(5000);
IScrollAlgorithm IScrollingInfo.Algorithm { get; } = new ZeroScrollAlgorithm(); IScrollAlgorithm IScrollingInfo.Algorithm { get; } = new ConstantScrollAlgorithm();
}
private class ZeroScrollAlgorithm : IScrollAlgorithm
{
public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength)
=> double.MinValue;
public float GetLength(double startTime, double endTime, double timeRange, float scrollLength)
=> scrollLength;
public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
=> (float)((time - START_TIME) / timeRange) * scrollLength;
public double TimeAt(float position, double currentTime, double timeRange, float scrollLength)
=> 0;
public void Reset()
{
}
} }
} }
} }

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -13,6 +14,10 @@ using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Configuration;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Tests namespace osu.Game.Rulesets.Mania.Tests
{ {
@ -22,14 +27,65 @@ namespace osu.Game.Rulesets.Mania.Tests
[Resolved] [Resolved]
private RulesetConfigCache configCache { get; set; } private RulesetConfigCache configCache { get; set; }
private readonly Bindable<bool> configTimingBasedNoteColouring = new Bindable<bool>(); private Bindable<bool> configTimingBasedNoteColouring;
protected override void LoadComplete() private ManualClock clock;
private DrawableManiaRuleset drawableRuleset;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("setup hierarchy", () => Child = new Container
{
Clock = new FramedClock(clock = new ManualClock()),
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new[]
{
drawableRuleset = (DrawableManiaRuleset)Ruleset.Value.CreateInstance().CreateDrawableRulesetWith(createTestBeatmap())
}
});
AddStep("retrieve config bindable", () =>
{
var config = (ManiaRulesetConfigManager)configCache.GetConfigFor(Ruleset.Value.CreateInstance());
configTimingBasedNoteColouring = config.GetBindable<bool>(ManiaRulesetSetting.TimingBasedNoteColouring);
});
}
[Test]
public void TestSimple()
{
AddStep("enable", () => configTimingBasedNoteColouring.Value = true);
AddStep("disable", () => configTimingBasedNoteColouring.Value = false);
}
[Test]
public void TestToggleOffScreen()
{
AddStep("enable", () => configTimingBasedNoteColouring.Value = true);
seekTo(10000);
AddStep("disable", () => configTimingBasedNoteColouring.Value = false);
seekTo(0);
AddAssert("all notes not coloured", () => this.ChildrenOfType<DrawableNote>().All(note => note.Colour == Colour4.White));
seekTo(10000);
AddStep("enable again", () => configTimingBasedNoteColouring.Value = true);
seekTo(0);
AddAssert("some notes coloured", () => this.ChildrenOfType<DrawableNote>().Any(note => note.Colour != Colour4.White));
}
private void seekTo(double time)
{
AddStep($"seek to {time}", () => clock.CurrentTime = time);
AddUntilStep("wait for seek", () => Precision.AlmostEquals(drawableRuleset.FrameStableClock.CurrentTime, time, 1));
}
private ManiaBeatmap createTestBeatmap()
{ {
const double beat_length = 500; const double beat_length = 500;
var ruleset = new ManiaRuleset();
var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 }) var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 })
{ {
HitObjects = HitObjects =
@ -45,7 +101,7 @@ namespace osu.Game.Rulesets.Mania.Tests
new Note { StartTime = beat_length } new Note { StartTime = beat_length }
}, },
ControlPointInfo = new ControlPointInfo(), ControlPointInfo = new ControlPointInfo(),
BeatmapInfo = { Ruleset = ruleset.RulesetInfo }, BeatmapInfo = { Ruleset = Ruleset.Value },
}; };
foreach (var note in beatmap.HitObjects) foreach (var note in beatmap.HitObjects)
@ -57,24 +113,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{ {
BeatLength = beat_length BeatLength = beat_length
}); });
return beatmap;
Child = new Container
{
Clock = new FramedClock(new ManualClock()),
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new[]
{
ruleset.CreateDrawableRulesetWith(beatmap)
}
};
var config = (ManiaRulesetConfigManager)configCache.GetConfigFor(Ruleset.Value.CreateInstance());
config.BindWith(ManiaRulesetSetting.TimingBasedNoteColouring, configTimingBasedNoteColouring);
AddStep("Enable", () => configTimingBasedNoteColouring.Value = true);
AddStep("Disable", () => configTimingBasedNoteColouring.Value = false);
} }
} }
} }

View File

@ -66,6 +66,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
StartTimeBindable.BindValueChanged(_ => updateSnapColour(), true); StartTimeBindable.BindValueChanged(_ => updateSnapColour(), true);
} }
protected override void OnApply()
{
base.OnApply();
updateSnapColour();
}
protected override void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> e) protected override void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> e)
{ {
base.OnDirectionChanged(e); base.OnDirectionChanged(e);

View File

@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase(6.7568168283591499d, "diffcalc-test")] [TestCase(6.6634445062299665d, "diffcalc-test")]
[TestCase(1.0348244046058293d, "zero-length-sliders")] [TestCase(1.0414203870195022d, "zero-length-sliders")]
public void Test(double expected, string name) public void Test(double expected, string name)
=> base.Test(expected, name); => base.Test(expected, name);
[TestCase(8.4783236764532557d, "diffcalc-test")] [TestCase(8.3858089051603368d, "diffcalc-test")]
[TestCase(1.2708532136987165d, "zero-length-sliders")] [TestCase(1.2723279173428435d, "zero-length-sliders")]
public void TestClockRateAdjusted(double expected, string name) public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new OsuModDoubleTime()); => Test(expected, name, new OsuModDoubleTime());

View File

@ -35,7 +35,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier; double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier; double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
double starRating = aimRating + speedRating + Math.Abs(aimRating - speedRating) / 2;
double baseAimPerformance = Math.Pow(5 * Math.Max(1, aimRating / 0.0675) - 4, 3) / 100000;
double baseSpeedPerformance = Math.Pow(5 * Math.Max(1, speedRating / 0.0675) - 4, 3) / 100000;
double basePerformance = Math.Pow(Math.Pow(baseAimPerformance, 1.1) + Math.Pow(baseSpeedPerformance, 1.1), 1 / 1.1);
double starRating = basePerformance > 0.00001 ? Math.Cbrt(1.12) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4) : 0;
double preempt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; double preempt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;

View File

@ -64,6 +64,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsFalse(beatmapInfo.LetterboxInBreaks); Assert.IsFalse(beatmapInfo.LetterboxInBreaks);
Assert.IsFalse(beatmapInfo.SpecialStyle); Assert.IsFalse(beatmapInfo.SpecialStyle);
Assert.IsFalse(beatmapInfo.WidescreenStoryboard); Assert.IsFalse(beatmapInfo.WidescreenStoryboard);
Assert.IsFalse(beatmapInfo.SamplesMatchPlaybackRate);
Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown); Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown);
Assert.AreEqual(0, beatmapInfo.CountdownOffset); Assert.AreEqual(0, beatmapInfo.CountdownOffset);
} }

View File

@ -3,6 +3,7 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Tests.Mods namespace osu.Game.Tests.Mods
@ -11,26 +12,42 @@ namespace osu.Game.Tests.Mods
public class ModSettingsEqualityComparison public class ModSettingsEqualityComparison
{ {
[Test] [Test]
public void Test() public void TestAPIMod()
{ {
var apiMod1 = new APIMod(new OsuModDoubleTime { SpeedChange = { Value = 1.25 } });
var apiMod2 = new APIMod(new OsuModDoubleTime { SpeedChange = { Value = 1.26 } });
var apiMod3 = new APIMod(new OsuModDoubleTime { SpeedChange = { Value = 1.26 } });
Assert.That(apiMod1, Is.Not.EqualTo(apiMod2));
Assert.That(apiMod2, Is.EqualTo(apiMod2));
Assert.That(apiMod2, Is.EqualTo(apiMod3));
Assert.That(apiMod3, Is.EqualTo(apiMod2));
}
[Test]
public void TestMod()
{
var ruleset = new OsuRuleset();
var mod1 = new OsuModDoubleTime { SpeedChange = { Value = 1.25 } }; var mod1 = new OsuModDoubleTime { SpeedChange = { Value = 1.25 } };
var mod2 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } }; var mod2 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } };
var mod3 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } }; var mod3 = new OsuModDoubleTime { SpeedChange = { Value = 1.26 } };
var apiMod1 = new APIMod(mod1);
var apiMod2 = new APIMod(mod2); var doubleConvertedMod1 = new APIMod(mod1).ToMod(ruleset);
var apiMod3 = new APIMod(mod3); var doulbeConvertedMod2 = new APIMod(mod2).ToMod(ruleset);
var doulbeConvertedMod3 = new APIMod(mod3).ToMod(ruleset);
Assert.That(mod1, Is.Not.EqualTo(mod2)); Assert.That(mod1, Is.Not.EqualTo(mod2));
Assert.That(apiMod1, Is.Not.EqualTo(apiMod2)); Assert.That(doubleConvertedMod1, Is.Not.EqualTo(doulbeConvertedMod2));
Assert.That(mod2, Is.EqualTo(mod2)); Assert.That(mod2, Is.EqualTo(mod2));
Assert.That(apiMod2, Is.EqualTo(apiMod2)); Assert.That(doulbeConvertedMod2, Is.EqualTo(doulbeConvertedMod2));
Assert.That(mod2, Is.EqualTo(mod3)); Assert.That(mod2, Is.EqualTo(mod3));
Assert.That(apiMod2, Is.EqualTo(apiMod3)); Assert.That(doulbeConvertedMod2, Is.EqualTo(doulbeConvertedMod3));
Assert.That(mod3, Is.EqualTo(mod2)); Assert.That(mod3, Is.EqualTo(mod2));
Assert.That(apiMod3, Is.EqualTo(apiMod2)); Assert.That(doulbeConvertedMod3, Is.EqualTo(doulbeConvertedMod2));
} }
} }
} }

View File

@ -40,10 +40,10 @@ namespace osu.Game.Tests.NonVisual.Skinning
assertPlaybackPosition(0); assertPlaybackPosition(0);
AddStep("set start time to 1000", () => animationTimeReference.AnimationStartTime.Value = 1000); AddStep("set start time to 1000", () => animationTimeReference.AnimationStartTime.Value = 1000);
assertPlaybackPosition(-1000); assertPlaybackPosition(0);
AddStep("set current time to 500", () => animationTimeReference.ManualClock.CurrentTime = 500); AddStep("set current time to 500", () => animationTimeReference.ManualClock.CurrentTime = 500);
assertPlaybackPosition(-500); assertPlaybackPosition(0);
} }
private void assertPlaybackPosition(double expectedPosition) private void assertPlaybackPosition(double expectedPosition)

View File

@ -0,0 +1,177 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Tests.Beatmaps.IO;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneDifficultySwitching : EditorTestScene
{
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected override bool IsolateSavingFromDatabase => false;
[Resolved]
private OsuGameBase game { get; set; }
[Resolved]
private BeatmapManager beatmaps { get; set; }
private BeatmapSetInfo importedBeatmapSet;
public override void SetUpSteps()
{
AddStep("import test beatmap", () => importedBeatmapSet = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).Result);
base.SetUpSteps();
}
protected override void LoadEditor()
{
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First());
base.LoadEditor();
}
[Test]
public void TestBasicSwitch()
{
BeatmapInfo targetDifficulty = null;
AddStep("set target difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo)));
switchToDifficulty(() => targetDifficulty);
confirmEditingBeatmap(() => targetDifficulty);
AddStep("exit editor", () => Stack.Exit());
// ensure editor loader didn't resume.
AddAssert("stack empty", () => Stack.CurrentScreen == null);
}
[Test]
public void TestClockPositionPreservedBetweenSwitches()
{
BeatmapInfo targetDifficulty = null;
AddStep("seek editor to 00:05:00", () => EditorClock.Seek(5000));
AddStep("set target difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo)));
switchToDifficulty(() => targetDifficulty);
confirmEditingBeatmap(() => targetDifficulty);
AddAssert("editor clock at 00:05:00", () => EditorClock.CurrentTime == 5000);
AddStep("exit editor", () => Stack.Exit());
// ensure editor loader didn't resume.
AddAssert("stack empty", () => Stack.CurrentScreen == null);
}
[Test]
public void TestClipboardPreservedAfterSwitch([Values] bool sameRuleset)
{
BeatmapInfo targetDifficulty = null;
AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.First()));
AddStep("copy object", () => Editor.Copy());
AddStep("set target difficulty", () =>
{
targetDifficulty = sameRuleset
? importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo) && beatmap.RulesetID == Beatmap.Value.BeatmapInfo.RulesetID)
: importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo) && beatmap.RulesetID != Beatmap.Value.BeatmapInfo.RulesetID);
});
switchToDifficulty(() => targetDifficulty);
confirmEditingBeatmap(() => targetDifficulty);
AddAssert("no objects selected", () => !EditorBeatmap.SelectedHitObjects.Any());
AddStep("paste object", () => Editor.Paste());
if (sameRuleset)
AddAssert("object was pasted", () => EditorBeatmap.SelectedHitObjects.Any());
else
AddAssert("object was not pasted", () => !EditorBeatmap.SelectedHitObjects.Any());
AddStep("exit editor", () => Stack.Exit());
if (sameRuleset)
{
AddUntilStep("prompt for save dialog shown", () => DialogOverlay.CurrentDialog is PromptForSaveDialog);
AddStep("discard changes", () => ((PromptForSaveDialog)DialogOverlay.CurrentDialog).PerformOkAction());
}
// ensure editor loader didn't resume.
AddAssert("stack empty", () => Stack.CurrentScreen == null);
}
[Test]
public void TestPreventSwitchDueToUnsavedChanges()
{
BeatmapInfo targetDifficulty = null;
PromptForSaveDialog saveDialog = null;
AddStep("remove first hitobject", () => EditorBeatmap.RemoveAt(0));
AddStep("set target difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo)));
switchToDifficulty(() => targetDifficulty);
AddUntilStep("prompt for save dialog shown", () =>
{
saveDialog = this.ChildrenOfType<PromptForSaveDialog>().Single();
return saveDialog != null;
});
AddStep("continue editing", () =>
{
var continueButton = saveDialog.ChildrenOfType<PopupDialogCancelButton>().Last();
continueButton.TriggerClick();
});
confirmEditingBeatmap(() => importedBeatmapSet.Beatmaps.First());
AddRepeatStep("exit editor forcefully", () => Stack.Exit(), 2);
// ensure editor loader didn't resume.
AddAssert("stack empty", () => Stack.CurrentScreen == null);
}
[Test]
public void TestAllowSwitchAfterDiscardingUnsavedChanges()
{
BeatmapInfo targetDifficulty = null;
PromptForSaveDialog saveDialog = null;
AddStep("remove first hitobject", () => EditorBeatmap.RemoveAt(0));
AddStep("set target difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo)));
switchToDifficulty(() => targetDifficulty);
AddUntilStep("prompt for save dialog shown", () =>
{
saveDialog = this.ChildrenOfType<PromptForSaveDialog>().Single();
return saveDialog != null;
});
AddStep("discard changes", () =>
{
var continueButton = saveDialog.ChildrenOfType<PopupDialogOkButton>().Single();
continueButton.TriggerClick();
});
confirmEditingBeatmap(() => targetDifficulty);
AddStep("exit editor forcefully", () => Stack.Exit());
// ensure editor loader didn't resume.
AddAssert("stack empty", () => Stack.CurrentScreen == null);
}
private void switchToDifficulty(Func<BeatmapInfo> difficulty) => AddStep("switch to difficulty", () => Editor.SwitchToDifficulty(difficulty.Invoke()));
private void confirmEditingBeatmap(Func<BeatmapInfo> targetDifficulty)
{
AddUntilStep("current beatmap is correct", () => Beatmap.Value.BeatmapInfo.Equals(targetDifficulty.Invoke()));
AddUntilStep("current screen is editor", () => Stack.CurrentScreen == Editor && Editor?.IsLoaded == true);
}
}
}

View File

@ -11,6 +11,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Edit.Setup;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using SharpCompress.Archives; using SharpCompress.Archives;
@ -55,6 +56,9 @@ namespace osu.Game.Tests.Visual.Editing
[Test] [Test]
public void TestExitWithoutSave() public void TestExitWithoutSave()
{ {
EditorBeatmap editorBeatmap = null;
AddStep("store editor beatmap", () => editorBeatmap = EditorBeatmap);
AddStep("exit without save", () => AddStep("exit without save", () =>
{ {
Editor.Exit(); Editor.Exit();
@ -62,7 +66,7 @@ namespace osu.Game.Tests.Visual.Editing
}); });
AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen()); AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen());
AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == true); AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == editorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == true);
} }
[Test] [Test]

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -71,7 +70,7 @@ namespace osu.Game.Tests.Visual.Gameplay
var working = CreateWorkingBeatmap(rulesetInfo); var working = CreateWorkingBeatmap(rulesetInfo);
Beatmap.Value = working; Beatmap.Value = working;
SelectedMods.Value = new[] { ruleset.GetAllMods().First(m => m is ModNoFail) }; SelectedMods.Value = new[] { ruleset.CreateMod<ModNoFail>() };
Player = CreatePlayer(ruleset); Player = CreatePlayer(ruleset);

View File

@ -54,7 +54,11 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
recordingManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) recordingManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{ {
Recorder = recorder = new TestReplayRecorder(new Score { Replay = replay }) Recorder = recorder = new TestReplayRecorder(new Score
{
Replay = replay,
ScoreInfo = { Beatmap = gameplayBeatmap.BeatmapInfo }
})
{ {
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos),
}, },

View File

@ -45,7 +45,11 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
recordingManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) recordingManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique)
{ {
Recorder = new TestReplayRecorder(new Score { Replay = replay }) Recorder = new TestReplayRecorder(new Score
{
Replay = replay,
ScoreInfo = { Beatmap = gameplayBeatmap.BeatmapInfo }
})
{ {
ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos) ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos)
}, },

View File

@ -354,7 +354,7 @@ namespace osu.Game.Tests.Visual.Gameplay
internal class TestReplayRecorder : ReplayRecorder<TestAction> internal class TestReplayRecorder : ReplayRecorder<TestAction>
{ {
public TestReplayRecorder() public TestReplayRecorder()
: base(new Score()) : base(new Score { ScoreInfo = { Beatmap = new BeatmapInfo() } })
{ {
} }

View File

@ -130,6 +130,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
Type = { Value = MatchType.HeadToHead }, Type = { Value = MatchType.HeadToHead },
})); }));
AddUntilStep("wait for panel load", () => drawableRoom.ChildrenOfType<RecentParticipantsList>().Any());
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha)); AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
AddStep("set password", () => room.Password.Value = "password"); AddStep("set password", () => room.Password.Value = "password");

View File

@ -3,6 +3,7 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -51,6 +52,24 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("room join password correct", () => lastJoinedPassword == null); AddAssert("room join password correct", () => lastJoinedPassword == null);
} }
[Test]
public void TestPopoverHidesOnBackButton()
{
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().Any());
AddAssert("textbox has focus", () => InputManager.FocusedDrawable is OsuPasswordTextBox);
AddStep("hit escape", () => InputManager.Key(Key.Escape));
AddAssert("textbox lost focus", () => InputManager.FocusedDrawable is SearchTextBox);
AddStep("hit escape", () => InputManager.Key(Key.Escape));
AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().Any());
}
[Test] [Test]
public void TestPopoverHidesOnLeavingScreen() public void TestPopoverHidesOnLeavingScreen()
{ {
@ -64,7 +83,23 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
[Test] [Test]
public void TestJoinRoomWithPassword() public void TestJoinRoomWithIncorrectPassword()
{
DrawableLoungeRoom.PasswordEntryPopover passwordEntryPopover = null;
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "wrong");
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().TriggerClick());
AddAssert("room not joined", () => loungeScreen.IsCurrentScreen());
AddUntilStep("password prompt still visible", () => passwordEntryPopover.State.Value == Visibility.Visible);
}
[Test]
public void TestJoinRoomWithCorrectPassword()
{ {
DrawableLoungeRoom.PasswordEntryPopover passwordEntryPopover = null; DrawableLoungeRoom.PasswordEntryPopover passwordEntryPopover = null;

View File

@ -3,6 +3,7 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
@ -15,11 +16,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
SelectedRoom.Value = new Room(); SelectedRoom.Value = new Room();
Child = new MultiplayerMatchFooter Child = new Container
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Height = 50 RelativeSizeAxes = Axes.X,
Height = 50,
Child = new MultiplayerMatchFooter()
}; };
}); });
} }

View File

@ -75,7 +75,6 @@ namespace osu.Game.Tests.Visual.Navigation
typeof(FileStore), typeof(FileStore),
typeof(ScoreManager), typeof(ScoreManager),
typeof(BeatmapManager), typeof(BeatmapManager),
typeof(SettingsStore),
typeof(RulesetConfigCache), typeof(RulesetConfigCache),
typeof(OsuColour), typeof(OsuColour),
typeof(IBindable<WorkingBeatmap>), typeof(IBindable<WorkingBeatmap>),

View File

@ -15,6 +15,7 @@ using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Toolbar; using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -388,6 +389,19 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("now playing is hidden", () => nowPlayingOverlay.State.Value == Visibility.Hidden); AddAssert("now playing is hidden", () => nowPlayingOverlay.State.Value == Visibility.Hidden);
} }
[Test]
public void TestExitGameFromSongSelect()
{
PushAndConfirm(() => new TestPlaySongSelect());
exitViaEscapeAndConfirm();
pushEscape(); // returns to osu! logo
AddStep("Hold escape", () => InputManager.PressKey(Key.Escape));
AddUntilStep("Wait for intro", () => Game.ScreenStack.CurrentScreen is IntroTriangles);
AddUntilStep("Wait for game exit", () => Game.ScreenStack.CurrentScreen == null);
}
private void pushEscape() => private void pushEscape() =>
AddStep("Press escape", () => InputManager.Key(Key.Escape)); AddStep("Press escape", () => InputManager.Key(Key.Escape));

View File

@ -5,10 +5,10 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
@ -17,10 +17,11 @@ using osu.Game.Overlays.BeatmapListing;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users; using osu.Game.Users;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Online namespace osu.Game.Tests.Visual.Online
{ {
public class TestSceneBeatmapListingOverlay : OsuTestScene public class TestSceneBeatmapListingOverlay : OsuManualInputManagerTestScene
{ {
private readonly List<APIBeatmapSet> setsForResponse = new List<APIBeatmapSet>(); private readonly List<APIBeatmapSet> setsForResponse = new List<APIBeatmapSet>();
@ -28,27 +29,33 @@ namespace osu.Game.Tests.Visual.Online
private BeatmapListingSearchControl searchControl => overlay.ChildrenOfType<BeatmapListingSearchControl>().Single(); private BeatmapListingSearchControl searchControl => overlay.ChildrenOfType<BeatmapListingSearchControl>().Single();
[BackgroundDependencyLoader] [SetUpSteps]
private void load() public void SetUpSteps()
{ {
Child = overlay = new BeatmapListingOverlay { State = { Value = Visibility.Visible } }; AddStep("setup overlay", () =>
((DummyAPIAccess)API).HandleRequest = req =>
{ {
if (!(req is SearchBeatmapSetsRequest searchBeatmapSetsRequest)) return false; Child = overlay = new BeatmapListingOverlay { State = { Value = Visibility.Visible } };
setsForResponse.Clear();
searchBeatmapSetsRequest.TriggerSuccess(new SearchBeatmapSetsResponse });
{
BeatmapSets = setsForResponse,
});
return true;
};
AddStep("initialize dummy", () => AddStep("initialize dummy", () =>
{ {
var api = (DummyAPIAccess)API;
api.HandleRequest = req =>
{
if (!(req is SearchBeatmapSetsRequest searchBeatmapSetsRequest)) return false;
searchBeatmapSetsRequest.TriggerSuccess(new SearchBeatmapSetsResponse
{
BeatmapSets = setsForResponse,
});
return true;
};
// non-supporter user // non-supporter user
((DummyAPIAccess)API).LocalUser.Value = new User api.LocalUser.Value = new User
{ {
Username = "TestBot", Username = "TestBot",
Id = API.LocalUser.Value.Id + 1, Id = API.LocalUser.Value.Id + 1,
@ -56,6 +63,51 @@ namespace osu.Game.Tests.Visual.Online
}); });
} }
[Test]
public void TestHideViaBack()
{
AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
AddStep("hide", () => InputManager.Key(Key.Escape));
AddUntilStep("is hidden", () => overlay.State.Value == Visibility.Hidden);
}
[Test]
public void TestHideViaBackWithSearch()
{
AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
AddStep("search something", () => overlay.ChildrenOfType<SearchTextBox>().First().Text = "search");
AddStep("kill search", () => InputManager.Key(Key.Escape));
AddAssert("search textbox empty", () => string.IsNullOrEmpty(overlay.ChildrenOfType<SearchTextBox>().First().Text));
AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
AddStep("hide", () => InputManager.Key(Key.Escape));
AddUntilStep("is hidden", () => overlay.State.Value == Visibility.Hidden);
}
[Test]
public void TestHideViaBackWithScrolledSearch()
{
AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
AddStep("show many results", () => fetchFor(Enumerable.Repeat(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet, 100).ToArray()));
AddUntilStep("placeholder hidden", () => !overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().Any(d => d.IsPresent));
AddStep("scroll to bottom", () => overlay.ChildrenOfType<OverlayScrollContainer>().First().ScrollToEnd());
AddStep("kill search", () => InputManager.Key(Key.Escape));
AddUntilStep("search textbox empty", () => string.IsNullOrEmpty(overlay.ChildrenOfType<SearchTextBox>().First().Text));
AddUntilStep("is scrolled to top", () => overlay.ChildrenOfType<OverlayScrollContainer>().First().Current == 0);
AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
AddStep("hide", () => InputManager.Key(Key.Escape));
AddUntilStep("is hidden", () => overlay.State.Value == Visibility.Hidden);
}
[Test] [Test]
public void TestNoBeatmapsPlaceholder() public void TestNoBeatmapsPlaceholder()
{ {
@ -63,7 +115,7 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep("placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true); AddUntilStep("placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true);
AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet)); AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet));
AddUntilStep("placeholder hidden", () => !overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().Any()); AddUntilStep("placeholder hidden", () => !overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().Any(d => d.IsPresent));
AddStep("fetch for 0 beatmaps", () => fetchFor()); AddStep("fetch for 0 beatmaps", () => fetchFor());
AddUntilStep("placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true); AddUntilStep("placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true);
@ -193,13 +245,15 @@ namespace osu.Game.Tests.Visual.Online
noPlaceholderShown(); noPlaceholderShown();
} }
private static int searchCount;
private void fetchFor(params BeatmapSetInfo[] beatmaps) private void fetchFor(params BeatmapSetInfo[] beatmaps)
{ {
setsForResponse.Clear(); setsForResponse.Clear();
setsForResponse.AddRange(beatmaps.Select(b => new TestAPIBeatmapSet(b))); setsForResponse.AddRange(beatmaps.Select(b => new TestAPIBeatmapSet(b)));
// trigger arbitrary change for fetching. // trigger arbitrary change for fetching.
searchControl.Query.TriggerChange(); searchControl.Query.Value = $"search {searchCount++}";
} }
private void setRankAchievedFilter(ScoreRank[] ranks) private void setRankAchievedFilter(ScoreRank[] ranks)
@ -229,8 +283,8 @@ namespace osu.Game.Tests.Visual.Online
private void noPlaceholderShown() private void noPlaceholderShown()
{ {
AddUntilStep("no placeholder shown", () => AddUntilStep("no placeholder shown", () =>
!overlay.ChildrenOfType<BeatmapListingOverlay.SupporterRequiredDrawable>().Any() !overlay.ChildrenOfType<BeatmapListingOverlay.SupporterRequiredDrawable>().Any(d => d.IsPresent)
&& !overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().Any()); && !overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().Any(d => d.IsPresent));
} }
private class TestAPIBeatmapSet : APIBeatmapSet private class TestAPIBeatmapSet : APIBeatmapSet

View File

@ -21,6 +21,8 @@ namespace osu.Game.Tests.Visual.Online
protected override bool UseOnlineAPI => true; protected override bool UseOnlineAPI => true;
private int nextBeatmapSetId = 1;
public TestSceneBeatmapSetOverlay() public TestSceneBeatmapSetOverlay()
{ {
Add(overlay = new TestBeatmapSetOverlay()); Add(overlay = new TestBeatmapSetOverlay());
@ -240,12 +242,23 @@ namespace osu.Game.Tests.Visual.Online
{ {
AddStep("show explicit map", () => AddStep("show explicit map", () =>
{ {
var beatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet; var beatmapSet = getBeatmapSet();
beatmapSet.OnlineInfo.HasExplicitContent = true; beatmapSet.OnlineInfo.HasExplicitContent = true;
overlay.ShowBeatmapSet(beatmapSet); overlay.ShowBeatmapSet(beatmapSet);
}); });
} }
[Test]
public void TestFeaturedBeatmap()
{
AddStep("show featured map", () =>
{
var beatmapSet = getBeatmapSet();
beatmapSet.OnlineInfo.TrackId = 1;
overlay.ShowBeatmapSet(beatmapSet);
});
}
[Test] [Test]
public void TestHide() public void TestHide()
{ {
@ -308,6 +321,14 @@ namespace osu.Game.Tests.Visual.Online
}; };
} }
private BeatmapSetInfo getBeatmapSet()
{
var beatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet;
// Make sure the overlay is reloaded (see `BeatmapSetInfo.Equals`).
beatmapSet.OnlineBeatmapSetID = nextBeatmapSetId++;
return beatmapSet;
}
private void downloadAssert(bool shown) private void downloadAssert(bool shown)
{ {
AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.Header.HeaderContent.DownloadButtonsVisible == shown); AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.Header.HeaderContent.DownloadButtonsVisible == shown);

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
@ -85,6 +86,22 @@ namespace osu.Game.Tests.Visual.Online
case JoinChannelRequest joinChannel: case JoinChannelRequest joinChannel:
joinChannel.TriggerSuccess(); joinChannel.TriggerSuccess();
return true; return true;
case GetUserRequest getUser:
if (getUser.Lookup.Equals("some body", StringComparison.OrdinalIgnoreCase))
{
getUser.TriggerSuccess(new User
{
Username = "some body",
Id = 1,
});
}
else
{
getUser.TriggerFailure(new Exception());
}
return true;
} }
return false; return false;
@ -322,6 +339,27 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("Current channel is channel 1", () => currentChannel == channel1); AddAssert("Current channel is channel 1", () => currentChannel == channel1);
} }
[Test]
public void TestChatCommand()
{
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
AddStep("Open chat with user", () => channelManager.PostCommand("chat some body"));
AddAssert("PM channel is selected", () =>
channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single().Username == "some body");
AddStep("Open chat with non-existent user", () => channelManager.PostCommand("chat nobody"));
AddAssert("Last message is error", () => channelManager.CurrentChannel.Value.Messages.Last() is ErrorMessage);
// Make sure no unnecessary requests are made when the PM channel is already open.
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
AddStep("Unregister request handling", () => ((DummyAPIAccess)API).HandleRequest = null);
AddStep("Open chat with user", () => channelManager.PostCommand("chat some body"));
AddAssert("PM channel is selected", () =>
channelManager.CurrentChannel.Value.Type == ChannelType.PM && channelManager.CurrentChannel.Value.Users.Single().Username == "some body");
}
private void pressChannelHotkey(int number) private void pressChannelHotkey(int number)
{ {
var channelKey = Key.Number0 + number; var channelKey = Key.Number0 + number;

View File

@ -99,16 +99,23 @@ namespace osu.Game.Tests.Visual.Online
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(RulesetStore rulesets) private void load(RulesetStore rulesets)
{ {
var normal = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet; var normal = getBeatmapSet();
normal.OnlineInfo.HasVideo = true; normal.OnlineInfo.HasVideo = true;
normal.OnlineInfo.HasStoryboard = true; normal.OnlineInfo.HasStoryboard = true;
var undownloadable = getUndownloadableBeatmapSet(); var undownloadable = getUndownloadableBeatmapSet();
var manyDifficulties = getManyDifficultiesBeatmapSet(rulesets); var manyDifficulties = getManyDifficultiesBeatmapSet(rulesets);
var explicitMap = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet; var explicitMap = getBeatmapSet();
explicitMap.OnlineInfo.HasExplicitContent = true; explicitMap.OnlineInfo.HasExplicitContent = true;
var featuredMap = getBeatmapSet();
featuredMap.OnlineInfo.TrackId = 1;
var explicitFeaturedMap = getBeatmapSet();
explicitFeaturedMap.OnlineInfo.HasExplicitContent = true;
explicitFeaturedMap.OnlineInfo.TrackId = 2;
Child = new BasicScrollContainer Child = new BasicScrollContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -125,13 +132,19 @@ namespace osu.Game.Tests.Visual.Online
new GridBeatmapPanel(undownloadable), new GridBeatmapPanel(undownloadable),
new GridBeatmapPanel(manyDifficulties), new GridBeatmapPanel(manyDifficulties),
new GridBeatmapPanel(explicitMap), new GridBeatmapPanel(explicitMap),
new GridBeatmapPanel(featuredMap),
new GridBeatmapPanel(explicitFeaturedMap),
new ListBeatmapPanel(normal), new ListBeatmapPanel(normal),
new ListBeatmapPanel(undownloadable), new ListBeatmapPanel(undownloadable),
new ListBeatmapPanel(manyDifficulties), new ListBeatmapPanel(manyDifficulties),
new ListBeatmapPanel(explicitMap) new ListBeatmapPanel(explicitMap),
new ListBeatmapPanel(featuredMap),
new ListBeatmapPanel(explicitFeaturedMap)
}, },
}, },
}; };
BeatmapSetInfo getBeatmapSet() => CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet;
} }
} }
} }

View File

@ -32,6 +32,12 @@ namespace osu.Game.Tests.Visual.Playlists
private RoomsContainer roomsContainer => loungeScreen.ChildrenOfType<RoomsContainer>().First(); private RoomsContainer roomsContainer => loungeScreen.ChildrenOfType<RoomsContainer>().First();
[Test]
public void TestManyRooms()
{
AddStep("add rooms", () => RoomManager.AddRooms(500));
}
[Test] [Test]
public void TestScrollByDraggingRooms() public void TestScrollByDraggingRooms()
{ {

View File

@ -160,11 +160,14 @@ namespace osu.Game.Tests.Visual.Playlists
Ruleset = { Value = new OsuRuleset().RulesetInfo } Ruleset = { Value = new OsuRuleset().RulesetInfo }
})); }));
}); });
AddUntilStep("wait for load", () => resultsScreen.ChildrenOfType<ScorePanelList>().FirstOrDefault()?.AllPanelsVisible == true);
} }
private void waitForDisplay() private void waitForDisplay()
{ {
AddUntilStep("wait for request to complete", () => requestComplete); AddUntilStep("wait for request to complete", () => requestComplete);
AddUntilStep("wait for panels to be visible", () => resultsScreen.ChildrenOfType<ScorePanelList>().FirstOrDefault()?.AllPanelsVisible == true);
AddWaitStep("wait for display", 5); AddWaitStep("wait for display", 5);
} }

View File

@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual.Ranking
TestResultsScreen screen = null; TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
AddUntilStep("wait for loaded", () => screen.IsLoaded); AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
AddStep("click expanded panel", () => AddStep("click expanded panel", () =>
{ {
@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.Ranking
TestResultsScreen screen = null; TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
AddUntilStep("wait for loaded", () => screen.IsLoaded); AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
AddStep("click expanded panel", () => AddStep("click expanded panel", () =>
{ {
@ -177,7 +177,7 @@ namespace osu.Game.Tests.Visual.Ranking
TestResultsScreen screen = null; TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
AddUntilStep("wait for loaded", () => screen.IsLoaded); AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
ScorePanel expandedPanel = null; ScorePanel expandedPanel = null;
ScorePanel contractedPanel = null; ScorePanel contractedPanel = null;
@ -223,6 +223,7 @@ namespace osu.Game.Tests.Visual.Ranking
TestResultsScreen screen = null; TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
AddAssert("download button is disabled", () => !screen.ChildrenOfType<DownloadButton>().Last().Enabled.Value); AddAssert("download button is disabled", () => !screen.ChildrenOfType<DownloadButton>().Last().Enabled.Value);

View File

@ -159,6 +159,9 @@ namespace osu.Game.Tests.Visual.Ranking
var firstScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); var firstScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
var secondScore = new TestScoreInfo(new OsuRuleset().RulesetInfo); var secondScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
firstScore.User.Username = "A";
secondScore.User.Username = "B";
createListStep(() => new ScorePanelList()); createListStep(() => new ScorePanelList());
AddStep("add scores and select first", () => AddStep("add scores and select first", () =>
@ -168,6 +171,8 @@ namespace osu.Game.Tests.Visual.Ranking
list.SelectedScore.Value = firstScore; list.SelectedScore.Value = firstScore;
}); });
AddUntilStep("wait for load", () => list.AllPanelsVisible);
assertScoreState(firstScore, true); assertScoreState(firstScore, true);
assertScoreState(secondScore, false); assertScoreState(secondScore, false);
@ -182,6 +187,87 @@ namespace osu.Game.Tests.Visual.Ranking
assertExpandedPanelCentred(); assertExpandedPanelCentred();
} }
[Test]
public void TestAddScoreImmediately()
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
createListStep(() =>
{
var newList = new ScorePanelList { SelectedScore = { Value = score } };
newList.AddScore(score);
return newList;
});
assertScoreState(score, true);
assertExpandedPanelCentred();
}
[Test]
public void TestKeyboardNavigation()
{
var lowestScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { MaxCombo = 100 };
var middleScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { MaxCombo = 200 };
var highestScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { MaxCombo = 300 };
createListStep(() => new ScorePanelList());
AddStep("add scores and select middle", () =>
{
// order of addition purposefully scrambled.
list.AddScore(middleScore);
list.AddScore(lowestScore);
list.AddScore(highestScore);
list.SelectedScore.Value = middleScore;
});
assertScoreState(highestScore, false);
assertScoreState(middleScore, true);
assertScoreState(lowestScore, false);
AddStep("press left", () => InputManager.Key(Key.Left));
assertScoreState(highestScore, true);
assertScoreState(middleScore, false);
assertScoreState(lowestScore, false);
assertExpandedPanelCentred();
AddStep("press left at start of list", () => InputManager.Key(Key.Left));
assertScoreState(highestScore, true);
assertScoreState(middleScore, false);
assertScoreState(lowestScore, false);
assertExpandedPanelCentred();
AddStep("press right", () => InputManager.Key(Key.Right));
assertScoreState(highestScore, false);
assertScoreState(middleScore, true);
assertScoreState(lowestScore, false);
assertExpandedPanelCentred();
AddStep("press right again", () => InputManager.Key(Key.Right));
assertScoreState(highestScore, false);
assertScoreState(middleScore, false);
assertScoreState(lowestScore, true);
assertExpandedPanelCentred();
AddStep("press right at end of list", () => InputManager.Key(Key.Right));
assertScoreState(highestScore, false);
assertScoreState(middleScore, false);
assertScoreState(lowestScore, true);
assertExpandedPanelCentred();
AddStep("press left", () => InputManager.Key(Key.Left));
assertScoreState(highestScore, false);
assertScoreState(middleScore, true);
assertScoreState(lowestScore, false);
assertExpandedPanelCentred();
}
private void createListStep(Func<ScorePanelList> creationFunc) private void createListStep(Func<ScorePanelList> creationFunc)
{ {
AddStep("create list", () => Child = list = creationFunc().With(d => AddStep("create list", () => Child = list = creationFunc().With(d =>

View File

@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select EZ mod", () => AddStep("select EZ mod", () =>
{ {
var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance();
SelectedMods.Value = new[] { ruleset.GetAllMods().OfType<ModEasy>().Single() }; SelectedMods.Value = new[] { ruleset.CreateMod<ModEasy>() };
}); });
AddAssert("circle size bar is blue", () => barIsBlue(advancedStats.FirstValue)); AddAssert("circle size bar is blue", () => barIsBlue(advancedStats.FirstValue));
@ -106,7 +106,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select HR mod", () => AddStep("select HR mod", () =>
{ {
var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance();
SelectedMods.Value = new[] { ruleset.GetAllMods().OfType<ModHardRock>().Single() }; SelectedMods.Value = new[] { ruleset.CreateMod<ModHardRock>() };
}); });
AddAssert("circle size bar is red", () => barIsRed(advancedStats.FirstValue)); AddAssert("circle size bar is red", () => barIsRed(advancedStats.FirstValue));
@ -123,7 +123,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select unchanged Difficulty Adjust mod", () => AddStep("select unchanged Difficulty Adjust mod", () =>
{ {
var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance();
var difficultyAdjustMod = ruleset.GetAllMods().OfType<ModDifficultyAdjust>().Single(); var difficultyAdjustMod = ruleset.CreateMod<ModDifficultyAdjust>();
difficultyAdjustMod.ReadFromDifficulty(advancedStats.Beatmap.BaseDifficulty); difficultyAdjustMod.ReadFromDifficulty(advancedStats.Beatmap.BaseDifficulty);
SelectedMods.Value = new[] { difficultyAdjustMod }; SelectedMods.Value = new[] { difficultyAdjustMod };
}); });
@ -142,7 +142,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select changed Difficulty Adjust mod", () => AddStep("select changed Difficulty Adjust mod", () =>
{ {
var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance();
var difficultyAdjustMod = ruleset.GetAllMods().OfType<OsuModDifficultyAdjust>().Single(); var difficultyAdjustMod = ruleset.CreateMod<OsuModDifficultyAdjust>();
var originalDifficulty = advancedStats.Beatmap.BaseDifficulty; var originalDifficulty = advancedStats.Beatmap.BaseDifficulty;
difficultyAdjustMod.ReadFromDifficulty(originalDifficulty); difficultyAdjustMod.ReadFromDifficulty(originalDifficulty);

View File

@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.SongSelect
dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory)); dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default)); dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory, Scheduler));
return dependencies; return dependencies;
} }

View File

@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
AddStep("setup display", () => AddStep("setup display", () =>
{ {
var randomMods = Ruleset.Value.CreateInstance().GetAllMods().OrderBy(_ => RNG.Next()).Take(5).ToList(); var randomMods = Ruleset.Value.CreateInstance().CreateAllMods().OrderBy(_ => RNG.Next()).Take(5).ToList();
OsuLogo logo = new OsuLogo { Scale = new Vector2(0.15f) }; OsuLogo logo = new OsuLogo { Scale = new Vector2(0.15f) };

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq; using System.Linq;
using Humanizer;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -73,7 +74,7 @@ namespace osu.Game.Tests.Visual.UserInterface
}; };
control.Query.BindValueChanged(q => query.Text = $"Query: {q.NewValue}", true); control.Query.BindValueChanged(q => query.Text = $"Query: {q.NewValue}", true);
control.General.BindCollectionChanged((u, v) => general.Text = $"General: {(control.General.Any() ? string.Join('.', control.General.Select(i => i.ToString().ToLowerInvariant())) : "")}", true); control.General.BindCollectionChanged((u, v) => general.Text = $"General: {(control.General.Any() ? string.Join('.', control.General.Select(i => i.ToString().Underscore())) : "")}", true);
control.Ruleset.BindValueChanged(r => ruleset.Text = $"Ruleset: {r.NewValue}", true); control.Ruleset.BindValueChanged(r => ruleset.Text = $"Ruleset: {r.NewValue}", true);
control.Category.BindValueChanged(c => category.Text = $"Category: {c.NewValue}", true); control.Category.BindValueChanged(c => category.Text = $"Category: {c.NewValue}", true);
control.Genre.BindValueChanged(g => genre.Text = $"Genre: {g.NewValue}", true); control.Genre.BindValueChanged(g => genre.Text = $"Genre: {g.NewValue}", true);

View File

@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private BeatmapManager beatmapManager; private BeatmapManager beatmapManager;
private ScoreManager scoreManager; private ScoreManager scoreManager;
private readonly List<ScoreInfo> scores = new List<ScoreInfo>(); private readonly List<ScoreInfo> importedScores = new List<ScoreInfo>();
private BeatmapInfo beatmap; private BeatmapInfo beatmap;
[Cached] [Cached]
@ -82,7 +82,7 @@ namespace osu.Game.Tests.Visual.UserInterface
dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory)); dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default)); dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory, Scheduler));
beatmap = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).Result.Beatmaps[0]; beatmap = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).Result.Beatmaps[0];
@ -100,11 +100,9 @@ namespace osu.Game.Tests.Visual.UserInterface
User = new User { Username = "TestUser" }, User = new User { Username = "TestUser" },
}; };
scores.Add(scoreManager.Import(score).Result); importedScores.Add(scoreManager.Import(score).Result);
} }
scores.Sort(Comparer<ScoreInfo>.Create((s1, s2) => s2.TotalScore.CompareTo(s1.TotalScore)));
return dependencies; return dependencies;
} }
@ -134,9 +132,14 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test] [Test]
public void TestDeleteViaRightClick() public void TestDeleteViaRightClick()
{ {
ScoreInfo scoreBeingDeleted = null;
AddStep("open menu for top score", () => AddStep("open menu for top score", () =>
{ {
InputManager.MoveMouseTo(leaderboard.ChildrenOfType<LeaderboardScore>().First()); var leaderboardScore = leaderboard.ChildrenOfType<LeaderboardScore>().First();
scoreBeingDeleted = leaderboardScore.Score;
InputManager.MoveMouseTo(leaderboardScore);
InputManager.Click(MouseButton.Right); InputManager.Click(MouseButton.Right);
}); });
@ -158,14 +161,14 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scores[0].OnlineScoreID)); AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scoreBeingDeleted.OnlineScoreID));
} }
[Test] [Test]
public void TestDeleteViaDatabase() public void TestDeleteViaDatabase()
{ {
AddStep("delete top score", () => scoreManager.Delete(scores[0])); AddStep("delete top score", () => scoreManager.Delete(importedScores[0]));
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scores[0].OnlineScoreID)); AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != importedScores[0].OnlineScoreID));
} }
} }
} }

View File

@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Width = 200, Width = 200,
Current = Current =
{ {
Value = new OsuRuleset().GetAllMods().ToArray(), Value = new OsuRuleset().CreateAllMods().ToArray(),
} }
}; };
}); });

View File

@ -1,7 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
@ -17,5 +19,16 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("create mod icon", () => Child = icon = new ModIcon(new OsuModDoubleTime())); AddStep("create mod icon", () => Child = icon = new ModIcon(new OsuModDoubleTime()));
AddStep("change mod", () => icon.Mod = new OsuModEasy()); AddStep("change mod", () => icon.Mod = new OsuModEasy());
} }
[Test]
public void TestInterfaceModType()
{
ModIcon icon = null;
var ruleset = new OsuRuleset();
AddStep("create mod icon", () => Child = icon = new ModIcon(ruleset.AllMods.First(m => m.Acronym == "DT")));
AddStep("change mod", () => icon.Mod = ruleset.AllMods.First(m => m.Acronym == "EZ"));
}
} }
} }

View File

@ -158,8 +158,8 @@ namespace osu.Game.Tests.Visual.UserInterface
var mania = new ManiaRuleset(); var mania = new ManiaRuleset();
testModsWithSameBaseType( testModsWithSameBaseType(
mania.GetAllMods().Single(m => m.GetType() == typeof(ManiaModFadeIn)), mania.CreateMod<ManiaModFadeIn>(),
mania.GetAllMods().Single(m => m.GetType() == typeof(ManiaModHidden))); mania.CreateMod<ManiaModHidden>());
} }
[Test] [Test]

View File

@ -45,7 +45,7 @@ namespace osu.Game.Tournament.Tests.Components
private void success(APIBeatmap apiBeatmap) private void success(APIBeatmap apiBeatmap)
{ {
beatmap = apiBeatmap.ToBeatmap(rulesets); beatmap = apiBeatmap.ToBeatmap(rulesets);
var mods = rulesets.GetRuleset(Ladder.Ruleset.Value.ID ?? 0).CreateInstance().GetAllMods(); var mods = rulesets.GetRuleset(Ladder.Ruleset.Value.ID ?? 0).CreateInstance().AllMods;
foreach (var mod in mods) foreach (var mod in mods)
{ {

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -49,7 +48,7 @@ namespace osu.Game.Tournament.Components
} }
var ruleset = rulesets.GetRuleset(ladderInfo.Ruleset.Value?.ID ?? 0); var ruleset = rulesets.GetRuleset(ladderInfo.Ruleset.Value?.ID ?? 0);
var modIcon = ruleset?.CreateInstance().GetAllMods().FirstOrDefault(mod => mod.Acronym == modAcronym); var modIcon = ruleset?.CreateInstance().CreateModFromAcronym(modAcronym);
if (modIcon == null) if (modIcon == null)
return; return;

View File

@ -93,6 +93,12 @@ namespace osu.Game.Beatmaps
public bool WidescreenStoryboard { get; set; } public bool WidescreenStoryboard { get; set; }
public bool EpilepsyWarning { get; set; } public bool EpilepsyWarning { get; set; }
/// <summary>
/// Whether or not sound samples should change rate when playing with speed-changing mods.
/// TODO: only read/write supported for now, requires implementation in gameplay.
/// </summary>
public bool SamplesMatchPlaybackRate { get; set; }
public CountdownType Countdown { get; set; } = CountdownType.Normal; public CountdownType Countdown { get; set; } = CountdownType.Normal;
/// <summary> /// <summary>

View File

@ -129,6 +129,7 @@ namespace osu.Game.Beatmaps
Ruleset = ruleset, Ruleset = ruleset,
Metadata = metadata, Metadata = metadata,
WidescreenStoryboard = true, WidescreenStoryboard = true,
SamplesMatchPlaybackRate = true,
} }
} }
}; };

View File

@ -90,6 +90,12 @@ namespace osu.Game.Beatmaps
/// The song language of this beatmap set. /// The song language of this beatmap set.
/// </summary> /// </summary>
public BeatmapSetOnlineLanguage Language { get; set; } public BeatmapSetOnlineLanguage Language { get; set; }
/// <summary>
/// The track ID of this beatmap set.
/// Non-null only if the track is linked to a featured artist track entry.
/// </summary>
public int? TrackId { get; set; }
} }
public class BeatmapSetOnlineGenre public class BeatmapSetOnlineGenre

View File

@ -180,6 +180,10 @@ namespace osu.Game.Beatmaps.Formats
beatmap.BeatmapInfo.EpilepsyWarning = Parsing.ParseInt(pair.Value) == 1; beatmap.BeatmapInfo.EpilepsyWarning = Parsing.ParseInt(pair.Value) == 1;
break; break;
case @"SamplesMatchPlaybackRate":
beatmap.BeatmapInfo.SamplesMatchPlaybackRate = Parsing.ParseInt(pair.Value) == 1;
break;
case @"Countdown": case @"Countdown":
beatmap.BeatmapInfo.Countdown = (CountdownType)Enum.Parse(typeof(CountdownType), pair.Value); beatmap.BeatmapInfo.Countdown = (CountdownType)Enum.Parse(typeof(CountdownType), pair.Value);
break; break;

View File

@ -105,8 +105,8 @@ namespace osu.Game.Beatmaps.Formats
if (beatmap.BeatmapInfo.RulesetID == 3) if (beatmap.BeatmapInfo.RulesetID == 3)
writer.WriteLine(FormattableString.Invariant($"SpecialStyle: {(beatmap.BeatmapInfo.SpecialStyle ? '1' : '0')}")); writer.WriteLine(FormattableString.Invariant($"SpecialStyle: {(beatmap.BeatmapInfo.SpecialStyle ? '1' : '0')}"));
writer.WriteLine(FormattableString.Invariant($"WidescreenStoryboard: {(beatmap.BeatmapInfo.WidescreenStoryboard ? '1' : '0')}")); writer.WriteLine(FormattableString.Invariant($"WidescreenStoryboard: {(beatmap.BeatmapInfo.WidescreenStoryboard ? '1' : '0')}"));
// if (b.SamplesMatchPlaybackRate) if (beatmap.BeatmapInfo.SamplesMatchPlaybackRate)
// writer.WriteLine(@"SamplesMatchPlaybackRate: 1"); writer.WriteLine(@"SamplesMatchPlaybackRate: 1");
} }
private void handleEditor(TextWriter writer) private void handleEditor(TextWriter writer)

View File

@ -1,103 +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 System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Game.Rulesets;
namespace osu.Game.Configuration
{
public abstract class DatabasedConfigManager<TLookup> : ConfigManager<TLookup>
where TLookup : struct, Enum
{
private readonly SettingsStore settings;
private readonly int? variant;
private List<DatabasedSetting> databasedSettings;
private readonly RulesetInfo ruleset;
private bool legacySettingsExist;
protected DatabasedConfigManager(SettingsStore settings, RulesetInfo ruleset = null, int? variant = null)
{
this.settings = settings;
this.ruleset = ruleset;
this.variant = variant;
Load();
InitialiseDefaults();
}
protected override void PerformLoad()
{
databasedSettings = settings.Query(ruleset?.ID, variant);
legacySettingsExist = databasedSettings.Any(s => int.TryParse(s.Key, out _));
}
protected override bool PerformSave()
{
lock (dirtySettings)
{
foreach (var setting in dirtySettings)
settings.Update(setting);
dirtySettings.Clear();
}
return true;
}
private readonly List<DatabasedSetting> dirtySettings = new List<DatabasedSetting>();
protected override void AddBindable<TBindable>(TLookup lookup, Bindable<TBindable> bindable)
{
base.AddBindable(lookup, bindable);
if (legacySettingsExist)
{
var legacySetting = databasedSettings.Find(s => s.Key == ((int)(object)lookup).ToString());
if (legacySetting != null)
{
bindable.Parse(legacySetting.Value);
settings.Delete(legacySetting);
}
}
var setting = databasedSettings.Find(s => s.Key == lookup.ToString());
if (setting != null)
{
bindable.Parse(setting.Value);
}
else
{
settings.Update(setting = new DatabasedSetting
{
Key = lookup.ToString(),
Value = bindable.Value,
RulesetID = ruleset?.ID,
Variant = variant,
});
databasedSettings.Add(setting);
}
bindable.ValueChanged += b =>
{
setting.Value = b.NewValue;
lock (dirtySettings)
{
if (!dirtySettings.Contains(setting))
dirtySettings.Add(setting);
}
};
}
}
}

View File

@ -7,7 +7,7 @@ using osu.Game.Database;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
[Table("Settings")] [Table("Settings")]
public class DatabasedSetting : IHasPrimaryKey public class DatabasedSetting : IHasPrimaryKey // can be removed 20220315.
{ {
public int ID { get; set; } public int ID { get; set; }

View File

@ -0,0 +1,32 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Database;
using Realms;
#nullable enable
namespace osu.Game.Configuration
{
[MapTo(@"RulesetSetting")]
public class RealmRulesetSetting : RealmObject, IHasGuidPrimaryKey
{
[PrimaryKey]
public Guid ID { get; set; } = Guid.NewGuid();
[Indexed]
public int RulesetID { get; set; }
[Indexed]
public int Variant { get; set; }
[Required]
public string Key { get; set; } = string.Empty;
[Required]
public string Value { get; set; } = string.Empty;
public override string ToString() => $"{Key} => {Value}";
}
}

View File

@ -4,6 +4,7 @@
#nullable enable #nullable enable
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
@ -167,9 +168,21 @@ namespace osu.Game.Configuration
} }
} }
private static readonly ConcurrentDictionary<Type, (SettingSourceAttribute, PropertyInfo)[]> property_info_cache = new ConcurrentDictionary<Type, (SettingSourceAttribute, PropertyInfo)[]>();
public static IEnumerable<(SettingSourceAttribute, PropertyInfo)> GetSettingsSourceProperties(this object obj) public static IEnumerable<(SettingSourceAttribute, PropertyInfo)> GetSettingsSourceProperties(this object obj)
{ {
foreach (var property in obj.GetType().GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance)) var type = obj.GetType();
if (!property_info_cache.TryGetValue(type, out var properties))
property_info_cache[type] = properties = getSettingsSourceProperties(type).ToArray();
return properties;
}
private static IEnumerable<(SettingSourceAttribute, PropertyInfo)> getSettingsSourceProperties(Type type)
{
foreach (var property in type.GetProperties(BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Instance))
{ {
var attr = property.GetCustomAttribute<SettingSourceAttribute>(true); var attr = property.GetCustomAttribute<SettingSourceAttribute>(true);

View File

@ -1,46 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Database; using osu.Game.Database;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public class SettingsStore : DatabaseBackedStore public class SettingsStore
{ {
public event Action SettingChanged; // this class mostly exists as a wrapper to avoid breaking the ruleset API (see usage in RulesetConfigManager).
// it may cease to exist going forward, depending on how the structure of the config data layer changes.
public SettingsStore(DatabaseContextFactory contextFactory) public readonly RealmContextFactory Realm;
: base(contextFactory)
public SettingsStore(RealmContextFactory realmFactory)
{ {
} Realm = realmFactory;
/// <summary>
/// Retrieve <see cref="DatabasedSetting"/>s for a specified ruleset/variant content.
/// </summary>
/// <param name="rulesetId">The ruleset's internal ID.</param>
/// <param name="variant">An optional variant.</param>
public List<DatabasedSetting> Query(int? rulesetId = null, int? variant = null) =>
ContextFactory.Get().DatabasedSetting.Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList();
public void Update(DatabasedSetting setting)
{
using (ContextFactory.GetForWrite())
{
var newValue = setting.Value;
Refresh(ref setting);
setting.Value = newValue;
}
SettingChanged?.Invoke();
}
public void Delete(DatabasedSetting setting)
{
using (var usage = ContextFactory.GetForWrite())
usage.Context.Remove(setting);
} }
} }
} }

View File

@ -12,7 +12,6 @@ using osu.Game.Configuration;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Scoring; using osu.Game.Scoring;
using DatabasedKeyBinding = osu.Game.Input.Bindings.DatabasedKeyBinding;
using LogLevel = Microsoft.Extensions.Logging.LogLevel; using LogLevel = Microsoft.Extensions.Logging.LogLevel;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -24,14 +23,13 @@ namespace osu.Game.Database
public DbSet<BeatmapDifficulty> BeatmapDifficulty { get; set; } public DbSet<BeatmapDifficulty> BeatmapDifficulty { get; set; }
public DbSet<BeatmapMetadata> BeatmapMetadata { get; set; } public DbSet<BeatmapMetadata> BeatmapMetadata { get; set; }
public DbSet<BeatmapSetInfo> BeatmapSetInfo { get; set; } public DbSet<BeatmapSetInfo> BeatmapSetInfo { get; set; }
public DbSet<DatabasedSetting> DatabasedSetting { get; set; }
public DbSet<FileInfo> FileInfo { get; set; } public DbSet<FileInfo> FileInfo { get; set; }
public DbSet<RulesetInfo> RulesetInfo { get; set; } public DbSet<RulesetInfo> RulesetInfo { get; set; }
public DbSet<SkinInfo> SkinInfo { get; set; } public DbSet<SkinInfo> SkinInfo { get; set; }
public DbSet<ScoreInfo> ScoreInfo { get; set; } public DbSet<ScoreInfo> ScoreInfo { get; set; }
// migrated to realm // migrated to realm
public DbSet<DatabasedKeyBinding> DatabasedKeyBinding { get; set; } public DbSet<DatabasedSetting> DatabasedSetting { get; set; }
private readonly string connectionString; private readonly string connectionString;
@ -138,11 +136,6 @@ namespace osu.Game.Database
modelBuilder.Entity<SkinInfo>().HasIndex(b => b.Hash).IsUnique(); modelBuilder.Entity<SkinInfo>().HasIndex(b => b.Hash).IsUnique();
modelBuilder.Entity<SkinInfo>().HasIndex(b => b.DeletePending); modelBuilder.Entity<SkinInfo>().HasIndex(b => b.DeletePending);
modelBuilder.Entity<DatabasedKeyBinding>().HasIndex(b => new { b.RulesetID, b.Variant });
modelBuilder.Entity<DatabasedKeyBinding>().HasIndex(b => b.IntAction);
modelBuilder.Entity<DatabasedKeyBinding>().Ignore(b => b.KeyCombination);
modelBuilder.Entity<DatabasedKeyBinding>().Ignore(b => b.Action);
modelBuilder.Entity<DatabasedSetting>().HasIndex(b => new { b.RulesetID, b.Variant }); modelBuilder.Entity<DatabasedSetting>().HasIndex(b => new { b.RulesetID, b.Variant });
modelBuilder.Entity<FileInfo>().HasIndex(b => b.Hash).IsUnique(); modelBuilder.Entity<FileInfo>().HasIndex(b => b.Hash).IsUnique();

View File

@ -38,6 +38,33 @@ namespace osu.Game.Extensions
return repeatDelegate; return repeatDelegate;
} }
/// <summary>
/// Shakes this drawable.
/// </summary>
/// <param name="target">The target to shake.</param>
/// <param name="shakeDuration">The length of a single shake.</param>
/// <param name="shakeMagnitude">Pixels of displacement per shake.</param>
/// <param name="maximumLength">The maximum length the shake should last.</param>
public static void Shake(this Drawable target, double shakeDuration = 80, float shakeMagnitude = 8, double? maximumLength = null)
{
// if we don't have enough time, don't bother shaking.
if (maximumLength < shakeDuration * 2)
return;
var sequence = target.MoveToX(shakeMagnitude, shakeDuration / 2, Easing.OutSine).Then()
.MoveToX(-shakeMagnitude, shakeDuration, Easing.InOutSine).Then();
// if we don't have enough time for the second shake, skip it.
if (!maximumLength.HasValue || maximumLength >= shakeDuration * 4)
{
sequence = sequence
.MoveToX(shakeMagnitude, shakeDuration, Easing.InOutSine).Then()
.MoveToX(-shakeMagnitude, shakeDuration, Easing.InOutSine).Then();
}
sequence.MoveToX(0, shakeDuration / 2, Easing.InSine);
}
/// <summary> /// <summary>
/// Accepts a delta vector in screen-space coordinates and converts it to one which can be applied to this drawable's position. /// Accepts a delta vector in screen-space coordinates and converts it to one which can be applied to this drawable's position.
/// </summary> /// </summary>

View File

@ -87,23 +87,25 @@ namespace osu.Game.Graphics.Containers
private void createLink(IEnumerable<Drawable> drawables, LinkDetails link, string tooltipText, Action action = null) private void createLink(IEnumerable<Drawable> drawables, LinkDetails link, string tooltipText, Action action = null)
{ {
AddInternal(new DrawableLinkCompiler(drawables.OfType<SpriteText>().ToList()) var linkCompiler = CreateLinkCompiler(drawables.OfType<SpriteText>());
linkCompiler.RelativeSizeAxes = Axes.Both;
linkCompiler.TooltipText = tooltipText;
linkCompiler.Action = () =>
{ {
RelativeSizeAxes = Axes.Both, if (action != null)
TooltipText = tooltipText, action();
Action = () => else if (game != null)
{ game.HandleLink(link);
if (action != null) // fallback to handle cases where OsuGame is not available, ie. tournament client.
action(); else if (link.Action == LinkAction.External)
else if (game != null) host.OpenUrlExternally(link.Argument);
game.HandleLink(link); };
// fallback to handle cases where OsuGame is not available, ie. tournament client.
else if (link.Action == LinkAction.External) AddInternal(linkCompiler);
host.OpenUrlExternally(link.Argument);
},
});
} }
protected virtual DrawableLinkCompiler CreateLinkCompiler(IEnumerable<SpriteText> parts) => new DrawableLinkCompiler(parts);
// We want the compilers to always be visible no matter where they are, so RelativeSizeAxes is used. // We want the compilers to always be visible no matter where they are, so RelativeSizeAxes is used.
// However due to https://github.com/ppy/osu-framework/issues/2073, it's possible for the compilers to be relative size in the flow's auto-size axes - an unsupported operation. // However due to https://github.com/ppy/osu-framework/issues/2073, it's possible for the compilers to be relative size in the flow's auto-size axes - an unsupported operation.
// Since the compilers don't display any content and don't affect the layout, it's simplest to exclude them from the flow. // Since the compilers don't display any content and don't affect the layout, it's simplest to exclude them from the flow.

View File

@ -1,8 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Extensions;
namespace osu.Game.Graphics.Containers namespace osu.Game.Graphics.Containers
{ {
@ -16,40 +16,10 @@ namespace osu.Game.Graphics.Containers
/// </summary> /// </summary>
public float ShakeDuration = 80; public float ShakeDuration = 80;
/// <summary>
/// Total number of shakes. May be shortened if possible.
/// </summary>
public float TotalShakes = 4;
/// <summary>
/// Pixels of displacement per shake.
/// </summary>
public float ShakeMagnitude = 8;
/// <summary> /// <summary>
/// Shake the contents of this container. /// Shake the contents of this container.
/// </summary> /// </summary>
/// <param name="maximumLength">The maximum length the shake should last.</param> /// <param name="maximumLength">The maximum length the shake should last.</param>
public void Shake(double? maximumLength = null) public void Shake(double? maximumLength = null) => this.Shake(ShakeDuration, maximumLength: maximumLength);
{
const float shake_amount = 8;
// if we don't have enough time, don't bother shaking.
if (maximumLength < ShakeDuration * 2)
return;
var sequence = this.MoveToX(shake_amount, ShakeDuration / 2, Easing.OutSine).Then()
.MoveToX(-shake_amount, ShakeDuration, Easing.InOutSine).Then();
// if we don't have enough time for the second shake, skip it.
if (!maximumLength.HasValue || maximumLength >= ShakeDuration * 4)
{
sequence = sequence
.MoveToX(shake_amount, ShakeDuration, Easing.InOutSine).Then()
.MoveToX(-shake_amount, ShakeDuration, Easing.InOutSine).Then();
}
sequence.MoveToX(0, ShakeDuration / 2, Easing.InSine);
}
} }
} }

View File

@ -70,7 +70,7 @@ namespace osu.Game.Graphics.UserInterface
return base.OnKeyDown(e); return base.OnKeyDown(e);
} }
public bool OnPressed(GlobalAction action) public virtual bool OnPressed(GlobalAction action)
{ {
if (!HasFocus) return false; if (!HasFocus) return false;

View File

@ -46,7 +46,11 @@ namespace osu.Game.Graphics.UserInterface
}, },
}; };
Current.ValueChanged += filled => fill.FadeTo(filled.NewValue ? 1 : 0, 200, Easing.OutQuint); Current.ValueChanged += filled =>
{
fill.FadeTo(filled.NewValue ? 1 : 0, 200, Easing.OutQuint);
this.TransformTo(nameof(BorderThickness), filled.NewValue ? 8.5f : border_width, 200, Easing.OutQuint);
};
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]

View File

@ -4,14 +4,17 @@
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Bindings;
using osu.Game.Input.Bindings;
using osu.Game.Overlays; using osu.Game.Overlays;
using osuTK; using osuTK;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
public class OsuPopover : Popover public class OsuPopover : Popover, IKeyBindingHandler<GlobalAction>
{ {
private const float fade_duration = 250; private const float fade_duration = 250;
private const double scale_duration = 500; private const double scale_duration = 500;
@ -51,5 +54,23 @@ namespace osu.Game.Graphics.UserInterfaceV2
this.ScaleTo(0.7f, scale_duration, Easing.OutQuint); this.ScaleTo(0.7f, scale_duration, Easing.OutQuint);
this.FadeOut(fade_duration, Easing.OutQuint); this.FadeOut(fade_duration, Easing.OutQuint);
} }
public bool OnPressed(GlobalAction action)
{
if (State.Value == Visibility.Hidden)
return false;
if (action == GlobalAction.Back)
{
Hide();
return true;
}
return false;
}
public void OnReleased(GlobalAction action)
{
}
} }
} }

View File

@ -1,39 +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 System.ComponentModel.DataAnnotations.Schema;
using osu.Framework.Input.Bindings;
using osu.Game.Database;
namespace osu.Game.Input.Bindings
{
[Table("KeyBinding")]
public class DatabasedKeyBinding : IKeyBinding, IHasPrimaryKey
{
public int ID { get; set; }
public int? RulesetID { get; set; }
public int? Variant { get; set; }
[Column("Keys")]
public string KeysString { get; set; }
[Column("Action")]
public int IntAction { get; set; }
[NotMapped]
public KeyCombination KeyCombination
{
get => KeysString;
set => KeysString = value.ToString();
}
[NotMapped]
public object Action
{
get => IntAction;
set => IntAction = (int)value;
}
}
}

View File

@ -0,0 +1,515 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using osu.Game.Database;
namespace osu.Game.Migrations
{
[DbContext(typeof(OsuDbContext))]
[Migration("20210912144011_AddSamplesMatchPlaybackRate")]
partial class AddSamplesMatchPlaybackRate
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.2.6-servicing-10079");
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapDifficulty", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<float>("ApproachRate");
b.Property<float>("CircleSize");
b.Property<float>("DrainRate");
b.Property<float>("OverallDifficulty");
b.Property<double>("SliderMultiplier");
b.Property<double>("SliderTickRate");
b.HasKey("ID");
b.ToTable("BeatmapDifficulty");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<double>("AudioLeadIn");
b.Property<double>("BPM");
b.Property<int>("BaseDifficultyID");
b.Property<int>("BeatDivisor");
b.Property<int>("BeatmapSetInfoID");
b.Property<int>("Countdown");
b.Property<int>("CountdownOffset");
b.Property<double>("DistanceSpacing");
b.Property<bool>("EpilepsyWarning");
b.Property<int>("GridSize");
b.Property<string>("Hash");
b.Property<bool>("Hidden");
b.Property<double>("Length");
b.Property<bool>("LetterboxInBreaks");
b.Property<string>("MD5Hash");
b.Property<int?>("MetadataID");
b.Property<int?>("OnlineBeatmapID");
b.Property<string>("Path");
b.Property<int>("RulesetID");
b.Property<bool>("SamplesMatchPlaybackRate");
b.Property<bool>("SpecialStyle");
b.Property<float>("StackLeniency");
b.Property<double>("StarDifficulty");
b.Property<int>("Status");
b.Property<string>("StoredBookmarks");
b.Property<double>("TimelineZoom");
b.Property<string>("Version");
b.Property<bool>("WidescreenStoryboard");
b.HasKey("ID");
b.HasIndex("BaseDifficultyID");
b.HasIndex("BeatmapSetInfoID");
b.HasIndex("Hash");
b.HasIndex("MD5Hash");
b.HasIndex("MetadataID");
b.HasIndex("OnlineBeatmapID")
.IsUnique();
b.HasIndex("RulesetID");
b.ToTable("BeatmapInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapMetadata", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Artist");
b.Property<string>("ArtistUnicode");
b.Property<string>("AudioFile");
b.Property<int>("AuthorID")
.HasColumnName("AuthorID");
b.Property<string>("AuthorString")
.HasColumnName("Author");
b.Property<string>("BackgroundFile");
b.Property<int>("PreviewTime");
b.Property<string>("Source");
b.Property<string>("Tags");
b.Property<string>("Title");
b.Property<string>("TitleUnicode");
b.HasKey("ID");
b.ToTable("BeatmapMetadata");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("BeatmapSetInfoID");
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.HasKey("ID");
b.HasIndex("BeatmapSetInfoID");
b.HasIndex("FileInfoID");
b.ToTable("BeatmapSetFileInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<DateTimeOffset>("DateAdded");
b.Property<bool>("DeletePending");
b.Property<string>("Hash");
b.Property<int?>("MetadataID");
b.Property<int?>("OnlineBeatmapSetID");
b.Property<bool>("Protected");
b.Property<int>("Status");
b.HasKey("ID");
b.HasIndex("DeletePending");
b.HasIndex("Hash")
.IsUnique();
b.HasIndex("MetadataID");
b.HasIndex("OnlineBeatmapSetID")
.IsUnique();
b.ToTable("BeatmapSetInfo");
});
modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Key")
.HasColumnName("Key");
b.Property<int?>("RulesetID");
b.Property<int?>("SkinInfoID");
b.Property<string>("StringValue")
.HasColumnName("Value");
b.Property<int?>("Variant");
b.HasKey("ID");
b.HasIndex("SkinInfoID");
b.HasIndex("RulesetID", "Variant");
b.ToTable("Settings");
});
modelBuilder.Entity("osu.Game.IO.FileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Hash");
b.Property<int>("ReferenceCount");
b.HasKey("ID");
b.HasIndex("Hash")
.IsUnique();
b.HasIndex("ReferenceCount");
b.ToTable("FileInfo");
});
modelBuilder.Entity("osu.Game.Input.Bindings.DatabasedKeyBinding", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("IntAction")
.HasColumnName("Action");
b.Property<string>("KeysString")
.HasColumnName("Keys");
b.Property<int?>("RulesetID");
b.Property<int?>("Variant");
b.HasKey("ID");
b.HasIndex("IntAction");
b.HasIndex("RulesetID", "Variant");
b.ToTable("KeyBinding");
});
modelBuilder.Entity("osu.Game.Rulesets.RulesetInfo", b =>
{
b.Property<int?>("ID")
.ValueGeneratedOnAdd();
b.Property<bool>("Available");
b.Property<string>("InstantiationInfo");
b.Property<string>("Name");
b.Property<string>("ShortName");
b.HasKey("ID");
b.HasIndex("Available");
b.HasIndex("ShortName")
.IsUnique();
b.ToTable("RulesetInfo");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.Property<int?>("ScoreInfoID");
b.HasKey("ID");
b.HasIndex("FileInfoID");
b.HasIndex("ScoreInfoID");
b.ToTable("ScoreFileInfo");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<double>("Accuracy")
.HasColumnType("DECIMAL(1,4)");
b.Property<int>("BeatmapInfoID");
b.Property<int>("Combo");
b.Property<DateTimeOffset>("Date");
b.Property<bool>("DeletePending");
b.Property<string>("Hash");
b.Property<int>("MaxCombo");
b.Property<string>("ModsJson")
.HasColumnName("Mods");
b.Property<long?>("OnlineScoreID");
b.Property<double?>("PP");
b.Property<int>("Rank");
b.Property<int>("RulesetID");
b.Property<string>("StatisticsJson")
.HasColumnName("Statistics");
b.Property<long>("TotalScore");
b.Property<int?>("UserID")
.HasColumnName("UserID");
b.Property<string>("UserString")
.HasColumnName("User");
b.HasKey("ID");
b.HasIndex("BeatmapInfoID");
b.HasIndex("OnlineScoreID")
.IsUnique();
b.HasIndex("RulesetID");
b.ToTable("ScoreInfo");
});
modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<int>("FileInfoID");
b.Property<string>("Filename")
.IsRequired();
b.Property<int>("SkinInfoID");
b.HasKey("ID");
b.HasIndex("FileInfoID");
b.HasIndex("SkinInfoID");
b.ToTable("SkinFileInfo");
});
modelBuilder.Entity("osu.Game.Skinning.SkinInfo", b =>
{
b.Property<int>("ID")
.ValueGeneratedOnAdd();
b.Property<string>("Creator");
b.Property<bool>("DeletePending");
b.Property<string>("Hash");
b.Property<string>("InstantiationInfo");
b.Property<string>("Name");
b.HasKey("ID");
b.HasIndex("DeletePending");
b.HasIndex("Hash")
.IsUnique();
b.ToTable("SkinInfo");
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapDifficulty", "BaseDifficulty")
.WithMany()
.HasForeignKey("BaseDifficultyID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo", "BeatmapSet")
.WithMany("Beatmaps")
.HasForeignKey("BeatmapSetInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
.WithMany("Beatmaps")
.HasForeignKey("MetadataID");
b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
.WithMany()
.HasForeignKey("RulesetID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetFileInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapSetInfo")
.WithMany("Files")
.HasForeignKey("BeatmapSetInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Beatmaps.BeatmapSetInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapMetadata", "Metadata")
.WithMany("BeatmapSets")
.HasForeignKey("MetadataID");
});
modelBuilder.Entity("osu.Game.Configuration.DatabasedSetting", b =>
{
b.HasOne("osu.Game.Skinning.SkinInfo")
.WithMany("Settings")
.HasForeignKey("SkinInfoID");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreFileInfo", b =>
{
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Scoring.ScoreInfo")
.WithMany("Files")
.HasForeignKey("ScoreInfoID");
});
modelBuilder.Entity("osu.Game.Scoring.ScoreInfo", b =>
{
b.HasOne("osu.Game.Beatmaps.BeatmapInfo", "Beatmap")
.WithMany("Scores")
.HasForeignKey("BeatmapInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Rulesets.RulesetInfo", "Ruleset")
.WithMany()
.HasForeignKey("RulesetID")
.OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity("osu.Game.Skinning.SkinFileInfo", b =>
{
b.HasOne("osu.Game.IO.FileInfo", "FileInfo")
.WithMany()
.HasForeignKey("FileInfoID")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("osu.Game.Skinning.SkinInfo")
.WithMany("Files")
.HasForeignKey("SkinInfoID")
.OnDelete(DeleteBehavior.Cascade);
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace osu.Game.Migrations
{
public partial class AddSamplesMatchPlaybackRate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<bool>(
name: "SamplesMatchPlaybackRate",
table: "BeatmapInfo",
nullable: false,
defaultValue: false);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SamplesMatchPlaybackRate",
table: "BeatmapInfo");
}
}
}

View File

@ -81,6 +81,8 @@ namespace osu.Game.Migrations
b.Property<int>("RulesetID"); b.Property<int>("RulesetID");
b.Property<bool>("SamplesMatchPlaybackRate");
b.Property<bool>("SpecialStyle"); b.Property<bool>("SpecialStyle");
b.Property<float>("StackLeniency"); b.Property<float>("StackLeniency");

View File

@ -16,7 +16,7 @@ using osu.Game.Utils;
namespace osu.Game.Online.API namespace osu.Game.Online.API
{ {
[MessagePackObject] [MessagePackObject]
public class APIMod : IMod, IEquatable<APIMod> public class APIMod : IEquatable<APIMod>
{ {
[JsonProperty("acronym")] [JsonProperty("acronym")]
[Key(0)] [Key(0)]
@ -48,31 +48,31 @@ namespace osu.Game.Online.API
public Mod ToMod(Ruleset ruleset) public Mod ToMod(Ruleset ruleset)
{ {
Mod resultMod = ruleset.GetAllMods().FirstOrDefault(m => m.Acronym == Acronym); Mod resultMod = ruleset.CreateModFromAcronym(Acronym);
if (resultMod == null) if (resultMod == null)
throw new InvalidOperationException($"There is no mod in the ruleset ({ruleset.ShortName}) matching the acronym {Acronym}."); throw new InvalidOperationException($"There is no mod in the ruleset ({ruleset.ShortName}) matching the acronym {Acronym}.");
foreach (var (_, property) in resultMod.GetSettingsSourceProperties()) if (Settings.Count > 0)
{ {
if (!Settings.TryGetValue(property.Name.Underscore(), out object settingValue)) foreach (var (_, property) in resultMod.GetSettingsSourceProperties())
continue; {
if (!Settings.TryGetValue(property.Name.Underscore(), out object settingValue))
continue;
resultMod.CopyAdjustedSetting((IBindable)property.GetValue(resultMod), settingValue); resultMod.CopyAdjustedSetting((IBindable)property.GetValue(resultMod), settingValue);
}
} }
return resultMod; return resultMod;
} }
public bool Equals(IMod other) => other is APIMod them && Equals(them);
public bool Equals(APIMod other) public bool Equals(APIMod other)
{ {
if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(this, other)) return true;
return Acronym == other.Acronym && return Acronym == other.Acronym && Settings.SequenceEqual(other.Settings, ModSettingsEqualityComparer.Default);
Settings.SequenceEqual(other.Settings, ModSettingsEqualityComparer.Default);
} }
public override string ToString() public override string ToString()

View File

@ -18,9 +18,9 @@ namespace osu.Game.Online.API.Requests
private readonly BeatmapInfo beatmap; private readonly BeatmapInfo beatmap;
private readonly BeatmapLeaderboardScope scope; private readonly BeatmapLeaderboardScope scope;
private readonly RulesetInfo ruleset; private readonly RulesetInfo ruleset;
private readonly IEnumerable<Mod> mods; private readonly IEnumerable<IMod> mods;
public GetScoresRequest(BeatmapInfo beatmap, RulesetInfo ruleset, BeatmapLeaderboardScope scope = BeatmapLeaderboardScope.Global, IEnumerable<Mod> mods = null) public GetScoresRequest(BeatmapInfo beatmap, RulesetInfo ruleset, BeatmapLeaderboardScope scope = BeatmapLeaderboardScope.Global, IEnumerable<IMod> mods = null)
{ {
if (!beatmap.OnlineBeatmapID.HasValue) if (!beatmap.OnlineBeatmapID.HasValue)
throw new InvalidOperationException($"Cannot lookup a beatmap's scores without having a populated {nameof(BeatmapInfo.OnlineBeatmapID)}."); throw new InvalidOperationException($"Cannot lookup a beatmap's scores without having a populated {nameof(BeatmapInfo.OnlineBeatmapID)}.");
@ -31,7 +31,7 @@ namespace osu.Game.Online.API.Requests
this.beatmap = beatmap; this.beatmap = beatmap;
this.scope = scope; this.scope = scope;
this.ruleset = ruleset ?? throw new ArgumentNullException(nameof(ruleset)); this.ruleset = ruleset ?? throw new ArgumentNullException(nameof(ruleset));
this.mods = mods ?? Array.Empty<Mod>(); this.mods = mods ?? Array.Empty<IMod>();
Success += onSuccess; Success += onSuccess;
} }

View File

@ -8,7 +8,7 @@ namespace osu.Game.Online.API.Requests
{ {
public class GetUserRequest : APIRequest<User> public class GetUserRequest : APIRequest<User>
{ {
private readonly string lookup; public readonly string Lookup;
public readonly RulesetInfo Ruleset; public readonly RulesetInfo Ruleset;
private readonly LookupType lookupType; private readonly LookupType lookupType;
@ -26,7 +26,7 @@ namespace osu.Game.Online.API.Requests
/// <param name="ruleset">The ruleset to get the user's info for.</param> /// <param name="ruleset">The ruleset to get the user's info for.</param>
public GetUserRequest(long? userId = null, RulesetInfo ruleset = null) public GetUserRequest(long? userId = null, RulesetInfo ruleset = null)
{ {
lookup = userId.ToString(); Lookup = userId.ToString();
lookupType = LookupType.Id; lookupType = LookupType.Id;
Ruleset = ruleset; Ruleset = ruleset;
} }
@ -38,12 +38,12 @@ namespace osu.Game.Online.API.Requests
/// <param name="ruleset">The ruleset to get the user's info for.</param> /// <param name="ruleset">The ruleset to get the user's info for.</param>
public GetUserRequest(string username = null, RulesetInfo ruleset = null) public GetUserRequest(string username = null, RulesetInfo ruleset = null)
{ {
lookup = username; Lookup = username;
lookupType = LookupType.Username; lookupType = LookupType.Username;
Ruleset = ruleset; Ruleset = ruleset;
} }
protected override string Target => lookup != null ? $@"users/{lookup}/{Ruleset?.ShortName}?k={lookupType.ToString().ToLower()}" : $@"me/{Ruleset?.ShortName}"; protected override string Target => Lookup != null ? $@"users/{Lookup}/{Ruleset?.ShortName}?key={lookupType.ToString().ToLower()}" : $@"me/{Ruleset?.ShortName}";
private enum LookupType private enum LookupType
{ {

View File

@ -63,6 +63,9 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"ratings")] [JsonProperty(@"ratings")]
private int[] ratings { get; set; } private int[] ratings { get; set; }
[JsonProperty(@"track_id")]
private int? trackId { get; set; }
[JsonProperty(@"user_id")] [JsonProperty(@"user_id")]
private int creatorId private int creatorId
{ {
@ -106,7 +109,8 @@ namespace osu.Game.Online.API.Requests.Responses
Availability = availability, Availability = availability,
HasFavourited = hasFavourited, HasFavourited = hasFavourited,
Genre = genre, Genre = genre,
Language = language Language = language,
TrackId = trackId
}, },
}; };

View File

@ -23,10 +23,10 @@ namespace osu.Game.Online.API.Requests.Responses
var rulesetInstance = ruleset.CreateInstance(); var rulesetInstance = ruleset.CreateInstance();
var mods = Mods != null ? rulesetInstance.GetAllMods().Where(mod => Mods.Contains(mod.Acronym)).ToArray() : Array.Empty<Mod>(); var mods = Mods != null ? Mods.Select(acronym => rulesetInstance.CreateModFromAcronym(acronym)).Where(m => m != null).ToArray() : Array.Empty<Mod>();
// all API scores provided by this class are considered to be legacy. // all API scores provided by this class are considered to be legacy.
mods = mods.Append(rulesetInstance.GetAllMods().OfType<ModClassic>().Single()).ToArray(); mods = mods.Append(rulesetInstance.CreateMod<ModClassic>()).ToArray();
var scoreInfo = new ScoreInfo var scoreInfo = new ScoreInfo
{ {

View File

@ -3,6 +3,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Humanizer;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.IO.Network; using osu.Framework.IO.Network;
using osu.Game.Extensions; using osu.Game.Extensions;
@ -83,7 +84,7 @@ namespace osu.Game.Online.API.Requests
req.AddParameter("q", query); req.AddParameter("q", query);
if (General != null && General.Any()) if (General != null && General.Any())
req.AddParameter("c", string.Join('.', General.Select(e => e.ToString().ToLowerInvariant()))); req.AddParameter("c", string.Join('.', General.Select(e => e.ToString().Underscore())));
if (ruleset.ID.HasValue) if (ruleset.ID.HasValue)
req.AddParameter("m", ruleset.ID.Value.ToString()); req.AddParameter("m", ruleset.ID.Value.ToString());

View File

@ -256,8 +256,36 @@ namespace osu.Game.Online.Chat
JoinChannel(channel); JoinChannel(channel);
break; break;
case "chat":
case "msg":
case "query":
if (string.IsNullOrWhiteSpace(content))
{
target.AddNewMessages(new ErrorMessage($"Usage: /{command} [user]"));
break;
}
// Check if the user has joined the requested channel already.
// This uses the channel name for comparison as the PM user's username is unavailable after a restart.
var privateChannel = JoinedChannels.FirstOrDefault(
c => c.Type == ChannelType.PM && c.Users.Count == 1 && c.Name.Equals(content, StringComparison.OrdinalIgnoreCase));
if (privateChannel != null)
{
CurrentChannel.Value = privateChannel;
break;
}
var request = new GetUserRequest(content);
request.Success += OpenPrivateChannel;
request.Failure += e => target.AddNewMessages(
new ErrorMessage(e.InnerException?.Message == @"NotFound" ? $"User '{content}' was not found." : $"Could not fetch user '{content}'."));
api.Queue(request);
break;
case "help": case "help":
target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action], /join [channel], /np")); target.AddNewMessages(new InfoMessage("Supported commands: /help, /me [action], /join [channel], /chat [user], /np"));
break; break;
default: default:

View File

@ -34,6 +34,8 @@ namespace osu.Game.Online.Leaderboards
{ {
public const float HEIGHT = 60; public const float HEIGHT = 60;
public readonly ScoreInfo Score;
private const float corner_radius = 5; private const float corner_radius = 5;
private const float edge_margin = 5; private const float edge_margin = 5;
private const float background_alpha = 0.25f; private const float background_alpha = 0.25f;
@ -41,7 +43,6 @@ namespace osu.Game.Online.Leaderboards
protected Container RankContainer { get; private set; } protected Container RankContainer { get; private set; }
private readonly ScoreInfo score;
private readonly int? rank; private readonly int? rank;
private readonly bool allowHighlight; private readonly bool allowHighlight;
@ -67,7 +68,8 @@ namespace osu.Game.Online.Leaderboards
public LeaderboardScore(ScoreInfo score, int? rank, bool allowHighlight = true) public LeaderboardScore(ScoreInfo score, int? rank, bool allowHighlight = true)
{ {
this.score = score; Score = score;
this.rank = rank; this.rank = rank;
this.allowHighlight = allowHighlight; this.allowHighlight = allowHighlight;
@ -78,9 +80,9 @@ namespace osu.Game.Online.Leaderboards
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IAPIProvider api, OsuColour colour, ScoreManager scoreManager) private void load(IAPIProvider api, OsuColour colour, ScoreManager scoreManager)
{ {
var user = score.User; var user = Score.User;
statisticsLabels = GetStatistics(score).Select(s => new ScoreComponentLabel(s)).ToList(); statisticsLabels = GetStatistics(Score).Select(s => new ScoreComponentLabel(s)).ToList();
ClickableAvatar innerAvatar; ClickableAvatar innerAvatar;
@ -198,7 +200,7 @@ namespace osu.Game.Online.Leaderboards
{ {
TextColour = Color4.White, TextColour = Color4.White,
GlowColour = Color4Extensions.FromHex(@"83ccfa"), GlowColour = Color4Extensions.FromHex(@"83ccfa"),
Current = scoreManager.GetBindableTotalScoreString(score), Current = scoreManager.GetBindableTotalScoreString(Score),
Font = OsuFont.Numeric.With(size: 23), Font = OsuFont.Numeric.With(size: 23),
}, },
RankContainer = new Container RankContainer = new Container
@ -206,7 +208,7 @@ namespace osu.Game.Online.Leaderboards
Size = new Vector2(40f, 20f), Size = new Vector2(40f, 20f),
Children = new[] Children = new[]
{ {
scoreRank = new UpdateableRank(score.Rank) scoreRank = new UpdateableRank(Score.Rank)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -223,7 +225,7 @@ namespace osu.Game.Online.Leaderboards
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Spacing = new Vector2(1), Spacing = new Vector2(1),
ChildrenEnumerable = score.Mods.Select(mod => new ModIcon(mod) { Scale = new Vector2(0.375f) }) ChildrenEnumerable = Score.Mods.Select(mod => new ModIcon(mod) { Scale = new Vector2(0.375f) })
}, },
}, },
}, },
@ -389,14 +391,14 @@ namespace osu.Game.Online.Leaderboards
{ {
List<MenuItem> items = new List<MenuItem>(); List<MenuItem> items = new List<MenuItem>();
if (score.Mods.Length > 0 && modsContainer.Any(s => s.IsHovered) && songSelect != null) if (Score.Mods.Length > 0 && modsContainer.Any(s => s.IsHovered) && songSelect != null)
items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = score.Mods)); items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods));
if (score.Files?.Count > 0) if (Score.Files?.Count > 0)
items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => scoreManager.Export(score))); items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => scoreManager.Export(Score)));
if (score.ID != 0) if (Score.ID != 0)
items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score)))); items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(Score))));
return items.ToArray(); return items.ToArray();
} }

View File

@ -179,11 +179,7 @@ namespace osu.Game.Online.Rooms
if (EndDate.Value != null && DateTimeOffset.Now >= EndDate.Value) if (EndDate.Value != null && DateTimeOffset.Now >= EndDate.Value)
Status.Value = new RoomStatusEnded(); Status.Value = new RoomStatusEnded();
// Todo: This is not the best way/place to do this, but the intention is to display all playlist items when the room has ended, other.RemoveExpiredPlaylistItems();
// and display only the non-expired playlist items while the room is still active. In order to achieve this, all expired items are removed from the source Room.
// More refactoring is required before this can be done locally instead - DrawableRoomPlaylist is currently directly bound to the playlist to display items in the room.
if (!(Status.Value is RoomStatusEnded))
other.Playlist.RemoveAll(i => i.Expired);
if (!Playlist.SequenceEqual(other.Playlist)) if (!Playlist.SequenceEqual(other.Playlist))
{ {
@ -200,6 +196,15 @@ namespace osu.Game.Online.Rooms
Position.Value = other.Position.Value; Position.Value = other.Position.Value;
} }
public void RemoveExpiredPlaylistItems()
{
// Todo: This is not the best way/place to do this, but the intention is to display all playlist items when the room has ended,
// and display only the non-expired playlist items while the room is still active. In order to achieve this, all expired items are removed from the source Room.
// More refactoring is required before this can be done locally instead - DrawableRoomPlaylist is currently directly bound to the playlist to display items in the room.
if (!(Status.Value is RoomStatusEnded))
Playlist.RemoveAll(i => i.Expired);
}
public bool ShouldSerializeRoomID() => false; public bool ShouldSerializeRoomID() => false;
public bool ShouldSerializeHost() => false; public bool ShouldSerializeHost() => false;
public bool ShouldSerializeEndDate() => false; public bool ShouldSerializeEndDate() => false;

View File

@ -15,8 +15,6 @@ using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Replays.Legacy; using osu.Game.Replays.Legacy;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Replays.Types;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -46,15 +44,8 @@ namespace osu.Game.Online.Spectator
private readonly BindableDictionary<int, SpectatorState> playingUserStates = new BindableDictionary<int, SpectatorState>(); private readonly BindableDictionary<int, SpectatorState> playingUserStates = new BindableDictionary<int, SpectatorState>();
private IBeatmap? currentBeatmap; private IBeatmap? currentBeatmap;
private Score? currentScore; private Score? currentScore;
[Resolved]
private IBindable<RulesetInfo> currentRuleset { get; set; } = null!;
[Resolved]
private IBindable<IReadOnlyList<Mod>> currentMods { get; set; } = null!;
private readonly SpectatorState currentState = new SpectatorState(); private readonly SpectatorState currentState = new SpectatorState();
/// <summary> /// <summary>
@ -153,9 +144,9 @@ namespace osu.Game.Online.Spectator
IsPlaying = true; IsPlaying = true;
// transfer state at point of beginning play // transfer state at point of beginning play
currentState.BeatmapID = beatmap.BeatmapInfo.OnlineBeatmapID; currentState.BeatmapID = score.ScoreInfo.Beatmap.OnlineBeatmapID;
currentState.RulesetID = currentRuleset.Value.ID; currentState.RulesetID = score.ScoreInfo.RulesetID;
currentState.Mods = currentMods.Value.Select(m => new APIMod(m)); currentState.Mods = score.ScoreInfo.Mods.Select(m => new APIMod(m)).ToArray();
currentBeatmap = beatmap.PlayableBeatmap; currentBeatmap = beatmap.PlayableBeatmap;
currentScore = score; currentScore = score;

View File

@ -140,8 +140,6 @@ namespace osu.Game
private FileStore fileStore; private FileStore fileStore;
private SettingsStore settingsStore;
private RulesetConfigCache rulesetConfigCache; private RulesetConfigCache rulesetConfigCache;
private SpectatorClient spectatorClient; private SpectatorClient spectatorClient;
@ -243,7 +241,7 @@ namespace osu.Game
dependencies.Cache(fileStore = new FileStore(contextFactory, Storage)); dependencies.Cache(fileStore = new FileStore(contextFactory, Storage));
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Host, () => difficultyCache, LocalConfig)); dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Scheduler, Host, () => difficultyCache, LocalConfig));
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, true)); dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, true));
// this should likely be moved to ArchiveModelManager when another case appears where it is necessary // this should likely be moved to ArchiveModelManager when another case appears where it is necessary
@ -279,8 +277,7 @@ namespace osu.Game
migrateDataToRealm(); migrateDataToRealm();
dependencies.Cache(settingsStore = new SettingsStore(contextFactory)); dependencies.Cache(rulesetConfigCache = new RulesetConfigCache(realmFactory, RulesetStore));
dependencies.Cache(rulesetConfigCache = new RulesetConfigCache(settingsStore));
var powerStatus = CreateBatteryInfo(); var powerStatus = CreateBatteryInfo();
if (powerStatus != null) if (powerStatus != null)
@ -453,24 +450,27 @@ namespace osu.Game
using (var db = contextFactory.GetForWrite()) using (var db = contextFactory.GetForWrite())
using (var usage = realmFactory.GetForWrite()) using (var usage = realmFactory.GetForWrite())
{ {
var existingBindings = db.Context.DatabasedKeyBinding; // migrate ruleset settings. can be removed 20220315.
var existingSettings = db.Context.DatabasedSetting;
// only migrate data if the realm database is empty. // only migrate data if the realm database is empty.
if (!usage.Realm.All<RealmKeyBinding>().Any()) if (!usage.Realm.All<RealmRulesetSetting>().Any())
{ {
foreach (var dkb in existingBindings) foreach (var dkb in existingSettings)
{ {
usage.Realm.Add(new RealmKeyBinding if (dkb.RulesetID == null) continue;
usage.Realm.Add(new RealmRulesetSetting
{ {
KeyCombinationString = dkb.KeyCombination.ToString(), Key = dkb.Key,
ActionInt = (int)dkb.Action, Value = dkb.StringValue,
RulesetID = dkb.RulesetID, RulesetID = dkb.RulesetID.Value,
Variant = dkb.Variant Variant = dkb.Variant ?? 0,
}); });
} }
} }
db.Context.RemoveRange(existingBindings); db.Context.RemoveRange(existingSettings);
usage.Commit(); usage.Commit();
} }

View File

@ -3,21 +3,22 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osuTK;
using osu.Framework.Bindables;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
using osuTK.Graphics;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Scoring; using osu.Game.Scoring;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.BeatmapListing namespace osu.Game.Overlays.BeatmapListing
{ {
@ -117,7 +118,7 @@ namespace osu.Game.Overlays.BeatmapListing
textBox = new BeatmapSearchTextBox textBox = new BeatmapSearchTextBox
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
TypingStarted = () => TypingStarted?.Invoke(), TextChanged = () => TypingStarted?.Invoke(),
}, },
new ReverseChildIDFillFlowContainer<Drawable> new ReverseChildIDFillFlowContainer<Drawable>
{ {
@ -127,7 +128,7 @@ namespace osu.Game.Overlays.BeatmapListing
Padding = new MarginPadding { Horizontal = 10 }, Padding = new MarginPadding { Horizontal = 10 },
Children = new Drawable[] Children = new Drawable[]
{ {
generalFilter = new BeatmapSearchMultipleSelectionFilterRow<SearchGeneral>(BeatmapsStrings.ListingSearchFiltersGeneral), generalFilter = new BeatmapSearchGeneralFilterRow(),
modeFilter = new BeatmapSearchRulesetFilterRow(), modeFilter = new BeatmapSearchRulesetFilterRow(),
categoryFilter = new BeatmapSearchFilterRow<SearchCategory>(BeatmapsStrings.ListingSearchFiltersStatus), categoryFilter = new BeatmapSearchFilterRow<SearchCategory>(BeatmapsStrings.ListingSearchFiltersStatus),
genreFilter = new BeatmapSearchFilterRow<SearchGenre>(BeatmapsStrings.ListingSearchFiltersGenre), genreFilter = new BeatmapSearchFilterRow<SearchGenre>(BeatmapsStrings.ListingSearchFiltersGenre),
@ -167,7 +168,7 @@ namespace osu.Game.Overlays.BeatmapListing
/// <summary> /// <summary>
/// Any time the text box receives key events (even while masked). /// Any time the text box receives key events (even while masked).
/// </summary> /// </summary>
public Action TypingStarted; public Action TextChanged;
protected override Color4 SelectionColour => Color4.Gray; protected override Color4 SelectionColour => Color4.Gray;
@ -181,7 +182,16 @@ namespace osu.Game.Overlays.BeatmapListing
if (!base.OnKeyDown(e)) if (!base.OnKeyDown(e))
return false; return false;
TypingStarted?.Invoke(); TextChanged?.Invoke();
return true;
}
public override bool OnPressed(GlobalAction action)
{
if (!base.OnPressed(action))
return false;
TextChanged?.Invoke();
return true; return true;
} }
} }

View File

@ -0,0 +1,39 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Resources.Localisation.Web;
using osuTK.Graphics;
namespace osu.Game.Overlays.BeatmapListing
{
public class BeatmapSearchGeneralFilterRow : BeatmapSearchMultipleSelectionFilterRow<SearchGeneral>
{
public BeatmapSearchGeneralFilterRow()
: base(BeatmapsStrings.ListingSearchFiltersGeneral)
{
}
protected override MultipleSelectionFilter CreateMultipleSelectionFilter() => new GeneralFilter();
private class GeneralFilter : MultipleSelectionFilter
{
protected override MultipleSelectionFilterTabItem CreateTabItem(SearchGeneral value)
{
if (value == SearchGeneral.FeaturedArtists)
return new FeaturedArtistsTabItem();
return new MultipleSelectionFilterTabItem(value);
}
}
private class FeaturedArtistsTabItem : MultipleSelectionFilterTabItem
{
public FeaturedArtistsTabItem()
: base(SearchGeneral.FeaturedArtists)
{
}
protected override Color4 GetStateColour() => OverlayColourProvider.Orange.Colour1;
}
}
}

View File

@ -71,10 +71,10 @@ namespace osu.Game.Overlays.BeatmapListing
private void updateState() private void updateState()
{ {
text.FadeColour(IsHovered ? colourProvider.Light1 : getStateColour(), 200, Easing.OutQuint); text.FadeColour(IsHovered ? colourProvider.Light1 : GetStateColour(), 200, Easing.OutQuint);
text.Font = text.Font.With(weight: Active.Value ? FontWeight.SemiBold : FontWeight.Regular); text.Font = text.Font.With(weight: Active.Value ? FontWeight.SemiBold : FontWeight.Regular);
} }
private Color4 getStateColour() => Active.Value ? colourProvider.Content1 : colourProvider.Light2; protected virtual Color4 GetStateColour() => Active.Value ? colourProvider.Content1 : colourProvider.Light2;
} }
} }

View File

@ -25,7 +25,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
private const float horizontal_padding = 10; private const float horizontal_padding = 10;
private const float vertical_padding = 5; private const float vertical_padding = 5;
private FillFlowContainer bottomPanel, statusContainer, titleContainer; private FillFlowContainer bottomPanel, statusContainer, titleContainer, artistContainer;
private PlayButton playButton; private PlayButton playButton;
private Box progressBar; private Box progressBar;
@ -89,11 +89,19 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
}, },
} }
}, },
new OsuSpriteText artistContainer = new FillFlowContainer
{ {
Text = new RomanisableString(SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist), AutoSizeAxes = Axes.Both,
Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true) Direction = FillDirection.Horizontal,
}, Children = new Drawable[]
{
new OsuSpriteText
{
Text = new RomanisableString(SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist),
Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true)
}
}
}
}, },
}, },
new Container new Container
@ -213,6 +221,16 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
}); });
} }
if (SetInfo.OnlineInfo?.TrackId != null)
{
artistContainer.Add(new FeaturedArtistBeatmapPill
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Left = 10f, Top = 2f },
});
}
if (SetInfo.OnlineInfo?.HasVideo ?? false) if (SetInfo.OnlineInfo?.HasVideo ?? false)
{ {
statusContainer.Add(new IconPill(FontAwesome.Solid.Film)); statusContainer.Add(new IconPill(FontAwesome.Solid.Film));

View File

@ -27,7 +27,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
private const float vertical_padding = 5; private const float vertical_padding = 5;
private const float height = 70; private const float height = 70;
private FillFlowContainer statusContainer, titleContainer; private FillFlowContainer statusContainer, titleContainer, artistContainer;
protected BeatmapPanelDownloadButton DownloadButton; protected BeatmapPanelDownloadButton DownloadButton;
private PlayButton playButton; private PlayButton playButton;
private Box progressBar; private Box progressBar;
@ -112,10 +112,18 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
}, },
} }
}, },
new OsuSpriteText artistContainer = new FillFlowContainer
{ {
Text = new RomanisableString(SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist), AutoSizeAxes = Axes.Both,
Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true) Direction = FillDirection.Horizontal,
Children = new[]
{
new OsuSpriteText
{
Text = new RomanisableString(SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist),
Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true)
},
},
}, },
} }
}, },
@ -227,6 +235,16 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
}); });
} }
if (SetInfo.OnlineInfo?.TrackId != null)
{
artistContainer.Add(new FeaturedArtistBeatmapPill
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Left = 10f, Top = 2f },
});
}
if (SetInfo.OnlineInfo?.HasVideo ?? false) if (SetInfo.OnlineInfo?.HasVideo ?? false)
{ {
statusContainer.Add(new IconPill(FontAwesome.Solid.Film) { IconSize = new Vector2(20) }); statusContainer.Add(new IconPill(FontAwesome.Solid.Film) { IconSize = new Vector2(20) });

View File

@ -19,6 +19,10 @@ namespace osu.Game.Overlays.BeatmapListing
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.GeneralFollows))] [LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.GeneralFollows))]
[Description("Subscribed mappers")] [Description("Subscribed mappers")]
Follows Follows,
[LocalisableDescription(typeof(BeatmapsStrings), nameof(BeatmapsStrings.GeneralFeaturedArtists))]
[Description("Featured artists")]
FeaturedArtists
} }
} }

View File

@ -75,6 +75,7 @@ namespace osu.Game.Overlays
{ {
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Masking = true,
Padding = new MarginPadding { Horizontal = 20 }, Padding = new MarginPadding { Horizontal = 20 },
Children = new Drawable[] Children = new Drawable[]
{ {
@ -186,21 +187,16 @@ namespace osu.Game.Overlays
if (lastContent != null) if (lastContent != null)
{ {
var transform = lastContent.FadeOut(100, Easing.OutQuint); lastContent.FadeOut(100, Easing.OutQuint);
if (lastContent == notFoundContent || lastContent == supporterRequiredContent) // Consider the case when the new content is smaller than the last content.
{ // If the auto-size computation is delayed until fade out completes, the background remain high for too long making the resulting transition to the smaller height look weird.
// the placeholders may be used multiple times, so don't expire/dispose them. // At the same time, if the last content's height is bypassed immediately, there is a period where the new content is at Alpha = 0 when the auto-sized height will be 0.
transform.Schedule(() => panelTarget.Remove(lastContent)); // To resolve both of these issues, the bypass is delayed until a point when the content transitions (fade-in and fade-out) overlap and it looks good to do so.
} var sequence = lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y);
else
{ if (lastContent != notFoundContent && lastContent != supporterRequiredContent)
// Consider the case when the new content is smaller than the last content. sequence.Then().Schedule(() => lastContent.Expire());
// If the auto-size computation is delayed until fade out completes, the background remain high for too long making the resulting transition to the smaller height look weird.
// At the same time, if the last content's height is bypassed immediately, there is a period where the new content is at Alpha = 0 when the auto-sized height will be 0.
// To resolve both of these issues, the bypass is delayed until a point when the content transitions (fade-in and fade-out) overlap and it looks good to do so.
lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y).Then().Schedule(() => lastContent.Expire());
}
} }
if (!content.IsAlive) if (!content.IsAlive)
@ -208,6 +204,9 @@ namespace osu.Game.Overlays
content.FadeInFromZero(200, Easing.OutQuint); content.FadeInFromZero(200, Easing.OutQuint);
currentContent = content; currentContent = content;
// currentContent may be one of the placeholders, and still have BypassAutoSizeAxes set to Y from the last fade-out.
// restore to the initial state.
currentContent.BypassAutoSizeAxes = Axes.None;
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

@ -37,6 +37,7 @@ namespace osu.Game.Overlays.BeatmapSet
private readonly OsuSpriteText title, artist; private readonly OsuSpriteText title, artist;
private readonly AuthorInfo author; private readonly AuthorInfo author;
private readonly ExplicitContentBeatmapPill explicitContentPill; private readonly ExplicitContentBeatmapPill explicitContentPill;
private readonly FeaturedArtistBeatmapPill featuredArtistPill;
private readonly FillFlowContainer downloadButtonsContainer; private readonly FillFlowContainer downloadButtonsContainer;
private readonly BeatmapAvailability beatmapAvailability; private readonly BeatmapAvailability beatmapAvailability;
private readonly BeatmapSetOnlineStatusPill onlineStatusPill; private readonly BeatmapSetOnlineStatusPill onlineStatusPill;
@ -129,10 +130,25 @@ namespace osu.Game.Overlays.BeatmapSet
} }
} }
}, },
artist = new OsuSpriteText new FillFlowContainer
{ {
Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, italics: true), Direction = FillDirection.Horizontal,
Margin = new MarginPadding { Bottom = 20 } AutoSizeAxes = Axes.Both,
Margin = new MarginPadding { Bottom = 20 },
Children = new Drawable[]
{
artist = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, italics: true),
},
featuredArtistPill = new FeaturedArtistBeatmapPill
{
Alpha = 0f,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Margin = new MarginPadding { Left = 10 }
}
}
}, },
new Container new Container
{ {
@ -233,6 +249,7 @@ namespace osu.Game.Overlays.BeatmapSet
artist.Text = new RomanisableString(setInfo.NewValue.Metadata.ArtistUnicode, setInfo.NewValue.Metadata.Artist); artist.Text = new RomanisableString(setInfo.NewValue.Metadata.ArtistUnicode, setInfo.NewValue.Metadata.Artist);
explicitContentPill.Alpha = setInfo.NewValue.OnlineInfo.HasExplicitContent ? 1 : 0; explicitContentPill.Alpha = setInfo.NewValue.OnlineInfo.HasExplicitContent ? 1 : 0;
featuredArtistPill.Alpha = setInfo.NewValue.OnlineInfo.TrackId != null ? 1 : 0;
onlineStatusPill.FadeIn(500, Easing.OutQuint); onlineStatusPill.FadeIn(500, Easing.OutQuint);
onlineStatusPill.Status = setInfo.NewValue.OnlineInfo.Status; onlineStatusPill.Status = setInfo.NewValue.OnlineInfo.Status;

View File

@ -0,0 +1,47 @@
// 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.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.BeatmapSet
{
public class FeaturedArtistBeatmapPill : CompositeDrawable
{
public FeaturedArtistBeatmapPill()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader(true)]
private void load(OsuColour colours, OverlayColourProvider colourProvider)
{
InternalChild = new CircularContainer
{
Masking = true,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider?.Background5 ?? colours.Gray2,
},
new OsuSpriteText
{
Margin = new MarginPadding { Horizontal = 10f, Vertical = 2f },
Text = BeatmapsetsStrings.FeaturedArtistBadgeLabel.ToUpper(),
Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold),
Colour = OverlayColourProvider.Blue.Colour1,
}
}
};
}
}
}

View File

@ -19,7 +19,7 @@ namespace osu.Game.Overlays.BeatmapSet
{ {
public class LeaderboardModSelector : CompositeDrawable public class LeaderboardModSelector : CompositeDrawable
{ {
public readonly BindableList<Mod> SelectedMods = new BindableList<Mod>(); public readonly BindableList<IMod> SelectedMods = new BindableList<IMod>();
public readonly Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>(); public readonly Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
private readonly FillFlowContainer<ModButton> modsContainer; private readonly FillFlowContainer<ModButton> modsContainer;
@ -54,7 +54,7 @@ namespace osu.Game.Overlays.BeatmapSet
return; return;
modsContainer.Add(new ModButton(new ModNoMod())); modsContainer.Add(new ModButton(new ModNoMod()));
modsContainer.AddRange(ruleset.NewValue.CreateInstance().GetAllMods().Where(m => m.UserPlayable).Select(m => new ModButton(m))); modsContainer.AddRange(ruleset.NewValue.CreateInstance().AllMods.Where(m => m.UserPlayable).Select(m => new ModButton(m)));
modsContainer.ForEach(button => modsContainer.ForEach(button =>
{ {
@ -76,7 +76,7 @@ namespace osu.Game.Overlays.BeatmapSet
updateHighlighted(); updateHighlighted();
} }
private void selectionChanged(Mod mod, bool selected) private void selectionChanged(IMod mod, bool selected)
{ {
if (selected) if (selected)
SelectedMods.Add(mod); SelectedMods.Add(mod);
@ -101,9 +101,9 @@ namespace osu.Game.Overlays.BeatmapSet
private const int duration = 200; private const int duration = 200;
public readonly BindableBool Highlighted = new BindableBool(); public readonly BindableBool Highlighted = new BindableBool();
public Action<Mod, bool> OnSelectionChanged; public Action<IMod, bool> OnSelectionChanged;
public ModButton(Mod mod) public ModButton(IMod mod)
: base(mod) : base(mod)
{ {
Scale = new Vector2(0.4f); Scale = new Vector2(0.4f);

View File

@ -7,6 +7,8 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osuTK; using osuTK;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.API; using osu.Game.Online.API;
@ -14,6 +16,7 @@ using osu.Game.Online.API.Requests;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Screens.Select.Leaderboards; using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Users; using osu.Game.Users;
@ -42,34 +45,46 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
[Resolved] [Resolved]
private RulesetStore rulesets { get; set; } private RulesetStore rulesets { get; set; }
[Resolved]
private ScoreManager scoreManager { get; set; }
private GetScoresRequest getScoresRequest; private GetScoresRequest getScoresRequest;
private CancellationTokenSource loadCancellationSource;
protected APILegacyScores Scores protected APILegacyScores Scores
{ {
set => Schedule(() => set => Schedule(() =>
{ {
loadCancellationSource?.Cancel();
loadCancellationSource = new CancellationTokenSource();
topScoresContainer.Clear(); topScoresContainer.Clear();
scoreTable.ClearScores();
scoreTable.Hide();
if (value?.Scores.Any() != true) if (value?.Scores.Any() != true)
{
scoreTable.ClearScores();
scoreTable.Hide();
return; return;
}
var scoreInfos = value.Scores.Select(s => s.CreateScoreInfo(rulesets)).ToList(); scoreManager.OrderByTotalScoreAsync(value.Scores.Select(s => s.CreateScoreInfo(rulesets)).ToArray(), loadCancellationSource.Token)
var topScore = scoreInfos.First(); .ContinueWith(ordered => Schedule(() =>
{
if (loadCancellationSource.IsCancellationRequested)
return;
scoreTable.DisplayScores(scoreInfos, topScore.Beatmap?.Status.GrantsPerformancePoints() == true); var topScore = ordered.Result.First();
scoreTable.Show();
var userScore = value.UserScore; scoreTable.DisplayScores(ordered.Result, topScore.Beatmap?.Status.GrantsPerformancePoints() == true);
var userScoreInfo = userScore?.Score.CreateScoreInfo(rulesets); scoreTable.Show();
topScoresContainer.Add(new DrawableTopScore(topScore)); var userScore = value.UserScore;
var userScoreInfo = userScore?.Score.CreateScoreInfo(rulesets);
if (userScoreInfo != null && userScoreInfo.OnlineScoreID != topScore.OnlineScoreID) topScoresContainer.Add(new DrawableTopScore(topScore));
topScoresContainer.Add(new DrawableTopScore(userScoreInfo, userScore.Position));
if (userScoreInfo != null && userScoreInfo.OnlineScoreID != topScore.OnlineScoreID)
topScoresContainer.Add(new DrawableTopScore(userScoreInfo, userScore.Position));
}), TaskContinuationOptions.OnlyOnRanToCompletion);
}); });
} }

View File

@ -26,9 +26,6 @@ namespace osu.Game.Overlays.Changelog
private const float image_container_width = 164; private const float image_container_width = 164;
private const float heart_size = 75; private const float heart_size = 75;
private readonly FillFlowContainer textContainer;
private readonly Container imageContainer;
public ChangelogSupporterPromo() public ChangelogSupporterPromo()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
@ -38,6 +35,12 @@ namespace osu.Game.Overlays.Changelog
Vertical = 20, Vertical = 20,
Horizontal = 50, Horizontal = 50,
}; };
}
[BackgroundDependencyLoader]
private void load(OsuColour colour, TextureStore textures, OverlayColourProvider colourProvider)
{
SupporterPromoLinkFlowContainer supportLinkText;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
@ -59,7 +62,7 @@ namespace osu.Game.Overlays.Changelog
new Box new Box
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4.Black.Opacity(0.3f), Colour = colourProvider.Background5,
}, },
new Container new Container
{ {
@ -68,7 +71,7 @@ namespace osu.Game.Overlays.Changelog
Padding = new MarginPadding { Horizontal = 75 }, Padding = new MarginPadding { Horizontal = 75 },
Children = new Drawable[] Children = new Drawable[]
{ {
textContainer = new FillFlowContainer new FillFlowContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
@ -76,91 +79,84 @@ namespace osu.Game.Overlays.Changelog
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Padding = new MarginPadding { Right = 50 + image_container_width }, Padding = new MarginPadding { Right = 50 + image_container_width },
Children = new Drawable[]
{
new OsuSpriteText
{
Text = ChangelogStrings.SupportHeading,
Font = OsuFont.GetFont(size: 20, weight: FontWeight.Light),
Margin = new MarginPadding { Bottom = 20 },
},
supportLinkText = new SupporterPromoLinkFlowContainer(t =>
{
t.Font = t.Font.With(size: 14);
t.Colour = colour.PinkLighter;
})
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
new OsuTextFlowContainer(t =>
{
t.Font = t.Font.With(size: 12);
t.Colour = colour.PinkLighter;
})
{
Text = ChangelogStrings.SupportText2.ToString(),
Margin = new MarginPadding { Top = 10 },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
},
}, },
imageContainer = new Container new Container
{ {
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Width = image_container_width, Width = image_container_width,
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
Children = new Drawable[]
{
new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Bottom = 28 },
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill,
Texture = textures.Get(@"Online/supporter-pippi"),
},
new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Size = new Vector2(heart_size),
Margin = new MarginPadding { Top = 70 },
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Colour = colour.Pink,
Radius = 10,
Roundness = heart_size / 2,
},
Child = new Sprite
{
Size = new Vector2(heart_size),
Texture = textures.Get(@"Online/supporter-heart"),
},
},
}
} }
} }
}, },
} }
}, },
}; };
}
[BackgroundDependencyLoader]
private void load(OsuColour colour, TextureStore textures)
{
SupporterPromoLinkFlowContainer supportLinkText;
textContainer.Children = new Drawable[]
{
new OsuSpriteText
{
Text = ChangelogStrings.SupportHeading,
Font = OsuFont.GetFont(size: 20, weight: FontWeight.Light),
Margin = new MarginPadding { Bottom = 20 },
},
supportLinkText = new SupporterPromoLinkFlowContainer(t =>
{
t.Font = t.Font.With(size: 14);
t.Colour = colour.PinkLighter;
})
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
new OsuTextFlowContainer(t =>
{
t.Font = t.Font.With(size: 12);
t.Colour = colour.PinkLighter;
})
{
Text = ChangelogStrings.SupportText2.ToString(),
Margin = new MarginPadding { Top = 10 },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
};
supportLinkText.AddText("Support further development of osu! and "); supportLinkText.AddText("Support further development of osu! and ");
supportLinkText.AddLink("become and osu!supporter", "https://osu.ppy.sh/home/support", t => t.Font = t.Font.With(weight: FontWeight.Bold)); supportLinkText.AddLink("become an osu!supporter", @"https://osu.ppy.sh/home/support", t => t.Font = t.Font.With(weight: FontWeight.Bold));
supportLinkText.AddText(" today!"); supportLinkText.AddText(" today!");
imageContainer.Children = new Drawable[]
{
new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Bottom = 28 },
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill,
Texture = textures.Get(@"Online/supporter-pippi"),
},
new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Size = new Vector2(heart_size),
Margin = new MarginPadding { Top = 70 },
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Colour = colour.Pink,
Radius = 10,
Roundness = heart_size / 2,
},
Child = new Sprite
{
Size = new Vector2(heart_size),
Texture = textures.Get(@"Online/supporter-heart"),
},
},
};
} }
private class SupporterPromoLinkFlowContainer : LinkFlowContainer private class SupporterPromoLinkFlowContainer : LinkFlowContainer
@ -170,27 +166,18 @@ namespace osu.Game.Overlays.Changelog
{ {
} }
public new void AddLink(string text, string url, Action<SpriteText> creationParameters) => protected override DrawableLinkCompiler CreateLinkCompiler(IEnumerable<SpriteText> parts) => new SupporterPromoLinkCompiler(parts);
AddInternal(new SupporterPromoLinkCompiler(AddText(text, creationParameters)) { Url = url });
private class SupporterPromoLinkCompiler : DrawableLinkCompiler private class SupporterPromoLinkCompiler : DrawableLinkCompiler
{ {
[Resolved(CanBeNull = true)]
private OsuGame game { get; set; }
public string Url;
public SupporterPromoLinkCompiler(IEnumerable<Drawable> parts) public SupporterPromoLinkCompiler(IEnumerable<Drawable> parts)
: base(parts) : base(parts)
{ {
RelativeSizeAxes = Axes.Both;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colour) private void load(OsuColour colour)
{ {
TooltipText = Url;
Action = () => game?.HandleLink(Url);
IdleColour = colour.PinkDark; IdleColour = colour.PinkDark;
HoverColour = Color4.White; HoverColour = Color4.White;
} }

View File

@ -107,9 +107,9 @@ namespace osu.Game.Overlays.Mods
var incompatibleTypes = mod.IncompatibleMods; var incompatibleTypes = mod.IncompatibleMods;
var allMods = ruleset.Value.CreateInstance().GetAllMods(); var allMods = ruleset.Value.CreateInstance().AllMods;
incompatibleMods.Value = allMods.Where(m => m.GetType() != mod.GetType() && incompatibleTypes.Any(t => t.IsInstanceOfType(m))).ToList(); incompatibleMods.Value = allMods.Where(m => m.GetType() != mod.GetType() && incompatibleTypes.Any(t => t.IsInstanceOfType(m))).Select(m => m.CreateInstance()).ToList();
incompatibleText.Text = incompatibleMods.Value.Any() ? "Incompatible with:" : "Compatible with all mods"; incompatibleText.Text = incompatibleMods.Value.Any() ? "Incompatible with:" : "Compatible with all mods";
} }
} }

View File

@ -61,7 +61,7 @@ namespace osu.Game.Overlays.Profile.Header
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
Children = new Drawable[] Children = new Drawable[]
{ {
avatar = new UpdateableAvatar(openOnClick: false, showGuestOnNull: false) avatar = new UpdateableAvatar(isInteractive: false, showGuestOnNull: false)
{ {
Size = new Vector2(avatar_size), Size = new Vector2(avatar_size),
Masking = true, Masking = true,

View File

@ -67,7 +67,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{ {
Text = InputSettingsStrings.ResetSectionButton; Text = InputSettingsStrings.ResetSectionButton;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Width = 0.5f; Width = 0.8f;
Anchor = Anchor.TopCentre; Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre; Origin = Anchor.TopCentre;
Margin = new MarginPadding { Top = 15 }; Margin = new MarginPadding { Top = 15 };

View File

@ -24,6 +24,8 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
private OsuDirectorySelector directorySelector; private OsuDirectorySelector directorySelector;
public override bool AllowTrackAdjustments => false;
/// <summary> /// <summary>
/// Text to display in the header to inform the user of what they are selecting. /// Text to display in the header to inform the user of what they are selecting.
/// </summary> /// </summary>

View File

@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Toolbar
Add(new OpaqueBackground { Depth = 1 }); Add(new OpaqueBackground { Depth = 1 });
Flow.Add(avatar = new UpdateableAvatar(openOnClick: false) Flow.Add(avatar = new UpdateableAvatar(isInteractive: false)
{ {
Masking = true, Masking = true,
Size = new Vector2(32), Size = new Vector2(32),

View File

@ -355,6 +355,12 @@ namespace osu.Game.Overlays.Volume
return base.OnMouseMove(e); return base.OnMouseMove(e);
} }
protected override bool OnHover(HoverEvent e)
{
State = SelectionState.Selected;
return false;
}
protected override void OnHoverLost(HoverLostEvent e) protected override void OnHoverLost(HoverLostEvent e)
{ {
} }

View File

@ -2,16 +2,86 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Database;
namespace osu.Game.Rulesets.Configuration namespace osu.Game.Rulesets.Configuration
{ {
public abstract class RulesetConfigManager<TLookup> : DatabasedConfigManager<TLookup>, IRulesetConfigManager public abstract class RulesetConfigManager<TLookup> : ConfigManager<TLookup>, IRulesetConfigManager
where TLookup : struct, Enum where TLookup : struct, Enum
{ {
protected RulesetConfigManager(SettingsStore settings, RulesetInfo ruleset, int? variant = null) private readonly RealmContextFactory realmFactory;
: base(settings, ruleset, variant)
private readonly int variant;
private List<RealmRulesetSetting> databasedSettings = new List<RealmRulesetSetting>();
private readonly int rulesetId;
protected RulesetConfigManager(SettingsStore store, RulesetInfo ruleset, int? variant = null)
{ {
realmFactory = store?.Realm;
if (realmFactory != null && !ruleset.ID.HasValue)
throw new InvalidOperationException("Attempted to add databased settings for a non-databased ruleset");
rulesetId = ruleset.ID ?? -1;
this.variant = variant ?? 0;
Load();
InitialiseDefaults();
}
protected override void PerformLoad()
{
if (realmFactory != null)
{
// As long as RulesetConfigCache exists, there is no need to subscribe to realm events.
databasedSettings = realmFactory.Context.All<RealmRulesetSetting>().Where(b => b.RulesetID == rulesetId && b.Variant == variant).ToList();
}
}
protected override bool PerformSave()
{
// do nothing, realm saves immediately
return true;
}
protected override void AddBindable<TBindable>(TLookup lookup, Bindable<TBindable> bindable)
{
base.AddBindable(lookup, bindable);
var setting = databasedSettings.Find(s => s.Key == lookup.ToString());
if (setting != null)
{
bindable.Parse(setting.Value);
}
else
{
setting = new RealmRulesetSetting
{
Key = lookup.ToString(),
Value = bindable.Value.ToString(),
RulesetID = rulesetId,
Variant = variant,
};
realmFactory?.Context.Write(() => realmFactory.Context.Add(setting));
databasedSettings.Add(setting);
}
bindable.ValueChanged += b =>
{
realmFactory?.Context.Write(() => setting.Value = b.NewValue.ToString());
};
} }
} }
} }

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using Newtonsoft.Json; using osu.Framework.Graphics.Sprites;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
@ -11,7 +11,37 @@ namespace osu.Game.Rulesets.Mods
/// <summary> /// <summary>
/// The shortened name of this mod. /// The shortened name of this mod.
/// </summary> /// </summary>
[JsonProperty("acronym")]
string Acronym { get; } string Acronym { get; }
/// <summary>
/// The name of this mod.
/// </summary>
string Name { get; }
/// <summary>
/// The user readable description of this mod.
/// </summary>
string Description { get; }
/// <summary>
/// The type of this mod.
/// </summary>
ModType Type { get; }
/// <summary>
/// The icon of this mod.
/// </summary>
IconUsage? Icon { get; }
/// <summary>
/// Whether this mod is playable by an end user.
/// Should be <c>false</c> for cases where the user is not interacting with the game (so it can be excluded from multiplayer selection, for example).
/// </summary>
bool UserPlayable { get; }
/// <summary>
/// Create a fresh <see cref="Mod"/> instance based on this mod.
/// </summary>
Mod CreateInstance() => (Mod)Activator.CreateInstance(GetType());
} }
} }

View File

@ -22,32 +22,17 @@ namespace osu.Game.Rulesets.Mods
[ExcludeFromDynamicCompile] [ExcludeFromDynamicCompile]
public abstract class Mod : IMod, IEquatable<Mod>, IDeepCloneable<Mod> public abstract class Mod : IMod, IEquatable<Mod>, IDeepCloneable<Mod>
{ {
/// <summary>
/// The name of this mod.
/// </summary>
[JsonIgnore] [JsonIgnore]
public abstract string Name { get; } public abstract string Name { get; }
/// <summary>
/// The shortened name of this mod.
/// </summary>
public abstract string Acronym { get; } public abstract string Acronym { get; }
/// <summary>
/// The icon of this mod.
/// </summary>
[JsonIgnore] [JsonIgnore]
public virtual IconUsage? Icon => null; public virtual IconUsage? Icon => null;
/// <summary>
/// The type of this mod.
/// </summary>
[JsonIgnore] [JsonIgnore]
public virtual ModType Type => ModType.Fun; public virtual ModType Type => ModType.Fun;
/// <summary>
/// The user readable description of this mod.
/// </summary>
[JsonIgnore] [JsonIgnore]
public abstract string Description { get; } public abstract string Description { get; }
@ -106,10 +91,6 @@ namespace osu.Game.Rulesets.Mods
[JsonIgnore] [JsonIgnore]
public virtual bool HasImplementation => this is IApplicableMod; public virtual bool HasImplementation => this is IApplicableMod;
/// <summary>
/// Whether this mod is playable by an end user.
/// Should be <c>false</c> for cases where the user is not interacting with the game (so it can be excluded from mutliplayer selection, for example).
/// </summary>
[JsonIgnore] [JsonIgnore]
public virtual bool UserPlayable => true; public virtual bool UserPlayable => true;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -38,13 +39,58 @@ namespace osu.Game.Rulesets
{ {
public RulesetInfo RulesetInfo { get; internal set; } public RulesetInfo RulesetInfo { get; internal set; }
public IEnumerable<Mod> GetAllMods() => Enum.GetValues(typeof(ModType)).Cast<ModType>() private static readonly ConcurrentDictionary<int, IMod[]> mod_reference_cache = new ConcurrentDictionary<int, IMod[]>();
// Confine all mods of each mod type into a single IEnumerable<Mod>
.SelectMany(GetModsFor) /// <summary>
// Filter out all null mods /// A queryable source containing all available mods.
.Where(mod => mod != null) /// Call <see cref="IMod.CreateInstance"/> for consumption purposes.
// Resolve MultiMods as their .Mods property /// </summary>
.SelectMany(mod => (mod as MultiMod)?.Mods ?? new[] { mod }); public IEnumerable<IMod> AllMods
{
get
{
if (!(RulesetInfo.ID is int id))
return CreateAllMods();
if (!mod_reference_cache.TryGetValue(id, out var mods))
mod_reference_cache[id] = mods = CreateAllMods().Cast<IMod>().ToArray();
return mods;
}
}
/// <summary>
/// Returns fresh instances of all mods.
/// </summary>
/// <remarks>
/// This comes with considerable allocation overhead. If only accessing for reference purposes (ie. not changing bindables / settings)
/// use <see cref="AllMods"/> instead.
/// </remarks>
public IEnumerable<Mod> CreateAllMods() => Enum.GetValues(typeof(ModType)).Cast<ModType>()
// Confine all mods of each mod type into a single IEnumerable<Mod>
.SelectMany(GetModsFor)
// Filter out all null mods
.Where(mod => mod != null)
// Resolve MultiMods as their .Mods property
.SelectMany(mod => (mod as MultiMod)?.Mods ?? new[] { mod });
/// <summary>
/// Returns a fresh instance of the mod matching the specified acronym.
/// </summary>
/// <param name="acronym">The acronym to query for .</param>
public Mod CreateModFromAcronym(string acronym)
{
return AllMods.FirstOrDefault(m => m.Acronym == acronym)?.CreateInstance();
}
/// <summary>
/// Returns a fresh instance of the mod matching the specified type.
/// </summary>
public T CreateMod<T>()
where T : Mod
{
return AllMods.FirstOrDefault(m => m is T)?.CreateInstance() as T;
}
public abstract IEnumerable<Mod> GetModsFor(ModType type); public abstract IEnumerable<Mod> GetModsFor(ModType type);
@ -126,7 +172,7 @@ namespace osu.Game.Rulesets
} }
[CanBeNull] [CanBeNull]
public ModAutoplay GetAutoplayMod() => GetAllMods().OfType<ModAutoplay>().FirstOrDefault(); public ModAutoplay GetAutoplayMod() => CreateMod<ModAutoplay>();
public virtual ISkin CreateLegacySkinProvider([NotNull] ISkin skin, IBeatmap beatmap) => null; public virtual ISkin CreateLegacySkinProvider([NotNull] ISkin skin, IBeatmap beatmap) => null;

View File

@ -2,9 +2,10 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Concurrent; using System.Collections.Generic;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Configuration;
namespace osu.Game.Rulesets namespace osu.Game.Rulesets
@ -15,12 +16,31 @@ namespace osu.Game.Rulesets
/// </summary> /// </summary>
public class RulesetConfigCache : Component public class RulesetConfigCache : Component
{ {
private readonly ConcurrentDictionary<int, IRulesetConfigManager> configCache = new ConcurrentDictionary<int, IRulesetConfigManager>(); private readonly RealmContextFactory realmFactory;
private readonly SettingsStore settingsStore; private readonly RulesetStore rulesets;
public RulesetConfigCache(SettingsStore settingsStore) private readonly Dictionary<int, IRulesetConfigManager> configCache = new Dictionary<int, IRulesetConfigManager>();
public RulesetConfigCache(RealmContextFactory realmFactory, RulesetStore rulesets)
{ {
this.settingsStore = settingsStore; this.realmFactory = realmFactory;
this.rulesets = rulesets;
}
protected override void LoadComplete()
{
base.LoadComplete();
var settingsStore = new SettingsStore(realmFactory);
// let's keep things simple for now and just retrieve all the required configs at startup..
foreach (var ruleset in rulesets.AvailableRulesets)
{
if (ruleset.ID == null)
continue;
configCache[ruleset.ID.Value] = ruleset.CreateInstance().CreateConfig(settingsStore);
}
} }
/// <summary> /// <summary>
@ -34,7 +54,12 @@ namespace osu.Game.Rulesets
if (ruleset.RulesetInfo.ID == null) if (ruleset.RulesetInfo.ID == null)
return null; return null;
return configCache.GetOrAdd(ruleset.RulesetInfo.ID.Value, _ => ruleset.CreateConfig(settingsStore)); if (!configCache.TryGetValue(ruleset.RulesetInfo.ID.Value, out var config))
// any ruleset request which wasn't initialised on startup should not be stored to realm.
// this should only be used by tests.
return ruleset.CreateConfig(null);
return config;
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

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