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

Merge branch 'master' into 14421-multi-password-error-handling

This commit is contained in:
Bartłomiej Dach 2021-09-14 22:58:20 +02:00
commit 7b9e501fa0
No known key found for this signature in database
GPG Key ID: BCECCD4FA41F6497
86 changed files with 1771 additions and 548 deletions

View File

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

View File

@ -31,12 +31,11 @@ If you are looking to install or test osu! without setting up a development envi
**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.
- 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.
## Developing a custom ruleset

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" />
</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.907.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">

View File

@ -14,9 +14,9 @@ namespace osu.Game.Benchmarks
[Params(1, 10, 100)]
public int Times { get; set; }
[GlobalSetup]
public void GlobalSetup()
public override void SetUp()
{
base.SetUp();
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.
// See the LICENCE file in the repository root for full licence text.
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;
namespace osu.Game.Benchmarks
@ -11,7 +12,7 @@ namespace osu.Game.Benchmarks
{
BenchmarkSwitcher
.FromAssembly(typeof(Program).Assembly)
.Run(args);
.Run(args, DefaultConfig.Instance.WithOption(ConfigOptions.DisableOptimizationsValidator, true));
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
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.Configuration;
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
{
@ -22,14 +27,65 @@ namespace osu.Game.Rulesets.Mania.Tests
[Resolved]
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;
var ruleset = new ManiaRuleset();
var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 })
{
HitObjects =
@ -45,7 +101,7 @@ namespace osu.Game.Rulesets.Mania.Tests
new Note { StartTime = beat_length }
},
ControlPointInfo = new ControlPointInfo(),
BeatmapInfo = { Ruleset = ruleset.RulesetInfo },
BeatmapInfo = { Ruleset = Ruleset.Value },
};
foreach (var note in beatmap.HitObjects)
@ -57,24 +113,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
BeatLength = beat_length
});
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);
return beatmap;
}
}
}

View File

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

View File

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

View File

@ -3,6 +3,7 @@
using NUnit.Framework;
using osu.Game.Online.API;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Tests.Mods
@ -11,26 +12,42 @@ namespace osu.Game.Tests.Mods
public class ModSettingsEqualityComparison
{
[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 mod2 = 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 apiMod3 = new APIMod(mod3);
var doubleConvertedMod1 = new APIMod(mod1).ToMod(ruleset);
var doulbeConvertedMod2 = new APIMod(mod2).ToMod(ruleset);
var doulbeConvertedMod3 = new APIMod(mod3).ToMod(ruleset);
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(apiMod2, Is.EqualTo(apiMod2));
Assert.That(doulbeConvertedMod2, Is.EqualTo(doulbeConvertedMod2));
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(apiMod3, Is.EqualTo(apiMod2));
Assert.That(doulbeConvertedMod3, Is.EqualTo(doulbeConvertedMod2));
}
}
}

View File

@ -7,28 +7,19 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Screens.Menu;
using osu.Game.Tests.Beatmaps.IO;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing
{
public class TestSceneDifficultySwitching : ScreenTestScene
public class TestSceneDifficultySwitching : EditorTestScene
{
private BeatmapSetInfo importedBeatmapSet;
private Editor editor;
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
// required for screen transitions to work properly
// (see comment in EditorLoader.LogoArriving).
[Cached]
private OsuLogo logo = new OsuLogo
{
Alpha = 0
};
protected override bool IsolateSavingFromDatabase => false;
[Resolved]
private OsuGameBase game { get; set; }
@ -36,20 +27,18 @@ namespace osu.Game.Tests.Visual.Editing
[Resolved]
private BeatmapManager beatmaps { get; set; }
[BackgroundDependencyLoader]
private void load() => Add(logo);
private BeatmapSetInfo importedBeatmapSet;
[SetUpSteps]
public void SetUp()
public override void SetUpSteps()
{
AddStep("import test beatmap", () => importedBeatmapSet = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).Result);
base.SetUpSteps();
}
AddStep("set current beatmap", () => Beatmap.Value = beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First()));
AddStep("push loader", () => Stack.Push(new EditorLoader()));
AddUntilStep("wait for editor push", () => Stack.CurrentScreen is Editor);
AddStep("store editor", () => editor = (Editor)Stack.CurrentScreen);
AddUntilStep("wait for editor to load", () => editor.IsLoaded);
protected override void LoadEditor()
{
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First());
base.LoadEditor();
}
[Test]
@ -72,11 +61,7 @@ namespace osu.Game.Tests.Visual.Editing
BeatmapInfo targetDifficulty = null;
PromptForSaveDialog saveDialog = null;
AddStep("remove first hitobject", () =>
{
var editorBeatmap = editor.ChildrenOfType<EditorBeatmap>().Single();
editorBeatmap.RemoveAt(0);
});
AddStep("remove first hitobject", () => EditorBeatmap.RemoveAt(0));
AddStep("set target difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo)));
switchToDifficulty(() => targetDifficulty);
@ -105,11 +90,7 @@ namespace osu.Game.Tests.Visual.Editing
BeatmapInfo targetDifficulty = null;
PromptForSaveDialog saveDialog = null;
AddStep("remove first hitobject", () =>
{
var editorBeatmap = editor.ChildrenOfType<EditorBeatmap>().Single();
editorBeatmap.RemoveAt(0);
});
AddStep("remove first hitobject", () => EditorBeatmap.RemoveAt(0));
AddStep("set target difficulty", () => targetDifficulty = importedBeatmapSet.Beatmaps.Last(beatmap => !beatmap.Equals(Beatmap.Value.BeatmapInfo)));
switchToDifficulty(() => targetDifficulty);
@ -132,34 +113,7 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("stack empty", () => Stack.CurrentScreen == null);
}
private void switchToDifficulty(Func<BeatmapInfo> difficulty)
{
AddUntilStep("wait for menubar to load", () => editor.ChildrenOfType<EditorMenuBar>().Any());
AddStep("open file menu", () =>
{
var menuBar = editor.ChildrenOfType<EditorMenuBar>().Single();
var fileMenu = menuBar.ChildrenOfType<DrawableOsuMenuItem>().First();
InputManager.MoveMouseTo(fileMenu);
InputManager.Click(MouseButton.Left);
});
AddStep("open difficulty menu", () =>
{
var difficultySelector =
editor.ChildrenOfType<DrawableOsuMenuItem>().Single(item => item.Item.Text.Value.ToString().Contains("Change difficulty"));
InputManager.MoveMouseTo(difficultySelector);
});
AddWaitStep("wait for open", 3);
AddStep("switch to target difficulty", () =>
{
var difficultyMenuItem =
editor.ChildrenOfType<DrawableOsuMenuItem>()
.Last(item => item.Item is DifficultyMenuItem difficultyItem && difficultyItem.Beatmap.Equals(difficulty.Invoke()));
InputManager.MoveMouseTo(difficultyMenuItem);
InputManager.Click(MouseButton.Left);
});
}
private void switchToDifficulty(Func<BeatmapInfo> difficulty) => AddStep("switch to difficulty", () => Editor.SwitchToDifficulty(difficulty.Invoke()));
private void confirmEditingBeatmap(Func<BeatmapInfo> targetDifficulty)
{

View File

@ -11,6 +11,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Tests.Resources;
using SharpCompress.Archives;
@ -55,6 +56,9 @@ namespace osu.Game.Tests.Visual.Editing
[Test]
public void TestExitWithoutSave()
{
EditorBeatmap editorBeatmap = null;
AddStep("store editor beatmap", () => editorBeatmap = EditorBeatmap);
AddStep("exit without save", () =>
{
Editor.Exit();
@ -62,7 +66,7 @@ namespace osu.Game.Tests.Visual.Editing
});
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]

View File

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

View File

@ -130,6 +130,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
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));
AddStep("set password", () => room.Password.Value = "password");

View File

@ -52,6 +52,24 @@ namespace osu.Game.Tests.Visual.Multiplayer
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]
public void TestPopoverHidesOnLeavingScreen()
{

View File

@ -15,6 +15,7 @@ using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
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);
}
[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() =>
AddStep("Press escape", () => InputManager.Key(Key.Escape));

View File

@ -5,10 +5,10 @@ using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@ -17,10 +17,11 @@ using osu.Game.Overlays.BeatmapListing;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Users;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneBeatmapListingOverlay : OsuTestScene
public class TestSceneBeatmapListingOverlay : OsuManualInputManagerTestScene
{
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();
[BackgroundDependencyLoader]
private void load()
[SetUpSteps]
public void SetUpSteps()
{
Child = overlay = new BeatmapListingOverlay { State = { Value = Visibility.Visible } };
((DummyAPIAccess)API).HandleRequest = req =>
AddStep("setup overlay", () =>
{
if (!(req is SearchBeatmapSetsRequest searchBeatmapSetsRequest)) return false;
searchBeatmapSetsRequest.TriggerSuccess(new SearchBeatmapSetsResponse
{
BeatmapSets = setsForResponse,
});
return true;
};
Child = overlay = new BeatmapListingOverlay { State = { Value = Visibility.Visible } };
setsForResponse.Clear();
});
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
((DummyAPIAccess)API).LocalUser.Value = new User
api.LocalUser.Value = new User
{
Username = "TestBot",
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]
public void TestNoBeatmapsPlaceholder()
{
@ -63,7 +115,7 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep("placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true);
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());
AddUntilStep("placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true);
@ -193,13 +245,15 @@ namespace osu.Game.Tests.Visual.Online
noPlaceholderShown();
}
private static int searchCount;
private void fetchFor(params BeatmapSetInfo[] beatmaps)
{
setsForResponse.Clear();
setsForResponse.AddRange(beatmaps.Select(b => new TestAPIBeatmapSet(b)));
// trigger arbitrary change for fetching.
searchControl.Query.TriggerChange();
searchControl.Query.Value = $"search {searchCount++}";
}
private void setRankAchievedFilter(ScoreRank[] ranks)
@ -229,8 +283,8 @@ namespace osu.Game.Tests.Visual.Online
private void noPlaceholderShown()
{
AddUntilStep("no placeholder shown", () =>
!overlay.ChildrenOfType<BeatmapListingOverlay.SupporterRequiredDrawable>().Any()
&& !overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().Any());
!overlay.ChildrenOfType<BeatmapListingOverlay.SupporterRequiredDrawable>().Any(d => d.IsPresent)
&& !overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().Any(d => d.IsPresent));
}
private class TestAPIBeatmapSet : APIBeatmapSet

View File

@ -21,6 +21,8 @@ namespace osu.Game.Tests.Visual.Online
protected override bool UseOnlineAPI => true;
private int nextBeatmapSetId = 1;
public TestSceneBeatmapSetOverlay()
{
Add(overlay = new TestBeatmapSetOverlay());
@ -240,12 +242,23 @@ namespace osu.Game.Tests.Visual.Online
{
AddStep("show explicit map", () =>
{
var beatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet;
var beatmapSet = getBeatmapSet();
beatmapSet.OnlineInfo.HasExplicitContent = true;
overlay.ShowBeatmapSet(beatmapSet);
});
}
[Test]
public void TestFeaturedBeatmap()
{
AddStep("show featured map", () =>
{
var beatmapSet = getBeatmapSet();
beatmapSet.OnlineInfo.TrackId = 1;
overlay.ShowBeatmapSet(beatmapSet);
});
}
[Test]
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)
{
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.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@ -85,6 +86,22 @@ namespace osu.Game.Tests.Visual.Online
case JoinChannelRequest joinChannel:
joinChannel.TriggerSuccess();
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;
@ -322,6 +339,27 @@ namespace osu.Game.Tests.Visual.Online
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)
{
var channelKey = Key.Number0 + number;

View File

@ -99,16 +99,23 @@ namespace osu.Game.Tests.Visual.Online
[BackgroundDependencyLoader]
private void load(RulesetStore rulesets)
{
var normal = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet;
var normal = getBeatmapSet();
normal.OnlineInfo.HasVideo = true;
normal.OnlineInfo.HasStoryboard = true;
var undownloadable = getUndownloadableBeatmapSet();
var manyDifficulties = getManyDifficultiesBeatmapSet(rulesets);
var explicitMap = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet;
var explicitMap = getBeatmapSet();
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
{
RelativeSizeAxes = Axes.Both,
@ -125,13 +132,19 @@ namespace osu.Game.Tests.Visual.Online
new GridBeatmapPanel(undownloadable),
new GridBeatmapPanel(manyDifficulties),
new GridBeatmapPanel(explicitMap),
new GridBeatmapPanel(featuredMap),
new GridBeatmapPanel(explicitFeaturedMap),
new ListBeatmapPanel(normal),
new ListBeatmapPanel(undownloadable),
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();
[Test]
public void TestManyRooms()
{
AddStep("add rooms", () => RoomManager.AddRooms(500));
}
[Test]
public void TestScrollByDraggingRooms()
{

View File

@ -203,6 +203,71 @@ namespace osu.Game.Tests.Visual.Ranking
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)
{
AddStep("create list", () => Child = list = creationFunc().With(d =>

View File

@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select EZ mod", () =>
{
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));
@ -106,7 +106,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select HR mod", () =>
{
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));
@ -123,7 +123,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select unchanged Difficulty Adjust mod", () =>
{
var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance();
var difficultyAdjustMod = ruleset.GetAllMods().OfType<ModDifficultyAdjust>().Single();
var difficultyAdjustMod = ruleset.CreateMod<ModDifficultyAdjust>();
difficultyAdjustMod.ReadFromDifficulty(advancedStats.Beatmap.BaseDifficulty);
SelectedMods.Value = new[] { difficultyAdjustMod };
});
@ -142,7 +142,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select changed Difficulty Adjust mod", () =>
{
var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance();
var difficultyAdjustMod = ruleset.GetAllMods().OfType<OsuModDifficultyAdjust>().Single();
var difficultyAdjustMod = ruleset.CreateMod<OsuModDifficultyAdjust>();
var originalDifficulty = advancedStats.Beatmap.BaseDifficulty;
difficultyAdjustMod.ReadFromDifficulty(originalDifficulty);

View File

@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
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) };

View File

@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Width = 200,
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.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
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("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();
testModsWithSameBaseType(
mania.GetAllMods().Single(m => m.GetType() == typeof(ManiaModFadeIn)),
mania.GetAllMods().Single(m => m.GetType() == typeof(ManiaModHidden)));
mania.CreateMod<ManiaModFadeIn>(),
mania.CreateMod<ManiaModHidden>());
}
[Test]

View File

@ -45,7 +45,7 @@ namespace osu.Game.Tournament.Tests.Components
private void success(APIBeatmap apiBeatmap)
{
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)
{

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -49,7 +48,7 @@ namespace osu.Game.Tournament.Components
}
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)
return;

View File

@ -93,6 +93,12 @@ namespace osu.Game.Beatmaps
public bool WidescreenStoryboard { 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;
/// <summary>

View File

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

View File

@ -90,6 +90,12 @@ namespace osu.Game.Beatmaps
/// The song language of this beatmap set.
/// </summary>
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

View File

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

View File

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

View File

@ -4,6 +4,7 @@
#nullable enable
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
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)
{
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);

View File

@ -87,23 +87,25 @@ namespace osu.Game.Graphics.Containers
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,
TooltipText = tooltipText,
Action = () =>
{
if (action != null)
action();
else if (game != null)
game.HandleLink(link);
// fallback to handle cases where OsuGame is not available, ie. tournament client.
else if (link.Action == LinkAction.External)
host.OpenUrlExternally(link.Argument);
},
});
if (action != null)
action();
else if (game != null)
game.HandleLink(link);
// fallback to handle cases where OsuGame is not available, ie. tournament client.
else if (link.Action == LinkAction.External)
host.OpenUrlExternally(link.Argument);
};
AddInternal(linkCompiler);
}
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.
// 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.

View File

@ -70,7 +70,7 @@ namespace osu.Game.Graphics.UserInterface
return base.OnKeyDown(e);
}
public bool OnPressed(GlobalAction action)
public virtual bool OnPressed(GlobalAction action)
{
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]

View File

@ -4,14 +4,17 @@
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Bindings;
using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Graphics.UserInterfaceV2
{
public class OsuPopover : Popover
public class OsuPopover : Popover, IKeyBindingHandler<GlobalAction>
{
private const float fade_duration = 250;
private const double scale_duration = 500;
@ -51,5 +54,23 @@ namespace osu.Game.Graphics.UserInterfaceV2
this.ScaleTo(0.7f, scale_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

@ -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<bool>("SamplesMatchPlaybackRate");
b.Property<bool>("SpecialStyle");
b.Property<float>("StackLeniency");

View File

@ -16,7 +16,7 @@ using osu.Game.Utils;
namespace osu.Game.Online.API
{
[MessagePackObject]
public class APIMod : IMod, IEquatable<APIMod>
public class APIMod : IEquatable<APIMod>
{
[JsonProperty("acronym")]
[Key(0)]
@ -48,31 +48,31 @@ namespace osu.Game.Online.API
public Mod ToMod(Ruleset ruleset)
{
Mod resultMod = ruleset.GetAllMods().FirstOrDefault(m => m.Acronym == Acronym);
Mod resultMod = ruleset.CreateModFromAcronym(Acronym);
if (resultMod == null)
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))
continue;
foreach (var (_, property) in resultMod.GetSettingsSourceProperties())
{
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;
}
public bool Equals(IMod other) => other is APIMod them && Equals(them);
public bool Equals(APIMod other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Acronym == other.Acronym &&
Settings.SequenceEqual(other.Settings, ModSettingsEqualityComparer.Default);
return Acronym == other.Acronym && Settings.SequenceEqual(other.Settings, ModSettingsEqualityComparer.Default);
}
public override string ToString()

View File

@ -18,9 +18,9 @@ namespace osu.Game.Online.API.Requests
private readonly BeatmapInfo beatmap;
private readonly BeatmapLeaderboardScope scope;
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)
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.scope = scope;
this.ruleset = ruleset ?? throw new ArgumentNullException(nameof(ruleset));
this.mods = mods ?? Array.Empty<Mod>();
this.mods = mods ?? Array.Empty<IMod>();
Success += onSuccess;
}

View File

@ -8,7 +8,7 @@ namespace osu.Game.Online.API.Requests
{
public class GetUserRequest : APIRequest<User>
{
private readonly string lookup;
public readonly string Lookup;
public readonly RulesetInfo Ruleset;
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>
public GetUserRequest(long? userId = null, RulesetInfo ruleset = null)
{
lookup = userId.ToString();
Lookup = userId.ToString();
lookupType = LookupType.Id;
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>
public GetUserRequest(string username = null, RulesetInfo ruleset = null)
{
lookup = username;
Lookup = username;
lookupType = LookupType.Username;
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
{

View File

@ -63,6 +63,9 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"ratings")]
private int[] ratings { get; set; }
[JsonProperty(@"track_id")]
private int? trackId { get; set; }
[JsonProperty(@"user_id")]
private int creatorId
{
@ -106,7 +109,8 @@ namespace osu.Game.Online.API.Requests.Responses
Availability = availability,
HasFavourited = hasFavourited,
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 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.
mods = mods.Append(rulesetInstance.GetAllMods().OfType<ModClassic>().Single()).ToArray();
mods = mods.Append(rulesetInstance.CreateMod<ModClassic>()).ToArray();
var scoreInfo = new ScoreInfo
{

View File

@ -256,8 +256,36 @@ namespace osu.Game.Online.Chat
JoinChannel(channel);
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":
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;
default:

View File

@ -3,21 +3,22 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osuTK;
using osu.Framework.Bindables;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Resources.Localisation.Web;
using osuTK.Graphics;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.BeatmapListing
{
@ -117,7 +118,7 @@ namespace osu.Game.Overlays.BeatmapListing
textBox = new BeatmapSearchTextBox
{
RelativeSizeAxes = Axes.X,
TypingStarted = () => TypingStarted?.Invoke(),
TextChanged = () => TypingStarted?.Invoke(),
},
new ReverseChildIDFillFlowContainer<Drawable>
{
@ -167,7 +168,7 @@ namespace osu.Game.Overlays.BeatmapListing
/// <summary>
/// Any time the text box receives key events (even while masked).
/// </summary>
public Action TypingStarted;
public Action TextChanged;
protected override Color4 SelectionColour => Color4.Gray;
@ -181,7 +182,16 @@ namespace osu.Game.Overlays.BeatmapListing
if (!base.OnKeyDown(e))
return false;
TypingStarted?.Invoke();
TextChanged?.Invoke();
return true;
}
public override bool OnPressed(GlobalAction action)
{
if (!base.OnPressed(action))
return false;
TextChanged?.Invoke();
return true;
}
}

View File

@ -25,7 +25,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
private const float horizontal_padding = 10;
private const float vertical_padding = 5;
private FillFlowContainer bottomPanel, statusContainer, titleContainer;
private FillFlowContainer bottomPanel, statusContainer, titleContainer, artistContainer;
private PlayButton playButton;
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),
Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true)
},
AutoSizeAxes = Axes.Both,
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
@ -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)
{
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 height = 70;
private FillFlowContainer statusContainer, titleContainer;
private FillFlowContainer statusContainer, titleContainer, artistContainer;
protected BeatmapPanelDownloadButton DownloadButton;
private PlayButton playButton;
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),
Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true)
AutoSizeAxes = Axes.Both,
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)
{
statusContainer.Add(new IconPill(FontAwesome.Solid.Film) { IconSize = new Vector2(20) });

View File

@ -75,6 +75,7 @@ namespace osu.Game.Overlays
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Masking = true,
Padding = new MarginPadding { Horizontal = 20 },
Children = new Drawable[]
{
@ -186,21 +187,16 @@ namespace osu.Game.Overlays
if (lastContent != null)
{
var transform = lastContent.FadeOut(100, Easing.OutQuint);
lastContent.FadeOut(100, Easing.OutQuint);
if (lastContent == notFoundContent || lastContent == supporterRequiredContent)
{
// the placeholders may be used multiple times, so don't expire/dispose them.
transform.Schedule(() => panelTarget.Remove(lastContent));
}
else
{
// 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.
// 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());
}
// 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.
// 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.
var sequence = lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y);
if (lastContent != notFoundContent && lastContent != supporterRequiredContent)
sequence.Then().Schedule(() => lastContent.Expire());
}
if (!content.IsAlive)
@ -208,6 +204,9 @@ namespace osu.Game.Overlays
content.FadeInFromZero(200, Easing.OutQuint);
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)

View File

@ -37,6 +37,7 @@ namespace osu.Game.Overlays.BeatmapSet
private readonly OsuSpriteText title, artist;
private readonly AuthorInfo author;
private readonly ExplicitContentBeatmapPill explicitContentPill;
private readonly FeaturedArtistBeatmapPill featuredArtistPill;
private readonly FillFlowContainer downloadButtonsContainer;
private readonly BeatmapAvailability beatmapAvailability;
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),
Margin = new MarginPadding { Bottom = 20 }
Direction = FillDirection.Horizontal,
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
{
@ -233,6 +249,7 @@ namespace osu.Game.Overlays.BeatmapSet
artist.Text = new RomanisableString(setInfo.NewValue.Metadata.ArtistUnicode, setInfo.NewValue.Metadata.Artist);
explicitContentPill.Alpha = setInfo.NewValue.OnlineInfo.HasExplicitContent ? 1 : 0;
featuredArtistPill.Alpha = setInfo.NewValue.OnlineInfo.TrackId != null ? 1 : 0;
onlineStatusPill.FadeIn(500, Easing.OutQuint);
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 readonly BindableList<Mod> SelectedMods = new BindableList<Mod>();
public readonly BindableList<IMod> SelectedMods = new BindableList<IMod>();
public readonly Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
private readonly FillFlowContainer<ModButton> modsContainer;
@ -54,7 +54,7 @@ namespace osu.Game.Overlays.BeatmapSet
return;
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 =>
{
@ -76,7 +76,7 @@ namespace osu.Game.Overlays.BeatmapSet
updateHighlighted();
}
private void selectionChanged(Mod mod, bool selected)
private void selectionChanged(IMod mod, bool selected)
{
if (selected)
SelectedMods.Add(mod);
@ -101,9 +101,9 @@ namespace osu.Game.Overlays.BeatmapSet
private const int duration = 200;
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)
{
Scale = new Vector2(0.4f);

View File

@ -26,9 +26,6 @@ namespace osu.Game.Overlays.Changelog
private const float image_container_width = 164;
private const float heart_size = 75;
private readonly FillFlowContainer textContainer;
private readonly Container imageContainer;
public ChangelogSupporterPromo()
{
RelativeSizeAxes = Axes.X;
@ -38,6 +35,12 @@ namespace osu.Game.Overlays.Changelog
Vertical = 20,
Horizontal = 50,
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colour, TextureStore textures, OverlayColourProvider colourProvider)
{
SupporterPromoLinkFlowContainer supportLinkText;
InternalChildren = new Drawable[]
{
@ -59,7 +62,7 @@ namespace osu.Game.Overlays.Changelog
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black.Opacity(0.3f),
Colour = colourProvider.Background5,
},
new Container
{
@ -68,7 +71,7 @@ namespace osu.Game.Overlays.Changelog
Padding = new MarginPadding { Horizontal = 75 },
Children = new Drawable[]
{
textContainer = new FillFlowContainer
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@ -76,91 +79,84 @@ namespace osu.Game.Overlays.Changelog
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
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,
Width = image_container_width,
Anchor = 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.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!");
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
@ -170,27 +166,18 @@ namespace osu.Game.Overlays.Changelog
{
}
public new void AddLink(string text, string url, Action<SpriteText> creationParameters) =>
AddInternal(new SupporterPromoLinkCompiler(AddText(text, creationParameters)) { Url = url });
protected override DrawableLinkCompiler CreateLinkCompiler(IEnumerable<SpriteText> parts) => new SupporterPromoLinkCompiler(parts);
private class SupporterPromoLinkCompiler : DrawableLinkCompiler
{
[Resolved(CanBeNull = true)]
private OsuGame game { get; set; }
public string Url;
public SupporterPromoLinkCompiler(IEnumerable<Drawable> parts)
: base(parts)
{
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(OsuColour colour)
{
TooltipText = Url;
Action = () => game?.HandleLink(Url);
IdleColour = colour.PinkDark;
HoverColour = Color4.White;
}

View File

@ -107,9 +107,9 @@ namespace osu.Game.Overlays.Mods
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";
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,7 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using Newtonsoft.Json;
using osu.Framework.Graphics.Sprites;
namespace osu.Game.Rulesets.Mods
{
@ -11,7 +11,37 @@ namespace osu.Game.Rulesets.Mods
/// <summary>
/// The shortened name of this mod.
/// </summary>
[JsonProperty("acronym")]
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]
public abstract class Mod : IMod, IEquatable<Mod>, IDeepCloneable<Mod>
{
/// <summary>
/// The name of this mod.
/// </summary>
[JsonIgnore]
public abstract string Name { get; }
/// <summary>
/// The shortened name of this mod.
/// </summary>
public abstract string Acronym { get; }
/// <summary>
/// The icon of this mod.
/// </summary>
[JsonIgnore]
public virtual IconUsage? Icon => null;
/// <summary>
/// The type of this mod.
/// </summary>
[JsonIgnore]
public virtual ModType Type => ModType.Fun;
/// <summary>
/// The user readable description of this mod.
/// </summary>
[JsonIgnore]
public abstract string Description { get; }
@ -106,10 +91,6 @@ namespace osu.Game.Rulesets.Mods
[JsonIgnore]
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]
public virtual bool UserPlayable => true;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
@ -38,13 +39,58 @@ namespace osu.Game.Rulesets
{
public RulesetInfo RulesetInfo { get; internal set; }
public IEnumerable<Mod> GetAllMods() => 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 });
private static readonly ConcurrentDictionary<int, IMod[]> mod_reference_cache = new ConcurrentDictionary<int, IMod[]>();
/// <summary>
/// A queryable source containing all available mods.
/// Call <see cref="IMod.CreateInstance"/> for consumption purposes.
/// </summary>
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);
@ -126,7 +172,7 @@ namespace osu.Game.Rulesets
}
[CanBeNull]
public ModAutoplay GetAutoplayMod() => GetAllMods().OfType<ModAutoplay>().FirstOrDefault();
public ModAutoplay GetAutoplayMod() => CreateMod<ModAutoplay>();
public virtual ISkin CreateLegacySkinProvider([NotNull] ISkin skin, IBeatmap beatmap) => null;

View File

@ -30,12 +30,12 @@ namespace osu.Game.Rulesets.UI
private const float size = 80;
public virtual LocalisableString TooltipText => showTooltip ? mod.IconTooltip : null;
public virtual LocalisableString TooltipText => showTooltip ? ((mod as Mod)?.IconTooltip ?? mod.Name) : null;
private Mod mod;
private IMod mod;
private readonly bool showTooltip;
public Mod Mod
public IMod Mod
{
get => mod;
set
@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.UI
/// </summary>
/// <param name="mod">The mod to be displayed</param>
/// <param name="showTooltip">Whether a tooltip describing the mod should display on hover.</param>
public ModIcon(Mod mod, bool showTooltip = true)
public ModIcon(IMod mod, bool showTooltip = true)
{
this.mod = mod ?? throw new ArgumentNullException(nameof(mod));
this.showTooltip = showTooltip;
@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.UI
updateMod(mod);
}
private void updateMod(Mod value)
private void updateMod(IMod value)
{
modAcronym.Text = value.Acronym;
modIcon.Icon = value.Icon ?? FontAwesome.Solid.Question;

View File

@ -67,7 +67,7 @@ namespace osu.Game.Scoring.Legacy
// lazer replays get a really high version number.
if (version < LegacyScoreEncoder.FIRST_LAZER_VERSION)
scoreInfo.Mods = scoreInfo.Mods.Append(currentRuleset.GetAllMods().OfType<ModClassic>().Single()).ToArray();
scoreInfo.Mods = scoreInfo.Mods.Append(currentRuleset.CreateMod<ModClassic>()).ToArray();
currentBeatmap = workingBeatmap.GetPlayableBeatmap(currentRuleset.RulesetInfo, scoreInfo.Mods);
scoreInfo.Beatmap = currentBeatmap.BeatmapInfo;

View File

@ -737,10 +737,10 @@ namespace osu.Game.Screens.Edit
private DifficultyMenuItem createDifficultyMenuItem(BeatmapInfo beatmapInfo)
{
bool isCurrentDifficulty = playableBeatmap.BeatmapInfo.Equals(beatmapInfo);
return new DifficultyMenuItem(beatmapInfo, isCurrentDifficulty, switchToDifficulty);
return new DifficultyMenuItem(beatmapInfo, isCurrentDifficulty, SwitchToDifficulty);
}
private void switchToDifficulty(BeatmapInfo beatmapInfo) => loader?.ScheduleDifficultySwitch(beatmapInfo);
protected void SwitchToDifficulty(BeatmapInfo beatmapInfo) => loader?.ScheduleDifficultySwitch(beatmapInfo);
private void cancelExit() => loader?.CancelPendingDifficultySwitch();

View File

@ -34,6 +34,20 @@ namespace osu.Game.Screens.Edit
[CanBeNull]
private ScheduledDelegate scheduledDifficultySwitch;
[BackgroundDependencyLoader]
private void load()
{
AddRangeInternal(new Drawable[]
{
new LoadingSpinner(true)
{
State = { Value = Visibility.Visible },
}
});
}
protected virtual Editor CreateEditor() => new Editor(this);
protected override void LogoArriving(OsuLogo logo, bool resuming)
{
base.LogoArriving(logo, resuming);
@ -47,18 +61,6 @@ namespace osu.Game.Screens.Edit
}
}
[BackgroundDependencyLoader]
private void load()
{
AddRangeInternal(new Drawable[]
{
new LoadingSpinner(true)
{
State = { Value = Visibility.Visible },
}
});
}
public void ScheduleDifficultySwitch(BeatmapInfo beatmapInfo)
{
scheduledDifficultySwitch?.Cancel();
@ -81,7 +83,7 @@ namespace osu.Game.Screens.Edit
private void pushEditor()
{
this.Push(new Editor(this));
this.Push(CreateEditor());
ValidForResume = false;
}

View File

@ -25,6 +25,7 @@ namespace osu.Game.Screens.Edit.Setup
private LabelledSwitchButton widescreenSupport;
private LabelledSwitchButton epilepsyWarning;
private LabelledSwitchButton letterboxDuringBreaks;
private LabelledSwitchButton samplesMatchPlaybackRate;
public override LocalisableString Title => "Design";
@ -79,6 +80,12 @@ namespace osu.Game.Screens.Edit.Setup
Label = "Letterbox during breaks",
Description = "Adds horizontal letterboxing to give a cinematic look during breaks.",
Current = { Value = Beatmap.BeatmapInfo.LetterboxInBreaks }
},
samplesMatchPlaybackRate = new LabelledSwitchButton
{
Label = "Samples match playback rate",
Description = "When enabled, all samples will speed up or slow down when rate-changing mods are enabled.",
Current = { Value = Beatmap.BeatmapInfo.SamplesMatchPlaybackRate }
}
};
}
@ -96,6 +103,7 @@ namespace osu.Game.Screens.Edit.Setup
widescreenSupport.Current.BindValueChanged(_ => updateBeatmap());
epilepsyWarning.Current.BindValueChanged(_ => updateBeatmap());
letterboxDuringBreaks.Current.BindValueChanged(_ => updateBeatmap());
samplesMatchPlaybackRate.Current.BindValueChanged(_ => updateBeatmap());
}
private void updateCountdownSettingsVisibility() => CountdownSettings.FadeTo(EnableCountdown.Current.Value ? 1 : 0);
@ -115,6 +123,7 @@ namespace osu.Game.Screens.Edit.Setup
Beatmap.BeatmapInfo.WidescreenStoryboard = widescreenSupport.Current.Value;
Beatmap.BeatmapInfo.EpilepsyWarning = epilepsyWarning.Current.Value;
Beatmap.BeatmapInfo.LetterboxInBreaks = letterboxDuringBreaks.Current.Value;
Beatmap.BeatmapInfo.SamplesMatchPlaybackRate = samplesMatchPlaybackRate.Current.Value;
}
}
}

View File

@ -23,6 +23,8 @@ namespace osu.Game.Screens.Import
{
public override bool HideOverlaysOnEnter => true;
public override bool AllowTrackAdjustments => false;
private OsuFileSelector fileSelector;
private Container contentContainer;
private TextFlowContainer currentFileText;

View File

@ -115,7 +115,9 @@ namespace osu.Game.Screens.Menu
if (setInfo == null)
return false;
return (initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0])) != null;
initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]);
return UsingThemedIntro = initialBeatmap != null;
}
}
@ -165,7 +167,7 @@ namespace osu.Game.Screens.Menu
protected override BackgroundScreen CreateBackground() => new BackgroundScreenBlack();
protected void StartTrack()
protected virtual void StartTrack()
{
// Only start the current track if it is the menu music. A beatmap's track is started when entering the Main Menu.
if (UsingThemedIntro)
@ -184,7 +186,6 @@ namespace osu.Game.Screens.Menu
{
beatmap.Value = initialBeatmap;
Track = initialBeatmap.Track;
UsingThemedIntro = !initialBeatmap.Track.IsDummyDevice;
// ensure the track starts at maximum volume
musicController.CurrentTrack.FinishTransforms();

View File

@ -41,6 +41,9 @@ namespace osu.Game.Screens.Menu
private Sample welcome;
private DecoupleableInterpolatingFramedClock decoupledClock;
private TrianglesIntroSequence intro;
[BackgroundDependencyLoader]
private void load()
{
@ -56,10 +59,18 @@ namespace osu.Game.Screens.Menu
{
PrepareMenuLoad();
LoadComponentAsync(new TrianglesIntroSequence(logo, background)
decoupledClock = new DecoupleableInterpolatingFramedClock
{
IsCoupled = false
};
if (UsingThemedIntro)
decoupledClock.ChangeSource(Track);
LoadComponentAsync(intro = new TrianglesIntroSequence(logo, background)
{
RelativeSizeAxes = Axes.Both,
Clock = new FramedClock(UsingThemedIntro ? Track : null),
Clock = decoupledClock,
LoadMenu = LoadMenu
}, t =>
{
@ -72,12 +83,25 @@ namespace osu.Game.Screens.Menu
}
}
public override void OnSuspending(IScreen next)
{
base.OnSuspending(next);
// important as there is a clock attached to a track which will likely be disposed before returning to this screen.
intro.Expire();
}
public override void OnResuming(IScreen last)
{
base.OnResuming(last);
background.FadeOut(100);
}
protected override void StartTrack()
{
decoupledClock.Start();
}
private class TrianglesIntroSequence : CompositeDrawable
{
private readonly OsuLogo logo;

View File

@ -43,6 +43,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
private PasswordProtectedIcon passwordIcon;
private EndDateInfo endDateInfo;
private DelayedLoadWrapper wrapper;
public DrawableRoom(Room room)
{
Room = room;
@ -63,6 +65,14 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colours)
{
ButtonsContainer = new Container
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X
};
InternalChildren = new[]
{
// This resolves internal 1px gaps due to applying the (parenting) corner radius and masking across multiple filling background sprites.
@ -75,155 +85,153 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
d.RelativeSizeAxes = Axes.Both;
}),
new Container
{
Name = @"Room content",
RelativeSizeAxes = Axes.Both,
// This negative padding resolves 1px gaps between this background and the background above.
Padding = new MarginPadding { Left = 20, Vertical = -0.5f },
Child = new Container
wrapper = new DelayedLoadWrapper(() =>
new Container
{
Name = @"Room content",
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = CORNER_RADIUS,
Children = new Drawable[]
// This negative padding resolves 1px gaps between this background and the background above.
Padding = new MarginPadding { Left = 20, Vertical = -0.5f },
Child = new Container
{
new GridContainer
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = CORNER_RADIUS,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
new GridContainer
{
new Dimension(GridSizeMode.Relative, 0.2f)
},
Content = new[]
{
new Drawable[]
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Background5,
},
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f))
},
}
}
},
new Container
{
Name = @"Left details",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Left = 20,
Vertical = 5
},
Children = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Children = new Drawable[]
{
new RoomStatusPill
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
specialCategoryPill = new RoomSpecialCategoryPill
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
endDateInfo = new EndDateInfo
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
}
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 3 },
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new RoomNameText(),
new RoomHostText(),
}
}
},
new Dimension(GridSizeMode.Relative, 0.2f)
},
new FillFlowContainer
Content = new[]
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Children = new Drawable[]
new Drawable[]
{
new PlaylistCountPill
new Box
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Colour = colours.Background5,
},
new StarRatingRangeDisplay
new Box
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Scale = new Vector2(0.8f)
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientHorizontal(colours.Background5, colours.Background5.Opacity(0.3f))
},
}
}
},
new Container
{
Name = @"Left details",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Left = 20,
Vertical = 5
},
Children = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Children = new Drawable[]
{
new RoomStatusPill
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
specialCategoryPill = new RoomSpecialCategoryPill
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
endDateInfo = new EndDateInfo
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
}
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding { Top = 3 },
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new RoomNameText(),
new RoomHostText(),
}
}
},
},
new FillFlowContainer
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5),
Children = new Drawable[]
{
new PlaylistCountPill
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
new StarRatingRangeDisplay
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Scale = new Vector2(0.8f)
}
}
}
}
}
},
new FillFlowContainer
{
Name = "Right content",
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Spacing = new Vector2(5),
Padding = new MarginPadding
{
Right = 10,
Vertical = 20,
},
Children = new Drawable[]
new FillFlowContainer
{
ButtonsContainer = new Container
Name = "Right content",
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Spacing = new Vector2(5),
Padding = new MarginPadding
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X
Right = 10,
Vertical = 20,
},
recentParticipantsList = new RecentParticipantsList
Children = new Drawable[]
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
NumberOfCircles = NumberOfAvatars
ButtonsContainer,
recentParticipantsList = new RecentParticipantsList
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
NumberOfCircles = NumberOfAvatars
}
}
}
},
passwordIcon = new PasswordProtectedIcon { Alpha = 0 }
},
passwordIcon = new PasswordProtectedIcon { Alpha = 0 }
},
},
},
}, 0)
{
RelativeSizeAxes = Axes.Both,
}
};
}
@ -231,23 +239,28 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components
{
base.LoadComplete();
roomCategory.BindTo(Room.Category);
roomCategory.BindValueChanged(c =>
wrapper.DelayedLoadComplete += _ =>
{
if (c.NewValue == RoomCategory.Spotlight)
specialCategoryPill.Show();
else
specialCategoryPill.Hide();
}, true);
wrapper.FadeInFromZero(200);
roomType.BindTo(Room.Type);
roomType.BindValueChanged(t =>
{
endDateInfo.Alpha = t.NewValue == MatchType.Playlists ? 1 : 0;
}, true);
roomCategory.BindTo(Room.Category);
roomCategory.BindValueChanged(c =>
{
if (c.NewValue == RoomCategory.Spotlight)
specialCategoryPill.Show();
else
specialCategoryPill.Hide();
}, true);
hasPassword.BindTo(Room.HasPassword);
hasPassword.BindValueChanged(v => passwordIcon.Alpha = v.NewValue ? 1 : 0, true);
roomType.BindTo(Room.Type);
roomType.BindValueChanged(t =>
{
endDateInfo.Alpha = t.NewValue == MatchType.Playlists ? 1 : 0;
}, true);
hasPassword.BindTo(Room.HasPassword);
hasPassword.BindValueChanged(v => passwordIcon.Alpha = v.NewValue ? 1 : 0, true);
};
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)

View File

@ -85,12 +85,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
{
base.LoadComplete();
if (matchingFilter)
this.FadeInFromZero(transition_duration);
else
Alpha = 0;
Alpha = matchingFilter ? 1 : 0;
selectionBox.Alpha = SelectedRoom.Value == Room ? 1 : 0;
SelectedRoom.BindValueChanged(updateSelectedRoom, true);
SelectedRoom.BindValueChanged(updateSelectedRoom);
}
private void updateSelectedRoom(ValueChangedEvent<Room> selected)

View File

@ -3,6 +3,8 @@
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@ -22,6 +24,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
private Drawable box;
private Sample sampleTeamSwap;
[Resolved]
private OsuColour colours { get; set; }
@ -39,7 +43,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
}
[BackgroundDependencyLoader]
private void load()
private void load(AudioManager audio)
{
box = new Container
{
@ -72,6 +76,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{
InternalChild = box;
}
sampleTeamSwap = audio.Samples.Get(@"Multiplayer/team-swap");
}
private void changeTeam()
@ -99,6 +105,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
if (newTeam == displayedTeam)
return;
// only play the sample if an already valid team changes to another valid team.
// this avoids playing a sound for each user if the match type is changed to/from a team mode.
if (newTeam != null && displayedTeam != null)
sampleTeamSwap?.Play();
displayedTeam = newTeam;
if (displayedTeam != null)

View File

@ -24,6 +24,8 @@ namespace osu.Game.Screens.OnlinePlay
[Cached]
protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
public override bool AllowTrackAdjustments => false;
public override bool CursorVisible => (screenStack?.CurrentScreen as IOnlinePlaySubScreen)?.CursorVisible ?? true;
// this is required due to PlayerLoader eventually being pushed to the main stack

View File

@ -33,6 +33,8 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
private const float chevron_size = 8;
private SpriteIcon arrow;
private SpriteIcon iconEarly;
private SpriteIcon iconLate;
private Container colourBarsEarly;
private Container colourBarsLate;
@ -97,25 +99,21 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
},
new SpriteIcon
iconEarly = new SpriteIcon
{
Y = -10,
Size = new Vector2(10),
Icon = FontAwesome.Solid.ShippingFast,
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
// undo any layout rotation to display the icon the correct orientation
Rotation = -Rotation,
},
new SpriteIcon
iconLate = new SpriteIcon
{
Y = 10,
Size = new Vector2(10),
Icon = FontAwesome.Solid.Bicycle,
Anchor = Anchor.BottomCentre,
Origin = Anchor.Centre,
// undo any layout rotation to display the icon the correct orientation
Rotation = -Rotation,
}
}
},
@ -143,6 +141,15 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters
arrow.Delay(200).FadeInFromZero(600);
}
protected override void Update()
{
base.Update();
// undo any layout rotation to display icons in the correct orientation
iconEarly.Rotation = -Rotation;
iconLate.Rotation = -Rotation;
}
private void createColourBars(OsuColour colours)
{
var windows = HitWindows.GetAllAvailableWindows().ToArray();

View File

@ -7,6 +7,7 @@ using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -306,18 +307,18 @@ namespace osu.Game.Screens.Ranking
if (expandedPanel == null)
return base.OnKeyDown(e);
var expandedPanelIndex = flow.GetPanelIndex(expandedPanel.Score);
switch (e.Key)
{
case Key.Left:
if (expandedPanelIndex > 0)
SelectedScore.Value = flow.Children[expandedPanelIndex - 1].Panel.Score;
var previousScore = flow.GetPreviousScore(expandedPanel.Score);
if (previousScore != null)
SelectedScore.Value = previousScore;
return true;
case Key.Right:
if (expandedPanelIndex < flow.Count - 1)
SelectedScore.Value = flow.Children[expandedPanelIndex + 1].Panel.Score;
var nextScore = flow.GetNextScore(expandedPanel.Score);
if (nextScore != null)
SelectedScore.Value = nextScore;
return true;
}
@ -336,6 +337,12 @@ namespace osu.Game.Screens.Ranking
public int GetPanelIndex(ScoreInfo score) => applySorting(Children).TakeWhile(s => s.Panel.Score != score).Count();
[CanBeNull]
public ScoreInfo GetPreviousScore(ScoreInfo score) => applySorting(Children).TakeWhile(s => s.Panel.Score != score).LastOrDefault()?.Panel.Score;
[CanBeNull]
public ScoreInfo GetNextScore(ScoreInfo score) => applySorting(Children).SkipWhile(s => s.Panel.Score != score).ElementAtOrDefault(1)?.Panel.Score;
private IEnumerable<ScorePanelTrackingContainer> applySorting(IEnumerable<Drawable> drawables) => drawables.OfType<ScorePanelTrackingContainer>()
.OrderByDescending(GetLayoutPosition)
.ThenBy(s => s.Panel.Score.OnlineScoreID);

View File

@ -34,7 +34,7 @@ namespace osu.Game.Tests.Beatmaps
protected void TestToLegacy(LegacyMods expectedLegacyMods, Type[] providedModTypes)
{
var ruleset = CreateRuleset();
var modInstances = ruleset.GetAllMods()
var modInstances = ruleset.CreateAllMods()
.Where(mod => providedModTypes.Contains(mod.GetType()))
.ToArray();
var actualLegacyMods = ruleset.ConvertToLegacyMods(modInstances);

View File

@ -28,7 +28,7 @@ namespace osu.Game.Tests
RulesetID = ruleset.ID ?? 0;
Mods = excessMods
? ruleset.CreateInstance().GetAllMods().ToArray()
? ruleset.CreateInstance().CreateAllMods().ToArray()
: new Mod[] { new TestModHardRock(), new TestModDoubleTime() };
TotalScore = 2845370;

View File

@ -16,26 +16,38 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Edit;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Screens.Menu;
using osu.Game.Skinning;
namespace osu.Game.Tests.Visual
{
public abstract class EditorTestScene : ScreenTestScene
{
protected EditorBeatmap EditorBeatmap;
private TestEditorLoader editorLoader;
protected TestEditor Editor { get; private set; }
protected TestEditor Editor => editorLoader.Editor;
protected EditorClock EditorClock { get; private set; }
protected EditorBeatmap EditorBeatmap => Editor.ChildrenOfType<EditorBeatmap>().Single();
protected EditorClock EditorClock => Editor.ChildrenOfType<EditorClock>().Single();
/// <summary>
/// Whether any saves performed by the editor should be isolate (and not persist) to the underlying <see cref="BeatmapManager"/>.
/// </summary>
protected virtual bool IsolateSavingFromDatabase => true;
// required for screen transitions to work properly
// (see comment in EditorLoader.LogoArriving).
[Cached]
private OsuLogo logo = new OsuLogo
{
Alpha = 0
};
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio, RulesetStore rulesets)
{
Add(logo);
var working = CreateWorkingBeatmap(Ruleset.Value);
Beatmap.Value = working;
@ -53,13 +65,11 @@ namespace osu.Game.Tests.Visual
AddStep("load editor", LoadEditor);
AddUntilStep("wait for editor to load", () => EditorComponentsReady);
AddStep("get beatmap", () => EditorBeatmap = Editor.ChildrenOfType<EditorBeatmap>().Single());
AddStep("get clock", () => EditorClock = Editor.ChildrenOfType<EditorClock>().Single());
}
protected virtual void LoadEditor()
{
LoadScreen(Editor = CreateEditor());
LoadScreen(editorLoader = new TestEditorLoader());
}
/// <summary>
@ -70,7 +80,14 @@ namespace osu.Game.Tests.Visual
protected sealed override Ruleset CreateRuleset() => CreateEditorRuleset();
protected virtual TestEditor CreateEditor() => new TestEditor();
protected class TestEditorLoader : EditorLoader
{
public TestEditor Editor { get; private set; }
protected sealed override Editor CreateEditor() => Editor = CreateTestEditor(this);
protected virtual TestEditor CreateTestEditor(EditorLoader loader) => new TestEditor(loader);
}
protected class TestEditor : Editor
{
@ -86,7 +103,14 @@ namespace osu.Game.Tests.Visual
public new void Paste() => base.Paste();
public new void SwitchToDifficulty(BeatmapInfo beatmapInfo) => base.SwitchToDifficulty(beatmapInfo);
public new bool HasUnsavedChanges => base.HasUnsavedChanges;
public TestEditor(EditorLoader loader = null)
: base(loader)
{
}
}
private class TestBeatmapManager : BeatmapManager

View File

@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual
if (!AllowFail)
{
var noFailMod = ruleset.GetAllMods().FirstOrDefault(m => m is ModNoFail);
var noFailMod = ruleset.CreateMod<ModNoFail>();
if (noFailMod != null)
SelectedMods.Value = new[] { noFailMod };
}

View File

@ -44,7 +44,7 @@ namespace osu.Game.Users.Drawables
protected override double LoadDelay => 200;
private readonly bool openOnClick;
private readonly bool isInteractive;
private readonly bool showUsernameTooltip;
private readonly bool showGuestOnNull;
@ -52,12 +52,12 @@ namespace osu.Game.Users.Drawables
/// Construct a new UpdateableAvatar.
/// </summary>
/// <param name="user">The initial user to display.</param>
/// <param name="openOnClick">Whether to open the user's profile when clicked.</param>
/// <param name="showUsernameTooltip">Whether to show the username rather than "view profile" on the tooltip.</param>
/// <param name="isInteractive">If set to true, hover/click sounds will play and clicking the avatar will open the user's profile.</param>
/// <param name="showUsernameTooltip">Whether to show the username rather than "view profile" on the tooltip. (note: this only applies if <paramref name="isInteractive"/> is also true)</param>
/// <param name="showGuestOnNull">Whether to show a default guest representation on null user (as opposed to nothing).</param>
public UpdateableAvatar(User user = null, bool openOnClick = true, bool showUsernameTooltip = false, bool showGuestOnNull = true)
public UpdateableAvatar(User user = null, bool isInteractive = true, bool showUsernameTooltip = false, bool showGuestOnNull = true)
{
this.openOnClick = openOnClick;
this.isInteractive = isInteractive;
this.showUsernameTooltip = showUsernameTooltip;
this.showGuestOnNull = showGuestOnNull;
@ -69,14 +69,22 @@ namespace osu.Game.Users.Drawables
if (user == null && !showGuestOnNull)
return null;
var avatar = new ClickableAvatar(user)
if (isInteractive)
{
OpenOnClick = openOnClick,
ShowUsernameTooltip = showUsernameTooltip,
RelativeSizeAxes = Axes.Both,
};
return avatar;
return new ClickableAvatar(user)
{
OpenOnClick = true,
ShowUsernameTooltip = showUsernameTooltip,
RelativeSizeAxes = Axes.Both,
};
}
else
{
return new DrawableAvatar(user)
{
RelativeSizeAxes = Axes.Both,
};
}
}
}
}

View File

@ -37,7 +37,7 @@
</PackageReference>
<PackageReference Include="Realm" Version="10.3.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.907.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.907.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.913.0" />
<PackageReference Include="Sentry" Version="3.9.0" />
<PackageReference Include="SharpCompress" Version="0.28.3" />
<PackageReference Include="NUnit" Version="3.13.2" />

View File

@ -71,7 +71,7 @@
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.907.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.907.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.913.0" />
</ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
<PropertyGroup>