1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-07 00:27:26 +08:00

Merge branch 'master' into fl-skill

This commit is contained in:
smoogipoo 2021-09-13 16:09:24 +09:00
commit bf4ca7f489
40 changed files with 932 additions and 174 deletions

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.907.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2021.913.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.907.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2021.907.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -51,6 +51,24 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("room join password correct", () => lastJoinedPassword == null); AddAssert("room join password correct", () => lastJoinedPassword == null);
} }
[Test]
public void TestPopoverHidesOnBackButton()
{
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().Any());
AddAssert("textbox has focus", () => InputManager.FocusedDrawable is OsuPasswordTextBox);
AddStep("hit escape", () => InputManager.Key(Key.Escape));
AddAssert("textbox lost focus", () => InputManager.FocusedDrawable is SearchTextBox);
AddStep("hit escape", () => InputManager.Key(Key.Escape));
AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().Any());
}
[Test] [Test]
public void TestPopoverHidesOnLeavingScreen() public void TestPopoverHidesOnLeavingScreen()
{ {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,6 +13,21 @@ namespace osu.Game.Rulesets.Mods
/// </summary> /// </summary>
string Acronym { get; } string Acronym { get; }
/// <summary>
/// The name of this mod.
/// </summary>
string Name { get; }
/// <summary>
/// The user readable description of this mod.
/// </summary>
string Description { get; }
/// <summary>
/// The type of this mod.
/// </summary>
ModType Type { get; }
/// <summary> /// <summary>
/// The icon of this mod. /// The icon of this mod.
/// </summary> /// </summary>

View File

@ -22,29 +22,17 @@ namespace osu.Game.Rulesets.Mods
[ExcludeFromDynamicCompile] [ExcludeFromDynamicCompile]
public abstract class Mod : IMod, IEquatable<Mod>, IDeepCloneable<Mod> public abstract class Mod : IMod, IEquatable<Mod>, IDeepCloneable<Mod>
{ {
/// <summary>
/// The name of this mod.
/// </summary>
[JsonIgnore] [JsonIgnore]
public abstract string Name { get; } public abstract string Name { get; }
/// <summary>
/// The shortened name of this mod.
/// </summary>
public abstract string Acronym { get; } public abstract string Acronym { get; }
[JsonIgnore] [JsonIgnore]
public virtual IconUsage? Icon => null; public virtual IconUsage? Icon => null;
/// <summary>
/// The type of this mod.
/// </summary>
[JsonIgnore] [JsonIgnore]
public virtual ModType Type => ModType.Fun; public virtual ModType Type => ModType.Fun;
/// <summary>
/// The user readable description of this mod.
/// </summary>
[JsonIgnore] [JsonIgnore]
public abstract string Description { get; } public abstract string Description { get; }

View File

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

View File

@ -737,10 +737,10 @@ namespace osu.Game.Screens.Edit
private DifficultyMenuItem createDifficultyMenuItem(BeatmapInfo beatmapInfo) private DifficultyMenuItem createDifficultyMenuItem(BeatmapInfo beatmapInfo)
{ {
bool isCurrentDifficulty = playableBeatmap.BeatmapInfo.Equals(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(); private void cancelExit() => loader?.CancelPendingDifficultySwitch();

View File

@ -34,6 +34,20 @@ namespace osu.Game.Screens.Edit
[CanBeNull] [CanBeNull]
private ScheduledDelegate scheduledDifficultySwitch; 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) protected override void LogoArriving(OsuLogo logo, bool resuming)
{ {
base.LogoArriving(logo, 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) public void ScheduleDifficultySwitch(BeatmapInfo beatmapInfo)
{ {
scheduledDifficultySwitch?.Cancel(); scheduledDifficultySwitch?.Cancel();
@ -81,7 +83,7 @@ namespace osu.Game.Screens.Edit
private void pushEditor() private void pushEditor()
{ {
this.Push(new Editor(this)); this.Push(CreateEditor());
ValidForResume = false; ValidForResume = false;
} }

View File

@ -25,6 +25,7 @@ namespace osu.Game.Screens.Edit.Setup
private LabelledSwitchButton widescreenSupport; private LabelledSwitchButton widescreenSupport;
private LabelledSwitchButton epilepsyWarning; private LabelledSwitchButton epilepsyWarning;
private LabelledSwitchButton letterboxDuringBreaks; private LabelledSwitchButton letterboxDuringBreaks;
private LabelledSwitchButton samplesMatchPlaybackRate;
public override LocalisableString Title => "Design"; public override LocalisableString Title => "Design";
@ -79,6 +80,12 @@ namespace osu.Game.Screens.Edit.Setup
Label = "Letterbox during breaks", Label = "Letterbox during breaks",
Description = "Adds horizontal letterboxing to give a cinematic look during breaks.", Description = "Adds horizontal letterboxing to give a cinematic look during breaks.",
Current = { Value = Beatmap.BeatmapInfo.LetterboxInBreaks } 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()); widescreenSupport.Current.BindValueChanged(_ => updateBeatmap());
epilepsyWarning.Current.BindValueChanged(_ => updateBeatmap()); epilepsyWarning.Current.BindValueChanged(_ => updateBeatmap());
letterboxDuringBreaks.Current.BindValueChanged(_ => updateBeatmap()); letterboxDuringBreaks.Current.BindValueChanged(_ => updateBeatmap());
samplesMatchPlaybackRate.Current.BindValueChanged(_ => updateBeatmap());
} }
private void updateCountdownSettingsVisibility() => CountdownSettings.FadeTo(EnableCountdown.Current.Value ? 1 : 0); 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.WidescreenStoryboard = widescreenSupport.Current.Value;
Beatmap.BeatmapInfo.EpilepsyWarning = epilepsyWarning.Current.Value; Beatmap.BeatmapInfo.EpilepsyWarning = epilepsyWarning.Current.Value;
Beatmap.BeatmapInfo.LetterboxInBreaks = letterboxDuringBreaks.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 HideOverlaysOnEnter => true;
public override bool AllowTrackAdjustments => false;
private OsuFileSelector fileSelector; private OsuFileSelector fileSelector;
private Container contentContainer; private Container contentContainer;
private TextFlowContainer currentFileText; private TextFlowContainer currentFileText;

View File

@ -42,6 +42,7 @@ namespace osu.Game.Screens.Menu
private Sample welcome; private Sample welcome;
private DecoupleableInterpolatingFramedClock decoupledClock; private DecoupleableInterpolatingFramedClock decoupledClock;
private TrianglesIntroSequence intro;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
@ -66,7 +67,7 @@ namespace osu.Game.Screens.Menu
if (UsingThemedIntro) if (UsingThemedIntro)
decoupledClock.ChangeSource(Track); decoupledClock.ChangeSource(Track);
LoadComponentAsync(new TrianglesIntroSequence(logo, background) LoadComponentAsync(intro = new TrianglesIntroSequence(logo, background)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Clock = decoupledClock, Clock = decoupledClock,
@ -82,6 +83,14 @@ 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) public override void OnResuming(IScreen last)
{ {
base.OnResuming(last); base.OnResuming(last);

View File

@ -3,6 +3,8 @@
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -22,6 +24,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
private Drawable box; private Drawable box;
private Sample sampleTeamSwap;
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
@ -39,7 +43,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load(AudioManager audio)
{ {
box = new Container box = new Container
{ {
@ -72,6 +76,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{ {
InternalChild = box; InternalChild = box;
} }
sampleTeamSwap = audio.Samples.Get(@"Multiplayer/team-swap");
} }
private void changeTeam() private void changeTeam()
@ -99,6 +105,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
if (newTeam == displayedTeam) if (newTeam == displayedTeam)
return; 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; displayedTeam = newTeam;
if (displayedTeam != null) if (displayedTeam != null)

View File

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

View File

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

View File

@ -44,7 +44,7 @@ namespace osu.Game.Users.Drawables
protected override double LoadDelay => 200; protected override double LoadDelay => 200;
private readonly bool openOnClick; private readonly bool isInteractive;
private readonly bool showUsernameTooltip; private readonly bool showUsernameTooltip;
private readonly bool showGuestOnNull; private readonly bool showGuestOnNull;
@ -52,12 +52,12 @@ namespace osu.Game.Users.Drawables
/// Construct a new UpdateableAvatar. /// Construct a new UpdateableAvatar.
/// </summary> /// </summary>
/// <param name="user">The initial user to display.</param> /// <param name="user">The initial user to display.</param>
/// <param name="openOnClick">Whether to open the user's profile when clicked.</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.</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> /// <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.showUsernameTooltip = showUsernameTooltip;
this.showGuestOnNull = showGuestOnNull; this.showGuestOnNull = showGuestOnNull;
@ -69,14 +69,22 @@ namespace osu.Game.Users.Drawables
if (user == null && !showGuestOnNull) if (user == null && !showGuestOnNull)
return null; return null;
var avatar = new ClickableAvatar(user) if (isInteractive)
{ {
OpenOnClick = openOnClick, return new ClickableAvatar(user)
ShowUsernameTooltip = showUsernameTooltip, {
RelativeSizeAxes = Axes.Both, OpenOnClick = true,
}; ShowUsernameTooltip = showUsernameTooltip,
RelativeSizeAxes = Axes.Both,
return avatar; };
}
else
{
return new DrawableAvatar(user)
{
RelativeSizeAxes = Axes.Both,
};
}
} }
} }
} }

View File

@ -37,7 +37,7 @@
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="10.3.0" /> <PackageReference Include="Realm" Version="10.3.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.907.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="Sentry" Version="3.9.0" />
<PackageReference Include="SharpCompress" Version="0.28.3" /> <PackageReference Include="SharpCompress" Version="0.28.3" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />

View File

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