diff --git a/osu-framework b/osu-framework index 825505e788..5f2d5a57e5 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit 825505e788c4f093b269c61b485d38d50cd68096 +Subproject commit 5f2d5a57e5d506d7e5d87eeeb442adf10be061f6 diff --git a/osu.Desktop.Deploy/Program.cs b/osu.Desktop.Deploy/Program.cs index 37776b329f..644987ce5a 100644 --- a/osu.Desktop.Deploy/Program.cs +++ b/osu.Desktop.Deploy/Program.cs @@ -17,8 +17,8 @@ namespace osu.Desktop.Deploy { internal static class Program { - private const string nuget_path = @"packages\NuGet.CommandLine.3.5.0\tools\NuGet.exe"; - private const string squirrel_path = @"packages\squirrel.windows.1.5.2\tools\Squirrel.exe"; + private const string nuget_path = @"packages\NuGet.CommandLine.4.1.0\tools\NuGet.exe"; + private const string squirrel_path = @"packages\squirrel.windows.1.7.5\tools\Squirrel.exe"; private const string msbuild_path = @"C:\Program Files (x86)\MSBuild\14.0\Bin\MSBuild.exe"; public static string StagingFolder = ConfigurationManager.AppSettings["StagingFolder"]; diff --git a/osu.Desktop.Tests/Visual/OsuTestCase.cs b/osu.Desktop.Tests/Visual/OsuTestCase.cs index e54f7dbeb5..1b48ded4e2 100644 --- a/osu.Desktop.Tests/Visual/OsuTestCase.cs +++ b/osu.Desktop.Tests/Visual/OsuTestCase.cs @@ -1,20 +1,17 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using NUnit.Framework; using osu.Framework.Desktop.Platform; using osu.Framework.Testing; using osu.Game; namespace osu.Desktop.Tests.Visual { - [TestFixture] public abstract class OsuTestCase : TestCase { - [Test] public override void RunTest() { - using (var host = new HeadlessGameHost()) + using (var host = new HeadlessGameHost(realtime: false)) host.Run(new OsuTestCaseTestRunner(this)); } diff --git a/osu.Desktop.Tests/Visual/TestCaseContextMenu.cs b/osu.Desktop.Tests/Visual/TestCaseContextMenu.cs index f0894f794a..0d9c5e7ed1 100644 --- a/osu.Desktop.Tests/Visual/TestCaseContextMenu.cs +++ b/osu.Desktop.Tests/Visual/TestCaseContextMenu.cs @@ -69,28 +69,28 @@ namespace osu.Desktop.Tests.Visual private class MyContextMenuContainer : Container, IHasContextMenu { - public ContextMenuItem[] ContextMenuItems => new ContextMenuItem[] + public MenuItem[] ContextMenuItems => new MenuItem[] { - new OsuContextMenuItem(@"Some option"), - new OsuContextMenuItem(@"Highlighted option", MenuItemType.Highlighted), - new OsuContextMenuItem(@"Another option"), - new OsuContextMenuItem(@"Choose me please"), - new OsuContextMenuItem(@"And me too"), - new OsuContextMenuItem(@"Trying to fill"), - new OsuContextMenuItem(@"Destructive option", MenuItemType.Destructive), + new OsuMenuItem(@"Some option"), + new OsuMenuItem(@"Highlighted option", MenuItemType.Highlighted), + new OsuMenuItem(@"Another option"), + new OsuMenuItem(@"Choose me please"), + new OsuMenuItem(@"And me too"), + new OsuMenuItem(@"Trying to fill"), + new OsuMenuItem(@"Destructive option", MenuItemType.Destructive), }; } private class AnotherContextMenuContainer : Container, IHasContextMenu { - public ContextMenuItem[] ContextMenuItems => new ContextMenuItem[] + public MenuItem[] ContextMenuItems => new MenuItem[] { - new OsuContextMenuItem(@"Simple option"), - new OsuContextMenuItem(@"Simple very very long option"), - new OsuContextMenuItem(@"Change width", MenuItemType.Highlighted) { Action = () => this.ResizeWidthTo(Width * 2, 100, Easing.OutQuint) }, - new OsuContextMenuItem(@"Change height", MenuItemType.Highlighted) { Action = () => this.ResizeHeightTo(Height * 2, 100, Easing.OutQuint) }, - new OsuContextMenuItem(@"Change width back", MenuItemType.Destructive) { Action = () => this.ResizeWidthTo(Width / 2, 100, Easing.OutQuint) }, - new OsuContextMenuItem(@"Change height back", MenuItemType.Destructive) { Action = () => this.ResizeHeightTo(Height / 2, 100, Easing.OutQuint) }, + new OsuMenuItem(@"Simple option"), + new OsuMenuItem(@"Simple very very long option"), + new OsuMenuItem(@"Change width", MenuItemType.Highlighted, () => this.ResizeWidthTo(Width * 2, 100, Easing.OutQuint)), + new OsuMenuItem(@"Change height", MenuItemType.Highlighted, () => this.ResizeHeightTo(Height * 2, 100, Easing.OutQuint)), + new OsuMenuItem(@"Change width back", MenuItemType.Destructive, () => this.ResizeWidthTo(Width / 2, 100, Easing.OutQuint)), + new OsuMenuItem(@"Change height back", MenuItemType.Destructive, () => this.ResizeHeightTo(Height / 2, 100, Easing.OutQuint)), }; } } diff --git a/osu.Desktop.Tests/Visual/TestCaseEditorMenuBar.cs b/osu.Desktop.Tests/Visual/TestCaseEditorMenuBar.cs new file mode 100644 index 0000000000..9cb3053ff2 --- /dev/null +++ b/osu.Desktop.Tests/Visual/TestCaseEditorMenuBar.cs @@ -0,0 +1,84 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Edit.Menus; + +namespace osu.Desktop.Tests.Visual +{ + public class TestCaseEditorMenuBar : OsuTestCase + { + public TestCaseEditorMenuBar() + { + Add(new EditorMenuBar + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Y = 50, + Items = new[] + { + new EditorMenuBarItem("File") + { + Items = new[] + { + new EditorMenuItem("Clear All Notes"), + new EditorMenuItem("Open Difficulty..."), + new EditorMenuItem("Save"), + new EditorMenuItem("Create a new Difficulty..."), + new EditorMenuItemSpacer(), + new EditorMenuItem("Revert to Saved"), + new EditorMenuItem("Revert to Saved (Full)"), + new EditorMenuItemSpacer(), + new EditorMenuItem("Test Beatmap"), + new EditorMenuItem("Open AiMod"), + new EditorMenuItemSpacer(), + new EditorMenuItem("Upload Beatmap..."), + new EditorMenuItem("Export Package"), + new EditorMenuItem("Export Map Package"), + new EditorMenuItem("Import from..."), + new EditorMenuItemSpacer(), + new EditorMenuItem("Open Song Folder"), + new EditorMenuItem("Open .osu in Notepad"), + new EditorMenuItem("Open .osb in Notepad"), + new EditorMenuItemSpacer(), + new EditorMenuItem("Exit"), + } + }, + new EditorMenuBarItem("Timing") + { + Items = new[] + { + new EditorMenuItem("Time Signature"), + new EditorMenuItem("Metronome Clicks"), + new EditorMenuItemSpacer(), + new EditorMenuItem("Add Timing Section"), + new EditorMenuItem("Add Inheriting Section"), + new EditorMenuItem("Reset Current Section"), + new EditorMenuItem("Delete Timing Section"), + new EditorMenuItem("Resnap Current Section"), + new EditorMenuItemSpacer(), + new EditorMenuItem("Timing Setup"), + new EditorMenuItemSpacer(), + new EditorMenuItem("Resnap All Notes", MenuItemType.Destructive), + new EditorMenuItem("Move all notes in time...", MenuItemType.Destructive), + new EditorMenuItem("Recalculate Slider Lengths", MenuItemType.Destructive), + new EditorMenuItem("Delete All Timing Sections", MenuItemType.Destructive), + new EditorMenuItemSpacer(), + new EditorMenuItem("Set Current Position as Preview Point"), + } + }, + new EditorMenuBarItem("Testing") + { + Items = new[] + { + new EditorMenuItem("Item 1"), + new EditorMenuItem("Item 2"), + new EditorMenuItem("Item 3"), + } + }, + } + }); + } + } +} diff --git a/osu.Desktop.Tests/Visual/TestCaseKeyCounter.cs b/osu.Desktop.Tests/Visual/TestCaseKeyCounter.cs index 1861a77a3a..ffe37c83a6 100644 --- a/osu.Desktop.Tests/Visual/TestCaseKeyCounter.cs +++ b/osu.Desktop.Tests/Visual/TestCaseKeyCounter.cs @@ -2,12 +2,8 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; using osu.Framework.MathUtils; using osu.Game.Screens.Play; -using OpenTK; -using OpenTK.Graphics; using OpenTK.Input; namespace osu.Desktop.Tests.Visual @@ -41,38 +37,5 @@ namespace osu.Desktop.Tests.Visual Add(kc); } - private class TestSliderBar : SliderBar where T : struct - { - public Color4 Color - { - get { return Box.Colour; } - set { Box.Colour = value; } - } - - public Color4 SelectionColor - { - get { return SelectionBox.Colour; } - set { SelectionBox.Colour = value; } - } - - protected readonly Box SelectionBox; - protected readonly Box Box; - - public TestSliderBar() - { - Children = new Drawable[] - { - Box = new Box { RelativeSizeAxes = Axes.Both }, - SelectionBox = new Box { RelativeSizeAxes = Axes.Both } - }; - } - - protected override void UpdateValue(float value) - { - SelectionBox.ScaleTo( - new Vector2(value, 1), - 300, Easing.OutQuint); - } - } } } diff --git a/osu.Desktop.Tests/Visual/TestCaseManiaHitObjects.cs b/osu.Desktop.Tests/Visual/TestCaseManiaHitObjects.cs index 7dcc48c778..76235bbf19 100644 --- a/osu.Desktop.Tests/Visual/TestCaseManiaHitObjects.cs +++ b/osu.Desktop.Tests/Visual/TestCaseManiaHitObjects.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; using OpenTK; @@ -40,8 +41,8 @@ namespace osu.Desktop.Tests.Visual RelativeChildSize = new Vector2(1, 10000), Children = new[] { - new DrawableNote(new Note { StartTime = 5000 }) { AccentColour = Color4.Red }, - new DrawableNote(new Note { StartTime = 6000 }) { AccentColour = Color4.Red } + new DrawableNote(new Note { StartTime = 5000 }, ManiaAction.Key1) { AccentColour = Color4.Red }, + new DrawableNote(new Note { StartTime = 6000 }, ManiaAction.Key1) { AccentColour = Color4.Red } } } } @@ -66,7 +67,7 @@ namespace osu.Desktop.Tests.Visual { StartTime = 5000, Duration = 1000 - }) { AccentColour = Color4.Red } + }, ManiaAction.Key1) { AccentColour = Color4.Red } } } } diff --git a/osu.Desktop.Tests/Visual/TestCaseManiaPlayfield.cs b/osu.Desktop.Tests/Visual/TestCaseManiaPlayfield.cs index ed0e5d81e9..fb14bdb3bf 100644 --- a/osu.Desktop.Tests/Visual/TestCaseManiaPlayfield.cs +++ b/osu.Desktop.Tests/Visual/TestCaseManiaPlayfield.cs @@ -1,111 +1,34 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; using System.Linq; -using osu.Framework.Configuration; +using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; -using osu.Framework.Input; using osu.Framework.Timing; +using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Timing; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Timing; -using OpenTK; -using OpenTK.Input; +using osu.Game.Rulesets; namespace osu.Desktop.Tests.Visual { internal class TestCaseManiaPlayfield : OsuTestCase { + private const double start_time = 500; + private const double duration = 500; + public override string Description => @"Mania playfield"; protected override double TimePerAction => 200; + private RulesetInfo maniaRuleset; + public TestCaseManiaPlayfield() { - Action createPlayfield = (cols, pos) => - { - Clear(); - Add(new ManiaPlayfield(cols) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - SpecialColumnPosition = pos, - Scale = new Vector2(1, -1) - }); - }; - - const double start_time = 500; - const double duration = 500; - - Func createTimingChange = (time, gravity) => new ManiaSpeedAdjustmentContainer(new MultiplierControlPoint(time) - { - TimingPoint = { BeatLength = 1000 } - }, gravity ? ScrollingAlgorithm.Gravity : ScrollingAlgorithm.Basic); - - Action createPlayfieldWithNotes = gravity => - { - Clear(); - - var rateAdjustClock = new StopwatchClock(true) { Rate = 1 }; - - ManiaPlayfield playField; - Add(playField = new ManiaPlayfield(4) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(1, -1), - Clock = new FramedClock(rateAdjustClock) - }); - - if (!gravity) - playField.Columns.ForEach(c => c.Add(createTimingChange(0, false))); - - for (double t = start_time; t <= start_time + duration; t += 100) - { - if (gravity) - playField.Columns.ElementAt(0).Add(createTimingChange(t, true)); - - playField.Add(new DrawableNote(new Note - { - StartTime = t, - Column = 0 - }, new Bindable(Key.D))); - - if (gravity) - playField.Columns.ElementAt(3).Add(createTimingChange(t, true)); - - playField.Add(new DrawableNote(new Note - { - StartTime = t, - Column = 3 - }, new Bindable(Key.K))); - } - - if (gravity) - playField.Columns.ElementAt(1).Add(createTimingChange(start_time, true)); - - playField.Add(new DrawableHoldNote(new HoldNote - { - StartTime = start_time, - Duration = duration, - Column = 1 - }, new Bindable(Key.F))); - - if (gravity) - playField.Columns.ElementAt(2).Add(createTimingChange(start_time, true)); - - playField.Add(new DrawableHoldNote(new HoldNote - { - StartTime = start_time, - Duration = duration, - Column = 2 - }, new Bindable(Key.J))); - }; - AddStep("1 column", () => createPlayfield(1, SpecialColumnPosition.Normal)); AddStep("4 columns", () => createPlayfield(4, SpecialColumnPosition.Normal)); AddStep("Left special style", () => createPlayfield(4, SpecialColumnPosition.Left)); @@ -114,29 +37,105 @@ namespace osu.Desktop.Tests.Visual AddStep("8 columns", () => createPlayfield(8, SpecialColumnPosition.Normal)); AddStep("Left special style", () => createPlayfield(8, SpecialColumnPosition.Left)); AddStep("Right special style", () => createPlayfield(8, SpecialColumnPosition.Right)); + AddStep("Reversed", () => createPlayfield(4, SpecialColumnPosition.Normal, true)); AddStep("Notes with input", () => createPlayfieldWithNotes(false)); - AddWaitStep((int)Math.Ceiling((start_time + duration) / TimePerAction)); - + AddStep("Notes with input (reversed)", () => createPlayfieldWithNotes(false, true)); AddStep("Notes with gravity", () => createPlayfieldWithNotes(true)); - AddWaitStep((int)Math.Ceiling((start_time + duration) / TimePerAction)); + AddStep("Notes with gravity (reversed)", () => createPlayfieldWithNotes(true, true)); } - private void triggerKeyDown(Column column) + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) { - column.TriggerOnKeyDown(new InputState(), new KeyDownEventArgs - { - Key = column.Key, - Repeat = false - }); + maniaRuleset = rulesets.GetRuleset(3); } - private void triggerKeyUp(Column column) + private SpeedAdjustmentContainer createTimingChange(double time, bool gravity) => new ManiaSpeedAdjustmentContainer(new MultiplierControlPoint(time) { - column.TriggerOnKeyUp(new InputState(), new KeyUpEventArgs + TimingPoint = { BeatLength = 1000 } + }, gravity ? ScrollingAlgorithm.Gravity : ScrollingAlgorithm.Basic); + + private void createPlayfield(int cols, SpecialColumnPosition specialPos, bool inverted = false) + { + Clear(); + + var inputManager = new ManiaInputManager(maniaRuleset, cols) { RelativeSizeAxes = Axes.Both }; + Add(inputManager); + + ManiaPlayfield playfield; + inputManager.Add(playfield = new ManiaPlayfield(cols) { - Key = column.Key + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + SpecialColumnPosition = specialPos }); + + playfield.Inverted.Value = inverted; + } + + private void createPlayfieldWithNotes(bool gravity, bool inverted = false) + { + Clear(); + + var rateAdjustClock = new StopwatchClock(true) { Rate = 1 }; + + var inputManager = new ManiaInputManager(maniaRuleset, 4) { RelativeSizeAxes = Axes.Both }; + Add(inputManager); + + ManiaPlayfield playfield; + inputManager.Add(playfield = new ManiaPlayfield(4) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Clock = new FramedClock(rateAdjustClock) + }); + + playfield.Inverted.Value = inverted; + + if (!gravity) + playfield.Columns.ForEach(c => c.Add(createTimingChange(0, false))); + + for (double t = start_time; t <= start_time + duration; t += 100) + { + if (gravity) + playfield.Columns.ElementAt(0).Add(createTimingChange(t, true)); + + playfield.Add(new DrawableNote(new Note + { + StartTime = t, + Column = 0 + }, ManiaAction.Key1)); + + if (gravity) + playfield.Columns.ElementAt(3).Add(createTimingChange(t, true)); + + playfield.Add(new DrawableNote(new Note + { + StartTime = t, + Column = 3 + }, ManiaAction.Key4)); + } + + if (gravity) + playfield.Columns.ElementAt(1).Add(createTimingChange(start_time, true)); + + playfield.Add(new DrawableHoldNote(new HoldNote + { + StartTime = start_time, + Duration = duration, + Column = 1 + }, ManiaAction.Key2)); + + if (gravity) + playfield.Columns.ElementAt(2).Add(createTimingChange(start_time, true)); + + playfield.Add(new DrawableHoldNote(new HoldNote + { + StartTime = start_time, + Duration = duration, + Column = 2 + }, ManiaAction.Key3)); } } } diff --git a/osu.Desktop.Tests/Visual/TestCaseScrollingPlayfield.cs b/osu.Desktop.Tests/Visual/TestCaseScrollingPlayfield.cs index 43bd921cfc..444adf6c24 100644 --- a/osu.Desktop.Tests/Visual/TestCaseScrollingPlayfield.cs +++ b/osu.Desktop.Tests/Visual/TestCaseScrollingPlayfield.cs @@ -3,18 +3,23 @@ using System; using System.Collections.Generic; +using NUnit.Framework; using OpenTK; using osu.Desktop.Tests.Beatmaps; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; using osu.Framework.Timing; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Beatmaps; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Timing; using osu.Game.Rulesets.UI; namespace osu.Desktop.Tests.Visual @@ -22,6 +27,7 @@ namespace osu.Desktop.Tests.Visual /// /// The most minimal implementation of a playfield with scrolling hit objects. /// + [TestFixture] public class TestCaseScrollingPlayfield : OsuTestCase { public TestCaseScrollingPlayfield() @@ -58,11 +64,71 @@ namespace osu.Desktop.Tests.Visual AddStep("Reverse direction", () => { - horizontalRulesetContainer.Playfield.Reversed.Toggle(); - verticalRulesetContainer.Playfield.Reversed.Toggle(); + horizontalRulesetContainer.Playfield.Reverse(); + verticalRulesetContainer.Playfield.Reverse(); }); } + [Test] + public void TestSpeedAdjustmentOrdering() + { + var hitObjectContainer = new ScrollingPlayfield.ScrollingHitObjectContainer(Axes.X); + + var speedAdjustments = new[] + { + new SpeedAdjustmentContainer(new MultiplierControlPoint()), + new SpeedAdjustmentContainer(new MultiplierControlPoint(1000) + { + TimingPoint = new TimingControlPoint { BeatLength = 500 } + }), + new SpeedAdjustmentContainer(new MultiplierControlPoint(2000) + { + TimingPoint = new TimingControlPoint { BeatLength = 1000 }, + DifficultyPoint = new DifficultyControlPoint { SpeedMultiplier = 2} + }), + new SpeedAdjustmentContainer(new MultiplierControlPoint(3000) + { + TimingPoint = new TimingControlPoint { BeatLength = 1000 }, + DifficultyPoint = new DifficultyControlPoint { SpeedMultiplier = 1} + }), + }; + + var hitObjects = new[] + { + new DrawableTestHitObject(Axes.X, new TestHitObject { StartTime = -1000 }), + new DrawableTestHitObject(Axes.X, new TestHitObject()), + new DrawableTestHitObject(Axes.X, new TestHitObject { StartTime = 1000 }), + new DrawableTestHitObject(Axes.X, new TestHitObject { StartTime = 2000 }), + new DrawableTestHitObject(Axes.X, new TestHitObject { StartTime = 3000 }), + new DrawableTestHitObject(Axes.X, new TestHitObject { StartTime = 4000 }), + }; + + hitObjects.ForEach(h => hitObjectContainer.Add(h)); + speedAdjustments.ForEach(hitObjectContainer.AddSpeedAdjustment); + + // The 0th index in hitObjectContainer.SpeedAdjustments is the "default" control point + // Check multiplier of the default speed adjustment + Assert.AreEqual(1, hitObjectContainer.SpeedAdjustments[0].ControlPoint.Multiplier); + Assert.AreEqual(1, speedAdjustments[0].ControlPoint.Multiplier); + Assert.AreEqual(2, speedAdjustments[1].ControlPoint.Multiplier); + Assert.AreEqual(2, speedAdjustments[2].ControlPoint.Multiplier); + Assert.AreEqual(1, speedAdjustments[3].ControlPoint.Multiplier); + + // Check insertion of hit objects + Assert.IsTrue(hitObjectContainer.SpeedAdjustments[4].Contains(hitObjects[0])); + Assert.IsTrue(hitObjectContainer.SpeedAdjustments[3].Contains(hitObjects[1])); + Assert.IsTrue(hitObjectContainer.SpeedAdjustments[2].Contains(hitObjects[2])); + Assert.IsTrue(hitObjectContainer.SpeedAdjustments[1].Contains(hitObjects[3])); + Assert.IsTrue(hitObjectContainer.SpeedAdjustments[0].Contains(hitObjects[4])); + Assert.IsTrue(hitObjectContainer.SpeedAdjustments[0].Contains(hitObjects[5])); + + hitObjectContainer.RemoveSpeedAdjustment(hitObjectContainer.SpeedAdjustments[3]); + + // The hit object contained in this speed adjustment should be resorted into the one occuring before it + + Assert.IsTrue(hitObjectContainer.SpeedAdjustments[3].Contains(hitObjects[1])); + } + private class TestRulesetContainer : ScrollingRulesetContainer { private readonly Axes scrollingAxes; @@ -77,6 +143,8 @@ namespace osu.Desktop.Tests.Visual public override ScoreProcessor CreateScoreProcessor() => new TestScoreProcessor(); + public override PassThroughInputManager CreateInputManager() => new PassThroughInputManager(); + protected override BeatmapConverter CreateBeatmapConverter() => new TestBeatmapConverter(); protected override Playfield CreatePlayfield() => new TestPlayfield(scrollingAxes); @@ -142,6 +210,8 @@ namespace osu.Desktop.Tests.Visual content = new Container { RelativeSizeAxes = Axes.Both } }; } + + public void Reverse() => Reversed.Toggle(); } @@ -155,4 +225,4 @@ namespace osu.Desktop.Tests.Visual public override string MaxResultString { get { throw new NotImplementedException(); } } } } -} \ No newline at end of file +} diff --git a/osu.Desktop.Tests/Visual/TestCaseTaikoPlayfield.cs b/osu.Desktop.Tests/Visual/TestCaseTaikoPlayfield.cs index 43148da512..681805ced0 100644 --- a/osu.Desktop.Tests/Visual/TestCaseTaikoPlayfield.cs +++ b/osu.Desktop.Tests/Visual/TestCaseTaikoPlayfield.cs @@ -16,6 +16,8 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps; using osu.Desktop.Tests.Beatmaps; using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; namespace osu.Desktop.Tests.Visual @@ -30,10 +32,11 @@ namespace osu.Desktop.Tests.Visual protected override double TimePerAction => default_duration * 2; private readonly Random rng = new Random(1337); - private readonly TaikoRulesetContainer rulesetContainer; - private readonly Container playfieldContainer; + private TaikoRulesetContainer rulesetContainer; + private Container playfieldContainer; - public TestCaseTaikoPlayfield() + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) { AddStep("Hit!", () => addHitJudgement(false)); AddStep("Kiai hit", () => addHitJudgement(true)); @@ -82,7 +85,7 @@ namespace osu.Desktop.Tests.Visual RelativeSizeAxes = Axes.X, Height = 768, Clock = new FramedClock(rateAdjustClock), - Children = new[] { rulesetContainer = new TaikoRulesetContainer(null, beatmap, true) } + Children = new[] { rulesetContainer = new TaikoRulesetContainer(rulesets.GetRuleset(1).CreateInstance(), beatmap, true) } }); } diff --git a/osu.Desktop.Tests/osu.Desktop.Tests.csproj b/osu.Desktop.Tests/osu.Desktop.Tests.csproj index 24d112a45c..86268e6110 100644 --- a/osu.Desktop.Tests/osu.Desktop.Tests.csproj +++ b/osu.Desktop.Tests/osu.Desktop.Tests.csproj @@ -87,6 +87,7 @@ + diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 88c8a206c8..a623347540 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -20,16 +20,11 @@ namespace osu.Desktop { internal class OsuGameDesktop : OsuGame { - private readonly VersionManager versionManager; + private VersionManager versionManager; public OsuGameDesktop(string[] args = null) : base(args) { - versionManager = new VersionManager - { - Depth = int.MinValue, - State = Visibility.Hidden - }; } public override Storage GetStorageForStableInstall() @@ -88,11 +83,15 @@ namespace osu.Desktop { base.LoadComplete(); - LoadComponentAsync(versionManager, Add); + LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }); + ScreenChanged += s => { - if (!versionManager.IsPresent && s is Intro) + if (s is Intro && s.ChildScreen == null) + { + Add(versionManager); versionManager.State = Visibility.Visible; + } }; } diff --git a/osu.Desktop/Overlays/VersionManager.cs b/osu.Desktop/Overlays/VersionManager.cs index b445340f50..1c0c1475b1 100644 --- a/osu.Desktop/Overlays/VersionManager.cs +++ b/osu.Desktop/Overlays/VersionManager.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -19,6 +20,7 @@ using OpenTK.Graphics; using System.Net.Http; using osu.Framework.Logging; using osu.Game; +using osu.Game.Configuration; namespace osu.Desktop.Overlays { @@ -26,17 +28,22 @@ namespace osu.Desktop.Overlays { private UpdateManager updateManager; private NotificationOverlay notificationOverlay; + private OsuConfigManager config; + private OsuGameBase game; public override bool HandleInput => false; [BackgroundDependencyLoader] - private void load(NotificationOverlay notification, OsuColour colours, TextureStore textures, OsuGameBase game) + private void load(NotificationOverlay notification, OsuColour colours, TextureStore textures, OsuGameBase game, OsuConfigManager config) { notificationOverlay = notification; + this.config = config; + this.game = game; AutoSizeAxes = Axes.Both; Anchor = Anchor.BottomCentre; Origin = Anchor.BottomCentre; + Alpha = 0; Children = new Drawable[] @@ -91,6 +98,42 @@ namespace osu.Desktop.Overlays checkForUpdateAsync(); } + protected override void LoadComplete() + { + base.LoadComplete(); + + var version = game.Version; + var lastVersion = config.Get(OsuSetting.Version); + if (game.IsDeployedBuild && version != lastVersion) + { + config.Set(OsuSetting.Version, version); + + // only show a notification if we've previously saved a version to the config file (ie. not the first run). + if (!string.IsNullOrEmpty(lastVersion)) + Scheduler.AddDelayed(() => notificationOverlay.Post(new UpdateCompleteNotification(version)), 5000); + } + } + + private class UpdateCompleteNotification : SimpleNotification + { + public UpdateCompleteNotification(string version) + { + Text = $"You are now running osu!lazer {version}.\nClick to see what's new!"; + Icon = FontAwesome.fa_check_square; + Activated = delegate + { + Process.Start($"https://github.com/ppy/osu/releases/tag/v{version}"); + return true; + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + IconBackgound.Colour = colours.BlueDark; + } + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game.Rulesets.Catch/CatchInputManager.cs b/osu.Game.Rulesets.Catch/CatchInputManager.cs index 2a913a77d3..47e9b9905a 100644 --- a/osu.Game.Rulesets.Catch/CatchInputManager.cs +++ b/osu.Game.Rulesets.Catch/CatchInputManager.cs @@ -3,11 +3,11 @@ using System.ComponentModel; using osu.Framework.Input.Bindings; -using osu.Game.Input.Bindings; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Catch { - public class CatchInputManager : DatabasedKeyBindingInputManager + public class CatchInputManager : RulesetInputManager { public CatchInputManager(RulesetInfo ruleset) : base(ruleset, 0, SimultaneousBindingMode.Unique) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index dcd8005267..214cbc7f50 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -1,14 +1,12 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK.Input; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; -using osu.Game.Screens.Play; using System.Collections.Generic; using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.Scoring; @@ -99,13 +97,6 @@ namespace osu.Game.Rulesets.Catch public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_fruits_o }; - public override IEnumerable CreateGameplayKeys() => new KeyCounter[] - { - new KeyCounterKeyboard(Key.ShiftLeft), - new KeyCounterMouse(MouseButton.Left), - new KeyCounterMouse(MouseButton.Right) - }; - public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap) => new CatchDifficultyCalculator(beatmap); public override ScoreProcessor CreateScoreProcessor() => new CatchScoreProcessor(); diff --git a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs index 1d037deaef..f8f74af6c9 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchRulesetContainer.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.UI protected override Playfield CreatePlayfield() => new CatchPlayfield(); - public override PassThroughInputManager CreateKeyBindingInputManager() => new CatchInputManager(Ruleset?.RulesetInfo); + public override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo); protected override DrawableHitObject GetVisualRepresentation(CatchBaseHit h) { diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index b55b9fdb37..be670936fd 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -28,12 +28,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps private Pattern lastPattern = new Pattern(); private FastRandom random; private Beatmap beatmap; - private bool isForCurrentRuleset; - protected override Beatmap ConvertBeatmap(Beatmap original, bool isForCurrentRuleset) + private readonly int availableColumns; + private readonly bool isForCurrentRuleset; + + public ManiaBeatmapConverter(bool isForCurrentRuleset, int availableColumns) { - this.isForCurrentRuleset = isForCurrentRuleset; + if (availableColumns <= 0) throw new ArgumentOutOfRangeException(nameof(availableColumns)); + this.isForCurrentRuleset = isForCurrentRuleset; + this.availableColumns = availableColumns; + } + + protected override Beatmap ConvertBeatmap(Beatmap original) + { beatmap = original; BeatmapDifficulty difficulty = original.BeatmapInfo.Difficulty; @@ -41,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps int seed = (int)Math.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)Math.Round(difficulty.ApproachRate); random = new FastRandom(seed); - return base.ConvertBeatmap(original, isForCurrentRuleset); + return base.ConvertBeatmap(original); } protected override IEnumerable ConvertHitObject(HitObject original, Beatmap beatmap) @@ -89,7 +97,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// The hit objects generated. private IEnumerable generateSpecific(HitObject original) { - var generator = new SpecificBeatmapPatternGenerator(random, original, beatmap, lastPattern); + var generator = new SpecificBeatmapPatternGenerator(random, original, beatmap, availableColumns, lastPattern); Pattern newPattern = generator.Generate(); lastPattern = newPattern; @@ -113,14 +121,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps Patterns.PatternGenerator conversion = null; if (distanceData != null) - conversion = new DistanceObjectPatternGenerator(random, original, beatmap, lastPattern); + conversion = new DistanceObjectPatternGenerator(random, original, beatmap, availableColumns, lastPattern); else if (endTimeData != null) - conversion = new EndTimeObjectPatternGenerator(random, original, beatmap); + conversion = new EndTimeObjectPatternGenerator(random, original, beatmap, availableColumns); else if (positionData != null) { computeDensity(original.StartTime); - conversion = new HitObjectPatternGenerator(random, original, beatmap, lastPattern, lastTime, lastPosition, density, lastStair); + conversion = new HitObjectPatternGenerator(random, original, beatmap, availableColumns, lastPattern, lastTime, lastPosition, density, lastStair); recordNote(original.StartTime, positionData.Position); } @@ -142,8 +150,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// private class SpecificBeatmapPatternGenerator : Patterns.Legacy.PatternGenerator { - public SpecificBeatmapPatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, Pattern previousPattern) - : base(random, hitObject, beatmap, previousPattern) + public SpecificBeatmapPatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, int availableColumns, Pattern previousPattern) + : base(random, hitObject, beatmap, availableColumns, previousPattern) { } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index 7e9615a703..20966a75f7 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -29,8 +29,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy private PatternType convertType; - public DistanceObjectPatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, Pattern previousPattern) - : base(random, hitObject, beatmap, previousPattern) + public DistanceObjectPatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, int availableColumns, Pattern previousPattern) + : base(random, hitObject, beatmap, availableColumns, previousPattern) { convertType = PatternType.None; if (Beatmap.ControlPointInfo.EffectPointAt(hitObject.StartTime).KiaiMode) @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy // The true distance, accounting for any repeats double distance = (distanceData?.Distance ?? 0) * repeatCount; // The velocity of the osu! hit object - calculated as the velocity of a slider - double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.Difficulty.SliderMultiplier / (timingPoint.BeatLength * difficultyPoint.SpeedMultiplier); + double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.Difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / timingPoint.BeatLength; // The duration of the osu! hit object double osuDuration = distance / osuVelocity; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs index 6ad7489e0f..c353496410 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs @@ -15,8 +15,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { private readonly double endTime; - public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap) - : base(random, hitObject, beatmap, new Pattern()) + public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, int availableColumns) + : base(random, hitObject, beatmap, availableColumns, new Pattern()) { var endtimeData = HitObject as IHasEndTime; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs index 99f55cb98b..077b926635 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs @@ -20,9 +20,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy private readonly PatternType convertType; - public HitObjectPatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density, PatternType lastStair) - : base(random, hitObject, beatmap, previousPattern) + public HitObjectPatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, int availableColumns, Pattern previousPattern, double previousTime, Vector2 previousPosition, double density, PatternType lastStair) + : base(random, hitObject, beatmap, availableColumns, previousPattern) { + if (previousTime > hitObject.StartTime) throw new ArgumentOutOfRangeException(nameof(previousTime)); + if (density < 0) throw new ArgumentOutOfRangeException(nameof(density)); + StairType = lastStair; TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs index b72618c36c..a3173f9784 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/PatternGenerator.cs @@ -25,11 +25,15 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// protected readonly FastRandom Random; - protected PatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, Pattern previousPattern) - : base(hitObject, beatmap, previousPattern) + protected PatternGenerator(FastRandom random, HitObject hitObject, Beatmap beatmap, int availableColumns, Pattern previousPattern) + : base(hitObject, beatmap, availableColumns, previousPattern) { - Random = random; + if (random == null) throw new ArgumentNullException(nameof(random)); + if (beatmap == null) throw new ArgumentNullException(nameof(beatmap)); + if (availableColumns <= 0) throw new ArgumentOutOfRangeException(nameof(availableColumns)); + if (previousPattern == null) throw new ArgumentNullException(nameof(previousPattern)); + Random = random; RandomStart = AvailableColumns == 8 ? 1 : 0; } @@ -62,6 +66,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// The amount of notes to be generated. protected int GetRandomNoteCount(double p2, double p3, double p4 = 0, double p5 = 0, double p6 = 0) { + if (p2 < 0 || p2 > 1) throw new ArgumentOutOfRangeException(nameof(p2)); + if (p3 < 0 || p3 > 1) throw new ArgumentOutOfRangeException(nameof(p3)); + if (p4 < 0 || p4 > 1) throw new ArgumentOutOfRangeException(nameof(p4)); + if (p5 < 0 || p5 > 1) throw new ArgumentOutOfRangeException(nameof(p5)); + if (p6 < 0 || p6 > 1) throw new ArgumentOutOfRangeException(nameof(p6)); + double val = Random.NextDouble(); if (val >= 1 - p6) return 6; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs index dda4d07182..ef321232c8 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/PatternGenerator.cs @@ -32,13 +32,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns /// protected readonly Beatmap Beatmap; - protected PatternGenerator(HitObject hitObject, Beatmap beatmap, Pattern previousPattern) + protected PatternGenerator(HitObject hitObject, Beatmap beatmap, int availableColumns, Pattern previousPattern) { - PreviousPattern = previousPattern; + if (hitObject == null) throw new ArgumentNullException(nameof(hitObject)); + if (beatmap == null) throw new ArgumentNullException(nameof(beatmap)); + if (availableColumns <= 0) throw new ArgumentOutOfRangeException(nameof(availableColumns)); + if (previousPattern == null) throw new ArgumentNullException(nameof(previousPattern)); + HitObject = hitObject; Beatmap = beatmap; - - AvailableColumns = (int)Math.Round(beatmap.BeatmapInfo.Difficulty.CircleSize); + AvailableColumns = availableColumns; + PreviousPattern = previousPattern; } /// diff --git a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs index aaba4d94f0..1f01750f44 100644 --- a/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/ManiaDifficultyCalculator.cs @@ -6,6 +6,7 @@ using osu.Game.Rulesets.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using System.Collections.Generic; +using System; namespace osu.Game.Rulesets.Mania { @@ -21,6 +22,6 @@ namespace osu.Game.Rulesets.Mania return 0; } - protected override BeatmapConverter CreateBeatmapConverter() => new ManiaBeatmapConverter(); + protected override BeatmapConverter CreateBeatmapConverter() => new ManiaBeatmapConverter(true, (int)Math.Max(1, Math.Round(Beatmap.BeatmapInfo.Difficulty.CircleSize))); } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Mania/ManiaInputManager.cs b/osu.Game.Rulesets.Mania/ManiaInputManager.cs new file mode 100644 index 0000000000..a574bc75ec --- /dev/null +++ b/osu.Game.Rulesets.Mania/ManiaInputManager.cs @@ -0,0 +1,41 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.ComponentModel; +using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Mania +{ + public class ManiaInputManager : RulesetInputManager + { + public ManiaInputManager(RulesetInfo ruleset, int variant) + : base(ruleset, variant, SimultaneousBindingMode.Unique) + { + } + } + + public enum ManiaAction + { + [Description("Special")] + Special, + [Description("Key 1")] + Key1 = 10, + [Description("Key 2")] + Key2, + [Description("Key 3")] + Key3, + [Description("Key 4")] + Key4, + [Description("Key 5")] + Key5, + [Description("Key 6")] + Key6, + [Description("Key 7")] + Key7, + [Description("Key 8")] + Key8, + [Description("Key 9")] + Key9 + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 90732df1f2..d852a54ab6 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -6,9 +6,9 @@ using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; -using osu.Game.Screens.Play; using System.Collections.Generic; using osu.Framework.Graphics; +using osu.Framework.Input.Bindings; using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Scoring; @@ -109,8 +109,6 @@ namespace osu.Game.Rulesets.Mania public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_mania_o }; - public override IEnumerable CreateGameplayKeys() => new KeyCounter[] { /* Todo: Should be keymod specific */ }; - public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap) => new ManiaDifficultyCalculator(beatmap); public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(); @@ -121,5 +119,43 @@ namespace osu.Game.Rulesets.Mania : base(rulesetInfo) { } + + public override IEnumerable AvailableVariants => new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + + public override IEnumerable GetDefaultKeyBindings(int variant = 0) + { + var leftKeys = new[] + { + InputKey.A, + InputKey.S, + InputKey.D, + InputKey.F + }; + + var rightKeys = new[] + { + InputKey.J, + InputKey.K, + InputKey.L, + InputKey.Semicolon + }; + + ManiaAction currentKey = ManiaAction.Key1; + + var bindings = new List(); + + for (int i = leftKeys.Length - variant / 2; i < leftKeys.Length; i++) + bindings.Add(new KeyBinding(leftKeys[i], currentKey++)); + + for (int i = 0; i < variant / 2; i++) + bindings.Add(new KeyBinding(rightKeys[i], currentKey++)); + + if (variant % 2 == 1) + bindings.Add(new KeyBinding(InputKey.Space, ManiaAction.Special)); + + return bindings; + } + + public override string GetVariantName(int variant) => $"{variant}K"; } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 17b0b0a607..e06f71cb64 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -5,20 +5,18 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using OpenTK.Graphics; -using osu.Framework.Configuration; -using OpenTK.Input; -using osu.Framework.Input; using OpenTK; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Judgements; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Input.Bindings; namespace osu.Game.Rulesets.Mania.Objects.Drawables { /// /// Visualises a hit object. /// - public class DrawableHoldNote : DrawableManiaHitObject + public class DrawableHoldNote : DrawableManiaHitObject, IKeyBindingHandler { private readonly DrawableNote head; private readonly DrawableNote tail; @@ -36,8 +34,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// private bool hasBroken; - public DrawableHoldNote(HoldNote hitObject, Bindable key = null) - : base(hitObject, key) + public DrawableHoldNote(HoldNote hitObject, ManiaAction action) + : base(hitObject, action) { RelativeSizeAxes = Axes.Both; Height = (float)HitObject.Duration; @@ -58,12 +56,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables RelativeChildOffset = new Vector2(0, (float)HitObject.StartTime), RelativeChildSize = new Vector2(1, (float)HitObject.Duration) }, - head = new DrawableHeadNote(this, key) + head = new DrawableHeadNote(this, action) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre }, - tail = new DrawableTailNote(this, key) + tail = new DrawableTailNote(this, action) { Anchor = Anchor.BottomCentre, Origin = Anchor.TopCentre @@ -106,16 +104,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { } - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + public bool OnPressed(ManiaAction action) { - // Make sure the keypress happened within the body of the hold note + // Make sure the action happened within the body of the hold note if (Time.Current < HitObject.StartTime || Time.Current > HitObject.EndTime) return false; - if (args.Key != Key) - return false; - - if (args.Repeat) + if (action != Action) return false; // The user has pressed during the body of the hold note, after the head note and its hit windows have passed @@ -126,13 +121,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables return true; } - protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) + public bool OnReleased(ManiaAction action) { // Make sure that the user started holding the key during the hold note if (!holdStartTime.HasValue) return false; - if (args.Key != Key) + if (action != Action) return false; holdStartTime = null; @@ -151,8 +146,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { private readonly DrawableHoldNote holdNote; - public DrawableHeadNote(DrawableHoldNote holdNote, Bindable key = null) - : base(holdNote.HitObject.Head, key) + public DrawableHeadNote(DrawableHoldNote holdNote, ManiaAction action) + : base(holdNote.HitObject.Head, action) { this.holdNote = holdNote; @@ -160,9 +155,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables Y = 0; } - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + public override bool OnPressed(ManiaAction action) { - if (!base.OnKeyDown(state, args)) + if (!base.OnPressed(action)) return false; // We only want to trigger a holding state from the head if the head has received a judgement @@ -188,8 +183,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { private readonly DrawableHoldNote holdNote; - public DrawableTailNote(DrawableHoldNote holdNote, Bindable key = null) - : base(holdNote.HitObject.Tail, key) + public DrawableTailNote(DrawableHoldNote holdNote, ManiaAction action) + : base(holdNote.HitObject.Tail, action) { this.holdNote = holdNote; @@ -210,7 +205,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables tailJudgement.HasBroken = holdNote.hasBroken; } - protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) + public override bool OnPressed(ManiaAction action) => false; // Tail doesn't handle key down + + public override bool OnReleased(ManiaAction action) { // Make sure that the user started holding the key during the hold note if (!holdNote.holdStartTime.HasValue) @@ -219,7 +216,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (Judgement.Result != HitResult.None) return false; - if (args.Key != Key) + if (action != Action) return false; UpdateJudgement(true); @@ -227,12 +224,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables // Handled by the hold note, which will set holding = false return false; } - - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) - { - // Tail doesn't handle key down - return false; - } } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index 10dc607ec3..bfef05ea07 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -2,8 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using OpenTK.Graphics; -using OpenTK.Input; -using osu.Framework.Configuration; using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Objects.Drawables; @@ -15,17 +13,17 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// /// The key that will trigger input for this hit object. /// - protected Bindable Key { get; private set; } = new Bindable(); + protected ManiaAction Action { get; } public new TObject HitObject; - protected DrawableManiaHitObject(TObject hitObject, Bindable key = null) + protected DrawableManiaHitObject(TObject hitObject, ManiaAction? action = null) : base(hitObject) { HitObject = hitObject; - if (key != null) - Key.BindTo(key); + if (action != null) + Action = action.Value; } public override Color4 AccentColour diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 9322fed3eb..c201ab7bd0 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -3,10 +3,8 @@ using System; using OpenTK.Graphics; -using OpenTK.Input; -using osu.Framework.Configuration; using osu.Framework.Graphics; -using osu.Framework.Input; +using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Objects.Drawables; @@ -16,12 +14,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// /// Visualises a hit object. /// - public class DrawableNote : DrawableManiaHitObject + public class DrawableNote : DrawableManiaHitObject, IKeyBindingHandler { private readonly NotePiece headPiece; - public DrawableNote(Note hitObject, Bindable key = null) - : base(hitObject, key) + public DrawableNote(Note hitObject, ManiaAction action) + : base(hitObject, action) { RelativeSizeAxes = Axes.X; Height = 100; @@ -81,18 +79,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables } } - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + public virtual bool OnPressed(ManiaAction action) { - if (Judgement.Result != HitResult.None) - return false; - - if (args.Key != Key) - return false; - - if (args.Repeat) + if (action != Action) return false; return UpdateJudgement(true); } + + public virtual bool OnReleased(ManiaAction action) => false; } } diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 9fbc9ba5e7..d5bc7cc659 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -3,17 +3,15 @@ using OpenTK; using OpenTK.Graphics; -using OpenTK.Input; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Colour; -using osu.Framework.Input; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using System; -using osu.Framework.Configuration; +using osu.Framework.Input.Bindings; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Judgements; @@ -32,10 +30,7 @@ namespace osu.Game.Rulesets.Mania.UI private const float column_width = 45; private const float special_column_width = 70; - /// - /// The key that will trigger input actions for this column and hit objects contained inside it. - /// - public Bindable Key = new Bindable(); + public ManiaAction Action; private readonly Box background; private readonly Container hitTargetBar; @@ -101,8 +96,8 @@ namespace osu.Game.Rulesets.Mania.UI // For column lighting, we need to capture input events before the notes new InputTarget { - KeyDown = onKeyDown, - KeyUp = onKeyUp + Pressed = onPressed, + Released = onReleased } } }, @@ -199,23 +194,20 @@ namespace osu.Game.Rulesets.Mania.UI HitObjects.Add(hitObject); } - private bool onKeyDown(InputState state, KeyDownEventArgs args) + private bool onPressed(ManiaAction action) { - if (args.Repeat) - return false; - - if (args.Key == Key) + if (action == Action) { - background.FadeTo(background.Alpha + 0.2f, 50, Easing.OutQuint); + background.FadeTo(0.6f, 50, Easing.OutQuint); keyIcon.ScaleTo(1.4f, 50, Easing.OutQuint); } return false; } - private bool onKeyUp(InputState state, KeyUpEventArgs args) + private bool onReleased(ManiaAction action) { - if (args.Key == Key) + if (action == Action) { background.FadeTo(0.2f, 800, Easing.OutQuart); keyIcon.ScaleTo(1f, 400, Easing.OutQuart); @@ -227,10 +219,10 @@ namespace osu.Game.Rulesets.Mania.UI /// /// This is a simple container which delegates various input events that have to be captured before the notes. /// - private class InputTarget : Container + private class InputTarget : Container, IKeyBindingHandler { - public Func KeyDown; - public Func KeyUp; + public Func Pressed; + public Func Released; public InputTarget() { @@ -239,8 +231,8 @@ namespace osu.Game.Rulesets.Mania.UI Alpha = 0; } - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) => KeyDown?.Invoke(state, args) ?? false; - protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) => KeyUp?.Invoke(state, args) ?? false; + public bool OnPressed(ManiaAction action) => Pressed?.Invoke(action) ?? false; + public bool OnReleased(ManiaAction action) => Released?.Invoke(action) ?? false; } } } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs index b836b54fa8..ce91fda023 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaPlayfield.cs @@ -11,9 +11,9 @@ using osu.Framework.Graphics.Containers; using System; using osu.Game.Graphics; using osu.Framework.Allocation; -using OpenTK.Input; using System.Linq; using System.Collections.Generic; +using osu.Framework.Configuration; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Framework.Graphics.Shapes; @@ -24,12 +24,6 @@ namespace osu.Game.Rulesets.Mania.UI { public const float HIT_TARGET_POSITION = 50; - /// - /// Default column keys, expanding outwards from the middle as more column are added. - /// E.g. 2 columns use FJ, 4 columns use DFJK, 6 use SDFJKL, etc... - /// - private static readonly Key[] default_keys = { Key.A, Key.S, Key.D, Key.F, Key.J, Key.K, Key.L, Key.Semicolon }; - private SpecialColumnPosition specialColumnPosition; /// /// The style to use for the special column. @@ -45,6 +39,11 @@ namespace osu.Game.Rulesets.Mania.UI } } + /// + /// Whether this playfield should be inverted. This flips everything inside the playfield. + /// + public readonly Bindable Inverted = new Bindable(true); + private readonly FlowContainer columns; public IEnumerable Columns => columns.Children; @@ -64,6 +63,8 @@ namespace osu.Game.Rulesets.Mania.UI if (columnCount <= 0) throw new ArgumentException("Can't have zero or fewer columns."); + Inverted.Value = true; + InternalChildren = new Drawable[] { new Container @@ -122,14 +123,26 @@ namespace osu.Game.Rulesets.Mania.UI } }; + var currentAction = ManiaAction.Key1; for (int i = 0; i < columnCount; i++) { var c = new Column(); c.VisibleTimeRange.BindTo(VisibleTimeRange); + c.IsSpecial = isSpecialColumn(i); + c.Action = c.IsSpecial ? ManiaAction.Special : currentAction++; + columns.Add(c); AddNested(c); } + + Inverted.ValueChanged += invertedChanged; + Inverted.TriggerChange(); + } + + private void invertedChanged(bool newValue) + { + Scale = new Vector2(1, newValue ? -1 : 1); } [BackgroundDependencyLoader] @@ -144,15 +157,11 @@ namespace osu.Game.Rulesets.Mania.UI specialColumnColour = colours.BlueDark; // Set the special column + colour + key - for (int i = 0; i < columnCount; i++) + foreach (var column in Columns) { - Column column = Columns.ElementAt(i); - column.IsSpecial = isSpecialColumn(i); - if (!column.IsSpecial) continue; - column.Key.Value = Key.Space; column.AccentColour = specialColumnColour; } @@ -166,21 +175,6 @@ namespace osu.Game.Rulesets.Mania.UI nonSpecialColumns[i].AccentColour = colour; nonSpecialColumns[nonSpecialColumns.Count - 1 - i].AccentColour = colour; } - - // We'll set the keys for non-special columns in another separate loop because it's not mirrored like the above colours - // Todo: This needs to go when we get to bindings and use Button1, ..., ButtonN instead - for (int i = 0; i < nonSpecialColumns.Count; i++) - { - Column column = nonSpecialColumns[i]; - - int keyOffset = default_keys.Length / 2 - nonSpecialColumns.Count / 2 + i; - if (keyOffset >= 0 && keyOffset < default_keys.Length) - column.Key.Value = default_keys[keyOffset]; - else - // There is no default key defined for this column. Let's set this to Unknown for now - // however note that this will be gone after bindings are in place - column.Key.Value = Key.Unknown; - } } /// diff --git a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs index 7f6b19f3bd..5a3da6d074 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaRulesetContainer.cs @@ -5,11 +5,10 @@ using System; using System.Collections.Generic; using System.Linq; using OpenTK; -using OpenTK.Input; using osu.Framework.Allocation; -using osu.Framework.Configuration; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; +using osu.Framework.Input; using osu.Framework.Lists; using osu.Framework.MathUtils; using osu.Game.Beatmaps; @@ -32,9 +31,10 @@ namespace osu.Game.Rulesets.Mania.UI public class ManiaRulesetContainer : ScrollingRulesetContainer { /// - /// Preferred column count. This will only have an effect during the initialization of the play field. + /// The number of columns which the should display, and which + /// the beatmap converter will attempt to convert beatmaps to use. /// - public int PreferredColumns; + private int availableColumns; public IEnumerable BarLines; @@ -75,36 +75,47 @@ namespace osu.Game.Rulesets.Mania.UI BarLines.ForEach(Playfield.Add); } - protected override void ApplyBeatmap() - { - base.ApplyBeatmap(); - - PreferredColumns = (int)Math.Max(1, Math.Round(Beatmap.BeatmapInfo.Difficulty.CircleSize)); - } - - protected sealed override Playfield CreatePlayfield() => new ManiaPlayfield(PreferredColumns) + protected sealed override Playfield CreatePlayfield() => new ManiaPlayfield(availableColumns) { Anchor = Anchor.Centre, Origin = Anchor.Centre, - // Invert by default for now (should be moved to config/skin later) - Scale = new Vector2(1, -1) }; public override ScoreProcessor CreateScoreProcessor() => new ManiaScoreProcessor(this); - protected override BeatmapConverter CreateBeatmapConverter() => new ManiaBeatmapConverter(); + public override PassThroughInputManager CreateInputManager() => new ManiaInputManager(Ruleset.RulesetInfo, availableColumns); + + protected override BeatmapConverter CreateBeatmapConverter() + { + if (IsForCurrentRuleset) + availableColumns = (int)Math.Max(1, Math.Round(WorkingBeatmap.BeatmapInfo.Difficulty.CircleSize)); + else + { + float percentSliderOrSpinner = (float)WorkingBeatmap.Beatmap.HitObjects.Count(h => h is IHasEndTime) / WorkingBeatmap.Beatmap.HitObjects.Count; + if (percentSliderOrSpinner < 0.2) + availableColumns = 7; + else if (percentSliderOrSpinner < 0.3 || Math.Round(WorkingBeatmap.BeatmapInfo.Difficulty.CircleSize) >= 5) + availableColumns = Math.Round(WorkingBeatmap.BeatmapInfo.Difficulty.OverallDifficulty) > 5 ? 7 : 6; + else if (percentSliderOrSpinner > 0.6) + availableColumns = Math.Round(WorkingBeatmap.BeatmapInfo.Difficulty.OverallDifficulty) > 4 ? 5 : 4; + else + availableColumns = Math.Max(4, Math.Min((int)Math.Round(WorkingBeatmap.BeatmapInfo.Difficulty.OverallDifficulty) + 1, 7)); + } + + return new ManiaBeatmapConverter(IsForCurrentRuleset, availableColumns); + } protected override DrawableHitObject GetVisualRepresentation(ManiaHitObject h) { - Bindable key = Playfield.Columns.ElementAt(h.Column).Key; + ManiaAction action = Playfield.Columns.ElementAt(h.Column).Action; var holdNote = h as HoldNote; if (holdNote != null) - return new DrawableHoldNote(holdNote, key); + return new DrawableHoldNote(holdNote, action); var note = h as Note; if (note != null) - return new DrawableNote(note, key); + return new DrawableNote(note, action); return null; } diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj index fb0b2d907f..890c9116cf 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -79,6 +79,7 @@ + diff --git a/osu.Game.Rulesets.Osu/Mods/OsuMod.cs b/osu.Game.Rulesets.Osu/Mods/OsuMod.cs index 649e6cfb3c..2970055bff 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuMod.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuMod.cs @@ -7,8 +7,13 @@ using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Objects; using System; +using System.Collections.Generic; using System.Linq; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; +using OpenTK; namespace osu.Game.Rulesets.Osu.Mods { @@ -28,10 +33,23 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 1.06; } - public class OsuModHardRock : ModHardRock + public class OsuModHardRock : ModHardRock, IApplicableMod { public override double ScoreMultiplier => 1.06; public override bool Ranked => true; + + public void ApplyToRulesetContainer(RulesetContainer rulesetContainer) + { + rulesetContainer.Objects.OfType().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Y)); + rulesetContainer.Objects.OfType().ForEach(s => + { + var newControlPoints = new List(); + s.ControlPoints.ForEach(c => newControlPoints.Add(new Vector2(c.X, OsuPlayfield.BASE_SIZE.Y - c.Y))); + + s.ControlPoints = newControlPoints; + s.Curve?.Calculate(); // Recalculate the slider curve + }); + } } public class OsuModSuddenDeath : ModSuddenDeath diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index bdf67a48cc..0bcb8a4986 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input; +using OpenTK; using OpenTK.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces @@ -97,11 +98,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces return base.OnMouseMove(state); } + // If the current time is between the start and end of the slider, we should track mouse input regardless of the cursor position. + public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => canCurrentlyTrack || base.ReceiveMouseInputAt(screenSpacePos); + private bool tracking; public bool Tracking { get { return tracking; } - set + private set { if (value == tracking) return; @@ -118,8 +122,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { base.Update(); + // Make sure to use the base version of ReceiveMouseInputAt so that we correctly check the position. if (Time.Current < slider.EndTime) - Tracking = canCurrentlyTrack && lastState != null && ReceiveMouseInputAt(lastState.Mouse.NativeState.Position) && ((Parent as DrawableSlider)?.OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false); + Tracking = canCurrentlyTrack && lastState != null && base.ReceiveMouseInputAt(lastState.Mouse.NativeState.Position) && ((Parent as DrawableSlider)?.OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false); } public void UpdateProgress(double progress, int repeat) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 4d8c030ede..056bde4005 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Osu.Objects TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime); - double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier / difficultyPoint.SpeedMultiplier; + double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier; Velocity = scoringDistance / timingPoint.BeatLength; TickDistance = scoringDistance / difficulty.SliderTickRate; diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs index a29b128e07..a65d28cec0 100644 --- a/osu.Game.Rulesets.Osu/OsuInputManager.cs +++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs @@ -2,36 +2,15 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.ComponentModel; -using System.Linq; -using osu.Framework.Input; using osu.Framework.Input.Bindings; -using osu.Game.Input.Bindings; -using OpenTK.Input; -using KeyboardState = osu.Framework.Input.KeyboardState; -using MouseState = osu.Framework.Input.MouseState; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu { - public class OsuInputManager : DatabasedKeyBindingInputManager + public class OsuInputManager : RulesetInputManager { public OsuInputManager(RulesetInfo ruleset) : base(ruleset, 0, SimultaneousBindingMode.Unique) { - - } - protected override void TransformState(InputState state) - { - base.TransformState(state); - - var mouse = state.Mouse as MouseState; - var keyboard = state.Keyboard as KeyboardState; - - if (mouse != null && keyboard != null) - { - if (mouse.IsPressed(MouseButton.Left)) - keyboard.Keys = keyboard.Keys.Concat(new[] { Key.LastKey + 1 }); - if (mouse.IsPressed(MouseButton.Right)) - keyboard.Keys = keyboard.Keys.Concat(new[] { Key.LastKey + 2 }); - } } } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index e5a35377e6..c943518b0b 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -1,7 +1,6 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK.Input; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; @@ -10,7 +9,6 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.OsuDifficulty; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; -using osu.Game.Screens.Play; using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; @@ -29,8 +27,8 @@ namespace osu.Game.Rulesets.Osu { new KeyBinding(InputKey.Z, OsuAction.LeftButton), new KeyBinding(InputKey.X, OsuAction.RightButton), - new KeyBinding(InputKey.LastKey + 1, OsuAction.LeftButton), - new KeyBinding(InputKey.LastKey + 2, OsuAction.RightButton), + new KeyBinding(InputKey.MouseLeft, OsuAction.LeftButton), + new KeyBinding(InputKey.MouseRight, OsuAction.RightButton), }; public override IEnumerable GetBeatmapStatistics(WorkingBeatmap beatmap) => new[] @@ -120,14 +118,6 @@ namespace osu.Game.Rulesets.Osu public override string Description => "osu!"; - public override IEnumerable CreateGameplayKeys() => new KeyCounter[] - { - new KeyCounterKeyboard(Key.Z), - new KeyCounterKeyboard(Key.X), - new KeyCounterMouse(MouseButton.Left), - new KeyCounterMouse(MouseButton.Right) - }; - public override ScoreProcessor CreateScoreProcessor() => new OsuScoreProcessor(); public override SettingsSubsection CreateSettings() => new OsuSettings(); diff --git a/osu.Game.Rulesets.Osu/Replays/OsuReplayInputHandler.cs b/osu.Game.Rulesets.Osu/Replays/OsuReplayInputHandler.cs new file mode 100644 index 0000000000..0811a68392 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Replays/OsuReplayInputHandler.cs @@ -0,0 +1,35 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using osu.Framework.Input; +using osu.Game.Rulesets.Replays; +using OpenTK; + +namespace osu.Game.Rulesets.Osu.Replays +{ + public class OsuReplayInputHandler : FramedReplayInputHandler + { + public OsuReplayInputHandler(Replay replay) + : base(replay) + { + } + + public override List GetPendingStates() + { + List actions = new List(); + + if (CurrentFrame?.MouseLeft ?? false) actions.Add(OsuAction.LeftButton); + if (CurrentFrame?.MouseRight ?? false) actions.Add(OsuAction.RightButton); + + return new List + { + new ReplayState + { + Mouse = new ReplayMouseState(ToScreenSpace(Position ?? Vector2.Zero)), + PressedActions = actions + } + }; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 856ca0c98d..3ee8f56665 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -1,8 +1,11 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using System.Collections.Generic; +using osu.Framework.Configuration; using osu.Framework.Extensions; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; @@ -14,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Scoring { internal class OsuScoreProcessor : ScoreProcessor { + public readonly Bindable Mode = new Bindable(ScoringMode.Exponential); + public OsuScoreProcessor() { } @@ -23,6 +28,35 @@ namespace osu.Game.Rulesets.Osu.Scoring { } + private float hpDrainRate; + + private int totalAccurateJudgements; + + private readonly Dictionary scoreResultCounts = new Dictionary(); + private readonly Dictionary comboResultCounts = new Dictionary(); + + private double comboMaxScore; + + protected override void ComputeTargets(Beatmap beatmap) + { + hpDrainRate = beatmap.BeatmapInfo.Difficulty.DrainRate; + totalAccurateJudgements = beatmap.HitObjects.Count; + + foreach (var h in beatmap.HitObjects) + { + if (h != null) + { + // TODO: add support for other object types. + AddJudgement(new OsuJudgement + { + MaxScore = OsuScoreResult.Hit300, + Score = OsuScoreResult.Hit300, + Result = HitResult.Hit + }); + } + } + } + protected override void Reset() { base.Reset(); @@ -34,9 +68,6 @@ namespace osu.Game.Rulesets.Osu.Scoring comboResultCounts.Clear(); } - private readonly Dictionary scoreResultCounts = new Dictionary(); - private readonly Dictionary comboResultCounts = new Dictionary(); - public override void PopulateScore(Score score) { base.PopulateScore(score); @@ -57,28 +88,75 @@ namespace osu.Game.Rulesets.Osu.Scoring comboResultCounts[judgement.Combo] = comboResultCounts.GetOrDefault(judgement.Combo) + 1; } - switch (judgement.Result) + switch (judgement.Score) { - case HitResult.Hit: - Health.Value += 0.1f; + case OsuScoreResult.Hit300: + Health.Value += (10.2 - hpDrainRate) * 0.02; break; - case HitResult.Miss: - Health.Value -= 0.2f; + + case OsuScoreResult.Hit100: + Health.Value += (8 - hpDrainRate) * 0.02; + break; + + case OsuScoreResult.Hit50: + Health.Value += (4 - hpDrainRate) * 0.02; + break; + + case OsuScoreResult.SliderTick: + Health.Value += Math.Max(7 - hpDrainRate, 0) * 0.01; + break; + + case OsuScoreResult.Miss: + Health.Value -= hpDrainRate * 0.04; break; } } - int score = 0; - int maxScore = 0; + calculateScore(); + } + + private void calculateScore() + { + int baseScore = 0; + double comboScore = 0; + + int baseMaxScore = 0; foreach (var j in Judgements) { - score += j.ScoreValue; - maxScore += j.MaxScoreValue; + baseScore += j.ScoreValue; + baseMaxScore += j.MaxScoreValue; + + comboScore += j.ScoreValue * (1 + Combo.Value / 10d); } - TotalScore.Value = score; - Accuracy.Value = (double)score / maxScore; + Accuracy.Value = (double)baseScore / baseMaxScore; + + if (comboScore > comboMaxScore) + comboMaxScore = comboScore; + + if (baseScore == 0) + TotalScore.Value = 0; + else + { + // temporary to make scoring feel more like score v1 without being score v1. + float exponentialFactor = Mode.Value == ScoringMode.Exponential ? (float)Judgements.Count / 100 : 1; + + TotalScore.Value = + (int) + ( + exponentialFactor * + 700000 * comboScore / comboMaxScore + + 300000 * Math.Pow(Accuracy.Value, 10) * ((double)Judgements.Count / totalAccurateJudgements) + + 0 /* bonusScore */ + ); + } + } + + public enum ScoringMode + { + Standardised, + Exponential } } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs index 917322de44..0b87bf7346 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuRulesetContainer.cs @@ -10,9 +10,11 @@ using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.Replays; namespace osu.Game.Rulesets.Osu.UI { @@ -31,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.UI protected override Playfield CreatePlayfield() => new OsuPlayfield(); - public override PassThroughInputManager CreateKeyBindingInputManager() => new OsuInputManager(Ruleset?.RulesetInfo); + public override PassThroughInputManager CreateInputManager() => new OsuInputManager(Ruleset.RulesetInfo); protected override DrawableHitObject GetVisualRepresentation(OsuHitObject h) { @@ -49,6 +51,8 @@ namespace osu.Game.Rulesets.Osu.UI return null; } + protected override FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => new OsuReplayInputHandler(replay); + protected override Vector2 GetPlayfieldAspectAdjust() => new Vector2(0.75f); } } diff --git a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj index 1422ded407..0c9e53cf69 100644 --- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj +++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj @@ -78,6 +78,7 @@ + diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 44f61e2074..2bf058fc2b 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -39,15 +39,22 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps /// private const float taiko_base_distance = 100; + private readonly bool isForCurrentRuleset; + protected override IEnumerable ValidConversionTypes { get; } = new[] { typeof(HitObject) }; - protected override Beatmap ConvertBeatmap(Beatmap original, bool isForCurrentRuleset) + public TaikoBeatmapConverter(bool isForCurrentRuleset) + { + this.isForCurrentRuleset = isForCurrentRuleset; + } + + protected override Beatmap ConvertBeatmap(Beatmap original) { // Rewrite the beatmap info to add the slider velocity multiplier BeatmapInfo info = original.BeatmapInfo.DeepClone(); info.Difficulty.SliderMultiplier *= legacy_velocity_multiplier; - Beatmap converted = base.ConvertBeatmap(original, isForCurrentRuleset); + Beatmap converted = base.ConvertBeatmap(original); // Post processing step to transform hit objects with the same start time into strong hits converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x => @@ -81,7 +88,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime); double speedAdjustment = difficultyPoint.SpeedMultiplier; - double speedAdjustedBeatLength = timingPoint.BeatLength * speedAdjustment; + double speedAdjustedBeatLength = timingPoint.BeatLength / speedAdjustment; // The true distance, accounting for any repeats. This ends up being the drum roll distance later double distance = distanceData.Distance * repeats * legacy_velocity_multiplier; @@ -94,7 +101,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps // For some reason, old osu! always uses speedAdjustment to determine the taiko velocity, but // only uses it to determine osu! velocity if beatmap version < 8. Let's account for that here. if (beatmap.BeatmapInfo.BeatmapVersion >= 8) - speedAdjustedBeatLength /= speedAdjustment; + speedAdjustedBeatLength *= speedAdjustment; // The velocity of the osu! hit object - calculated as the velocity of a slider double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.Difficulty.SliderMultiplier * legacy_velocity_multiplier / speedAdjustedBeatLength; @@ -104,7 +111,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps // If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat double tickSpacing = Math.Min(speedAdjustedBeatLength / beatmap.BeatmapInfo.Difficulty.SliderTickRate, taikoDuration / repeats); - if (tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength) + if (!isForCurrentRuleset && tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength) { List allSamples = curveData != null ? curveData.RepeatSamples : new List(new[] { samples }); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs index 06702e8141..9df1b41a8b 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs @@ -58,10 +58,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables }; } - protected override TaikoJudgement CreateJudgement() => new TaikoJudgement(); + protected override TaikoJudgement CreateJudgement() => null; protected override void UpdateState(ArmedState state) { } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs index c07be915d5..7a4cf1f1f7 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLineMajor.cs @@ -20,10 +20,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// private const float triangle_size = 20f; + private readonly Container triangleContainer; + public DrawableBarLineMajor(BarLine barLine) : base(barLine) { - Add(new Container + Add(triangleContainer = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -53,5 +55,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Tracker.Alpha = 1f; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + using (triangleContainer.BeginAbsoluteSequence(HitObject.StartTime)) + triangleContainer.FadeOut(150); + } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs index 8bb78669ca..26aa20f1af 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHit.cs @@ -4,13 +4,12 @@ using osu.Framework.Allocation; using osu.Game.Graphics; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; -using OpenTK.Input; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public class DrawableCentreHit : DrawableHit { - protected override Key[] HitKeys { get; } = { Key.F, Key.J }; + protected override TaikoAction[] HitActions { get; } = { TaikoAction.LeftCentre, TaikoAction.RightCentre }; public DrawableCentreHit(Hit hit) : base(hit) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHitStrong.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHitStrong.cs index 434fb9377f..1dbab408de 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHitStrong.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableCentreHitStrong.cs @@ -4,13 +4,12 @@ using osu.Framework.Allocation; using osu.Game.Graphics; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; -using OpenTK.Input; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public class DrawableCentreHitStrong : DrawableHitStrong { - protected override Key[] HitKeys { get; } = { Key.F, Key.J }; + protected override TaikoAction[] HitActions { get; } = { TaikoAction.LeftCentre, TaikoAction.RightCentre }; public DrawableCentreHitStrong(Hit hit) : base(hit) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 8f81368a2c..5d6ea8f51e 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -56,6 +56,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override TaikoPiece CreateMainPiece() => new ElongatedCirclePiece(); + public override bool OnPressed(TaikoAction action) => false; + [BackgroundDependencyLoader] private void load(OsuColour colours) { diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index 6e916aa9d5..e94886df16 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -5,7 +5,6 @@ using System; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Judgements; -using OpenTK.Input; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -59,7 +58,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - protected override bool HandleKeyPress(Key key) + public override bool OnPressed(TaikoAction action) { return Judgement.Result == HitResult.None && UpdateJudgement(true); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 0666d75083..6b041dfe25 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; -using OpenTK.Input; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -16,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// /// A list of keys which can result in hits for this HitObject. /// - protected abstract Key[] HitKeys { get; } + protected abstract TaikoAction[] HitActions { get; } /// /// Whether the last key pressed is a valid hit key. @@ -29,14 +28,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables FillMode = FillMode.Fit; } - protected override void LoadComplete() - { - base.LoadComplete(); - - // We need to set this here because RelativeSizeAxes won't/can't set our size by default with a different RelativeChildSize - Width *= Parent.RelativeChildSize.X; - } - protected override void CheckJudgement(bool userTriggered) { if (!userTriggered) @@ -62,16 +53,23 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Judgement.Result = HitResult.Miss; } - protected override bool HandleKeyPress(Key key) + public override bool OnPressed(TaikoAction action) { if (Judgement.Result != HitResult.None) return false; - validKeyPressed = HitKeys.Contains(key); + validKeyPressed = HitActions.Contains(action); return UpdateJudgement(true); } + protected override void Update() + { + base.Update(); + + Size = BaseSize * Parent.RelativeChildSize; + } + protected override void UpdateState(ArmedState state) { var circlePiece = MainPiece as CirclePiece; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHitStrong.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHitStrong.cs index 57e7eea470..256c50d33a 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHitStrong.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHitStrong.cs @@ -3,10 +3,8 @@ using System; using System.Linq; -using osu.Framework.Input; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Judgements; -using OpenTK.Input; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -20,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private double firstHitTime; private bool firstKeyHeld; - private Key firstHitKey; + private TaikoAction firstHitAction; protected DrawableHitStrong(Hit hit) : base(hit) @@ -46,18 +44,26 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Judgement.SecondHit = true; } - protected override bool HandleKeyPress(Key key) + public override bool OnReleased(TaikoAction action) + { + if (action == firstHitAction) + firstKeyHeld = false; + return base.OnReleased(action); + } + + public override bool OnPressed(TaikoAction action) { // Check if we've handled the first key if (Judgement.Result == HitResult.None) { // First key hasn't been handled yet, attempt to handle it - bool handled = base.HandleKeyPress(key); + bool handled = base.OnPressed(action); if (handled) { firstHitTime = Time.Current; - firstHitKey = key; + firstHitAction = action; + firstKeyHeld = true; } return handled; @@ -68,22 +74,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return false; // Don't handle represses of the first key - if (firstHitKey == key) + if (firstHitAction == action) return false; - // Don't handle invalid hit key presses - if (!HitKeys.Contains(key)) + // Don't handle invalid hit action presses + if (!HitActions.Contains(action)) return false; // Assume the intention was to hit the strong hit with both keys only if the first key is still being held down return firstKeyHeld && UpdateJudgement(true); } - - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) - { - firstKeyHeld = state.Keyboard.Keys.Contains(firstHitKey); - - return base.OnKeyDown(state, args); - } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs index 20e8d36105..9f0cd3e061 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHit.cs @@ -4,13 +4,12 @@ using osu.Framework.Allocation; using osu.Game.Graphics; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; -using OpenTK.Input; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public class DrawableRimHit : DrawableHit { - protected override Key[] HitKeys { get; } = { Key.D, Key.K }; + protected override TaikoAction[] HitActions { get; } = { TaikoAction.LeftRim, TaikoAction.RightRim }; public DrawableRimHit(Hit hit) : base(hit) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHitStrong.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHitStrong.cs index 4b1bb62bab..294198f3ee 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHitStrong.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableRimHitStrong.cs @@ -4,13 +4,12 @@ using osu.Framework.Allocation; using osu.Game.Graphics; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; -using OpenTK.Input; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public class DrawableRimHitStrong : DrawableHitStrong { - protected override Key[] HitKeys { get; } = { Key.D, Key.K }; + protected override TaikoAction[] HitActions { get; } = { TaikoAction.LeftRim, TaikoAction.RightRim }; public DrawableRimHitStrong(Hit hit) : base(hit) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 2b1ed07da7..4eb5c46838 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -13,7 +13,6 @@ using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; using OpenTK; using OpenTK.Graphics; -using OpenTK.Input; using osu.Framework.Graphics.Shapes; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -35,9 +34,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; - private readonly Key[] rimKeys = { Key.D, Key.K }; - private readonly Key[] centreKeys = { Key.F, Key.J }; - private Key[] lastKeySet; + private readonly TaikoAction[] rimActions = { TaikoAction.LeftRim, TaikoAction.RightRim }; + private readonly TaikoAction[] centreActions = { TaikoAction.LeftCentre, TaikoAction.RightCentre }; + private TaikoAction[] lastAction; /// /// The amount of times the user has hit this swell. @@ -200,6 +199,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.Update(); + Size = BaseSize * Parent.RelativeChildSize; + // Make the swell stop at the hit target X = (float)Math.Max(Time.Current, HitObject.StartTime); @@ -211,8 +212,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - - protected override bool HandleKeyPress(Key key) + public override bool OnPressed(TaikoAction action) { if (Judgement.Result != HitResult.None) return false; @@ -222,12 +222,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return false; // Find the keyset which this key corresponds to - var keySet = rimKeys.Contains(key) ? rimKeys : centreKeys; + var keySet = rimActions.Contains(action) ? rimActions : centreActions; // Ensure alternating keysets - if (keySet == lastKeySet) + if (keySet == lastAction) return false; - lastKeySet = keySet; + lastAction = keySet; UpdateJudgement(true); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index caae70fe05..46cdfcc365 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -1,28 +1,23 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System.Collections.Generic; using osu.Framework.Graphics; -using osu.Framework.Input; +using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; using OpenTK; -using OpenTK.Input; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { - public abstract class DrawableTaikoHitObject : DrawableScrollingHitObject - where TaikoHitType : TaikoHitObject + public abstract class DrawableTaikoHitObject + : DrawableScrollingHitObject, IKeyBindingHandler + where TaikoHitType : TaikoHitObject { - /// - /// A list of keys which this hit object will accept. These are the standard Taiko keys for now. - /// These should be moved to bindings later. - /// - private readonly List validKeys = new List(new[] { Key.D, Key.F, Key.J, Key.K }); - public override Vector2 OriginPosition => new Vector2(DrawHeight / 2); + protected readonly Vector2 BaseSize; + protected readonly TaikoPiece MainPiece; public new TaikoHitType HitObject; @@ -36,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Origin = Anchor.Custom; RelativeSizeAxes = Axes.Both; - Size = new Vector2(HitObject.IsStrong ? TaikoHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE); + Size = BaseSize = new Vector2(HitObject.IsStrong ? TaikoHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE); Add(MainPiece = CreateMainPiece()); MainPiece.KiaiMode = HitObject.Kiai; @@ -46,20 +41,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected virtual TaikoPiece CreateMainPiece() => new CirclePiece(); - protected virtual bool HandleKeyPress(Key key) => false; + public abstract bool OnPressed(TaikoAction action); - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) - { - // Make sure we don't handle held-down keys - if (args.Repeat) - return false; - - // Check if we've pressed a valid taiko key - if (!validKeys.Contains(args.Key)) - return false; - - // Handle it! - return HandleKeyPress(args.Key); - } + public virtual bool OnReleased(TaikoAction action) => false; } } diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs index f6425dd66f..fce0179721 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs @@ -4,7 +4,6 @@ using osu.Game.Rulesets.Replays; using System.Collections.Generic; using osu.Framework.Input; -using OpenTK.Input; namespace osu.Game.Rulesets.Taiko.Replays { @@ -17,21 +16,18 @@ namespace osu.Game.Rulesets.Taiko.Replays public override List GetPendingStates() { - var keys = new List(); + var actions = new List(); if (CurrentFrame?.MouseRight1 == true) - keys.Add(Key.F); + actions.Add(TaikoAction.LeftCentre); if (CurrentFrame?.MouseRight2 == true) - keys.Add(Key.J); + actions.Add(TaikoAction.RightCentre); if (CurrentFrame?.MouseLeft1 == true) - keys.Add(Key.D); + actions.Add(TaikoAction.LeftRim); if (CurrentFrame?.MouseLeft2 == true) - keys.Add(Key.K); + actions.Add(TaikoAction.RightRim); - return new List - { - new InputState { Keyboard = new ReplayKeyboardState(keys) } - }; + return new List { new ReplayState { PressedActions = actions } }; } } } diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index 647a1381c6..83203f5a7e 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -268,6 +268,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring base.Reset(); Health.Value = 0; + Accuracy.Value = 1; bonusScore = 0; comboPortion = 0; diff --git a/osu.Game.Rulesets.Taiko/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/TaikoDifficultyCalculator.cs index 33e9510f1c..35f849c704 100644 --- a/osu.Game.Rulesets.Taiko/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/TaikoDifficultyCalculator.cs @@ -135,6 +135,6 @@ namespace osu.Game.Rulesets.Taiko return difficulty; } - protected override BeatmapConverter CreateBeatmapConverter() => new TaikoBeatmapConverter(); + protected override BeatmapConverter CreateBeatmapConverter() => new TaikoBeatmapConverter(true); } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Taiko/TaikoInputManager.cs b/osu.Game.Rulesets.Taiko/TaikoInputManager.cs new file mode 100644 index 0000000000..1e3c0fbcf2 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/TaikoInputManager.cs @@ -0,0 +1,29 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.ComponentModel; +using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Taiko +{ + public class TaikoInputManager : RulesetInputManager + { + public TaikoInputManager(RulesetInfo ruleset) + : base(ruleset, 0, SimultaneousBindingMode.Unique) + { + } + } + + public enum TaikoAction + { + [Description("Left (Rim)")] + LeftRim, + [Description("Left (Centre)")] + LeftCentre, + [Description("Right (Centre)")] + RightCentre, + [Description("Right (Rim)")] + RightRim + } +} diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 7fa053161d..7b1452766e 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -1,18 +1,17 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK.Input; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Rulesets.UI; -using osu.Game.Screens.Play; using System.Collections.Generic; using osu.Framework.Graphics; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Scoring; +using osu.Framework.Input.Bindings; namespace osu.Game.Rulesets.Taiko { @@ -20,6 +19,18 @@ namespace osu.Game.Rulesets.Taiko { public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap, bool isForCurrentRuleset) => new TaikoRulesetContainer(this, beatmap, isForCurrentRuleset); + public override IEnumerable GetDefaultKeyBindings(int variant = 0) => new[] + { + new KeyBinding(InputKey.D, TaikoAction.LeftRim), + new KeyBinding(InputKey.F, TaikoAction.LeftCentre), + new KeyBinding(InputKey.J, TaikoAction.RightCentre), + new KeyBinding(InputKey.K, TaikoAction.RightRim), + new KeyBinding(InputKey.MouseLeft, TaikoAction.LeftCentre), + new KeyBinding(InputKey.MouseLeft, TaikoAction.RightCentre), + new KeyBinding(InputKey.MouseRight, TaikoAction.LeftRim), + new KeyBinding(InputKey.MouseRight, TaikoAction.RightRim), + }; + public override IEnumerable GetModsFor(ModType type) { switch (type) @@ -88,14 +99,6 @@ namespace osu.Game.Rulesets.Taiko public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_taiko_o }; - public override IEnumerable CreateGameplayKeys() => new KeyCounter[] - { - new KeyCounterKeyboard(Key.D), - new KeyCounterKeyboard(Key.F), - new KeyCounterKeyboard(Key.J), - new KeyCounterKeyboard(Key.K) - }; - public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap) => new TaikoDifficultyCalculator(beatmap); public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(); diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs index 0255171284..659f421ebe 100644 --- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs +++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs @@ -3,13 +3,12 @@ using System; using OpenTK; -using OpenTK.Input; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; -using osu.Framework.Input; +using osu.Framework.Input.Bindings; using osu.Game.Graphics; namespace osu.Game.Rulesets.Taiko.UI @@ -36,8 +35,8 @@ namespace osu.Game.Rulesets.Taiko.UI RelativeSizeAxes = Axes.Both, RelativePositionAxes = Axes.X, X = -middle_split / 2, - RimKey = Key.D, - CentreKey = Key.F + RimAction = TaikoAction.LeftRim, + CentreAction = TaikoAction.LeftCentre }, new TaikoHalfDrum(true) { @@ -47,8 +46,8 @@ namespace osu.Game.Rulesets.Taiko.UI RelativeSizeAxes = Axes.Both, RelativePositionAxes = Axes.X, X = middle_split / 2, - RimKey = Key.K, - CentreKey = Key.J + RimAction = TaikoAction.RightRim, + CentreAction = TaikoAction.RightCentre } }; } @@ -56,17 +55,17 @@ namespace osu.Game.Rulesets.Taiko.UI /// /// A half-drum. Contains one centre and one rim hit. /// - private class TaikoHalfDrum : Container + private class TaikoHalfDrum : Container, IKeyBindingHandler { /// /// The key to be used for the rim of the half-drum. /// - public Key RimKey; + public TaikoAction RimAction; /// /// The key to be used for the centre of the half-drum. /// - public Key CentreKey; + public TaikoAction CentreAction; private readonly Sprite rim; private readonly Sprite rimHit; @@ -124,20 +123,17 @@ namespace osu.Game.Rulesets.Taiko.UI centreHit.Colour = colours.Pink; } - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + public bool OnPressed(TaikoAction action) { - if (args.Repeat) - return false; - Drawable target = null; Drawable back = null; - if (args.Key == CentreKey) + if (action == CentreAction) { target = centreHit; back = centre; } - else if (args.Key == RimKey) + else if (action == RimAction) { target = rimHit; back = rim; @@ -166,6 +162,8 @@ namespace osu.Game.Rulesets.Taiko.UI return false; } + + public bool OnReleased(TaikoAction action) => false; } } } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index eb9e41f771..678de7f713 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -45,6 +45,8 @@ namespace osu.Game.Rulesets.Taiko.UI private readonly Container topLevelHitContainer; + private readonly Container barlineContainer; + private readonly Container overlayBackgroundContainer; private readonly Container backgroundContainer; @@ -85,7 +87,7 @@ namespace osu.Game.Rulesets.Taiko.UI { new Container { - Name = "Masked elements", + Name = "Masked elements before hit objects", RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Left = HIT_TARGET_OFFSET }, Masking = true, @@ -103,13 +105,21 @@ namespace osu.Game.Rulesets.Taiko.UI Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit - }, - content = new Container - { - RelativeSizeAxes = Axes.Both, - }, + } } }, + barlineContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = HIT_TARGET_OFFSET } + }, + content = new Container + { + Name = "Hit objects", + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = HIT_TARGET_OFFSET }, + Masking = true + }, kiaiExplosionContainer = new Container { Name = "Kiai hit explosions", @@ -198,6 +208,10 @@ namespace osu.Game.Rulesets.Taiko.UI base.Add(h); + var barline = h as DrawableBarLine; + if (barline != null) + barlineContainer.Add(barline.CreateProxy()); + // Swells should be moved at the very top of the playfield when they reach the hit target var swell = h as DrawableSwell; if (swell != null) @@ -239,4 +253,4 @@ namespace osu.Game.Rulesets.Taiko.UI hitExplosionContainer.Children.FirstOrDefault(e => e.Judgement == judgedObject.Judgement)?.VisualiseSecondHit(); } } -} \ No newline at end of file +} diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs index 723af3e4e9..96e5df12cd 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoRulesetContainer.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets.Taiko.Replays; using OpenTK; using osu.Game.Rulesets.Beatmaps; using System.Linq; +using osu.Framework.Input; namespace osu.Game.Rulesets.Taiko.UI { @@ -90,7 +91,9 @@ namespace osu.Game.Rulesets.Taiko.UI public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(this); - protected override BeatmapConverter CreateBeatmapConverter() => new TaikoBeatmapConverter(); + protected override BeatmapConverter CreateBeatmapConverter() => new TaikoBeatmapConverter(IsForCurrentRuleset); + + public override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo); protected override Playfield CreatePlayfield() => new TaikoPlayfield { diff --git a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj index 59c6f4db0a..33748a267f 100644 --- a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj +++ b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj @@ -86,6 +86,7 @@ + diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index ecaf0db096..eb456d7a63 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -13,7 +13,6 @@ using osu.Framework.Platform; using osu.Game.IPC; using osu.Framework.Allocation; using osu.Game.Beatmaps; -using osu.Game.Rulesets; namespace osu.Game.Tests.Beatmaps.IO { @@ -98,16 +97,14 @@ namespace osu.Game.Tests.Beatmaps.IO private OsuGameBase loadOsu(GameHost host) { + host.Storage.DeleteDatabase(@"client"); + var osu = new OsuGameBase(); Task.Run(() => host.Run(osu)); while (!osu.IsLoaded) Thread.Sleep(1); - //reset beatmap database (sqlite and storage backing) - osu.Dependencies.Get().Reset(); - osu.Dependencies.Get().Reset(); - return osu; } diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index ec90cabf02..c962201fe3 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -49,9 +49,18 @@ namespace osu.Game.Beatmaps public string Path { get; set; } - [JsonProperty("file_md5")] + [JsonProperty("file_sha2")] public string Hash { get; set; } + public bool Hidden { get; set; } + + /// + /// MD5 is kept for legacy support (matching against replays, osu-web-10 etc.). + /// + [Indexed] + [JsonProperty("file_md5")] + public string MD5Hash { get; set; } + // General public int AudioLeadIn { get; set; } public bool Countdown { get; set; } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index e388f5b6d1..551612330b 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -33,11 +33,21 @@ namespace osu.Game.Beatmaps /// public event Action BeatmapSetAdded; + /// + /// Fired when a single difficulty has been hidden. + /// + public event Action BeatmapHidden; + /// /// Fired when a is removed from the database. /// public event Action BeatmapSetRemoved; + /// + /// Fired when a single difficulty has been restored. + /// + public event Action BeatmapRestored; + /// /// A default representation of a WorkingBeatmap to use when no beatmap is available. /// @@ -71,6 +81,8 @@ namespace osu.Game.Beatmaps beatmaps = new BeatmapStore(connection); beatmaps.BeatmapSetAdded += s => BeatmapSetAdded?.Invoke(s); beatmaps.BeatmapSetRemoved += s => BeatmapSetRemoved?.Invoke(s); + beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b); + beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b); this.storage = storage; this.files = files; @@ -162,24 +174,34 @@ namespace osu.Game.Beatmaps // If we have an ID then we already exist in the database. if (beatmapSetInfo.ID != 0) return; - lock (beatmaps) - beatmaps.Add(beatmapSetInfo); + beatmaps.Add(beatmapSetInfo); } /// /// Delete a beatmap from the manager. /// Is a no-op for already deleted beatmaps. /// - /// The beatmap to delete. + /// The beatmap set to delete. public void Delete(BeatmapSetInfo beatmapSet) { - lock (beatmaps) - if (!beatmaps.Delete(beatmapSet)) return; + if (!beatmaps.Delete(beatmapSet)) return; if (!beatmapSet.Protected) files.Dereference(beatmapSet.Files.Select(f => f.FileInfo).ToArray()); } + /// + /// Delete a beatmap difficulty. + /// + /// The beatmap difficulty to hide. + public void Hide(BeatmapInfo beatmap) => beatmaps.Hide(beatmap); + + /// + /// Restore a beatmap difficulty. + /// + /// The beatmap difficulty to restore. + public void Restore(BeatmapInfo beatmap) => beatmaps.Restore(beatmap); + /// /// Returns a to a usable state if it has previously been deleted but not yet purged. /// Is a no-op for already usable beatmaps. @@ -187,8 +209,7 @@ namespace osu.Game.Beatmaps /// The beatmap to restore. public void Undelete(BeatmapSetInfo beatmapSet) { - lock (beatmaps) - if (!beatmaps.Undelete(beatmapSet)) return; + if (!beatmaps.Undelete(beatmapSet)) return; if (!beatmapSet.Protected) files.Reference(beatmapSet.Files.Select(f => f.FileInfo).ToArray()); @@ -248,6 +269,13 @@ namespace osu.Game.Beatmaps } } + /// + /// Refresh an existing instance of a from the store. + /// + /// A stale instance. + /// A fresh instance. + public BeatmapSetInfo Refresh(BeatmapSetInfo beatmapSet) => QueryBeatmapSet(s => s.ID == beatmapSet.ID); + /// /// Perform a lookup query on available s. /// @@ -255,7 +283,7 @@ namespace osu.Game.Beatmaps /// Results from the provided query. public List QueryBeatmapSets(Expression> query) { - lock (beatmaps) return beatmaps.QueryAndPopulate(query); + return beatmaps.QueryAndPopulate(query); } /// @@ -265,15 +293,12 @@ namespace osu.Game.Beatmaps /// The first result for the provided query, or null if no results were found. public BeatmapInfo QueryBeatmap(Func query) { - lock (beatmaps) - { - BeatmapInfo set = beatmaps.Query().FirstOrDefault(query); + BeatmapInfo set = beatmaps.Query().FirstOrDefault(query); - if (set != null) - beatmaps.Populate(set); + if (set != null) + beatmaps.Populate(set); - return set; - } + return set; } /// @@ -372,6 +397,7 @@ namespace osu.Game.Beatmaps beatmap.BeatmapInfo.Path = name; beatmap.BeatmapInfo.Hash = ms.ComputeSHA2Hash(); + beatmap.BeatmapInfo.MD5Hash = ms.ComputeMD5Hash(); // TODO: Diff beatmap metadata with set metadata and leave it here if necessary beatmap.BeatmapInfo.Metadata = null; diff --git a/osu.Game/Beatmaps/BeatmapStore.cs b/osu.Game/Beatmaps/BeatmapStore.cs index 2ec9a7d759..0f2d8cffa6 100644 --- a/osu.Game/Beatmaps/BeatmapStore.cs +++ b/osu.Game/Beatmaps/BeatmapStore.cs @@ -16,11 +16,14 @@ namespace osu.Game.Beatmaps public event Action BeatmapSetAdded; public event Action BeatmapSetRemoved; + public event Action BeatmapHidden; + public event Action BeatmapRestored; + /// /// The current version of this store. Used for migrations (see ). /// The initial version is 1. /// - protected override int StoreVersion => 2; + protected override int StoreVersion => 4; public BeatmapStore(SQLiteConnection connection) : base(connection) @@ -77,6 +80,14 @@ namespace osu.Game.Beatmaps // cannot migrate; breaking underlying changes. Reset(); break; + case 3: + // Added MD5Hash column to BeatmapInfo + Connection.MigrateTable(); + break; + case 4: + // Added Hidden column to BeatmapInfo + Connection.MigrateTable(); + break; } } } @@ -96,7 +107,7 @@ namespace osu.Game.Beatmaps } /// - /// Delete a to the database. + /// Delete a from the database. /// /// The beatmap to delete. /// Whether the beatmap's was changed. @@ -127,6 +138,38 @@ namespace osu.Game.Beatmaps return true; } + /// + /// Hide a in the database. + /// + /// The beatmap to hide. + /// Whether the beatmap's was changed. + public bool Hide(BeatmapInfo beatmap) + { + if (beatmap.Hidden) return false; + + beatmap.Hidden = true; + Connection.Update(beatmap); + + BeatmapHidden?.Invoke(beatmap); + return true; + } + + /// + /// Restore a previously hidden . + /// + /// The beatmap to restore. + /// Whether the beatmap's was changed. + public bool Restore(BeatmapInfo beatmap) + { + if (!beatmap.Hidden) return false; + + beatmap.Hidden = false; + Connection.Update(beatmap); + + BeatmapRestored?.Invoke(beatmap); + return true; + } + private void cleanupPendingDeletions() { Connection.RunInTransaction(() => diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index 30c7884334..c622d5310c 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -10,4 +10,4 @@ namespace osu.Game.Beatmaps.ControlPoints /// public double SpeedMultiplier = 1; } -} \ No newline at end of file +} diff --git a/osu.Game/Beatmaps/DifficultyCalculator.cs b/osu.Game/Beatmaps/DifficultyCalculator.cs index 474d38aa1b..5071e2c1b6 100644 --- a/osu.Game/Beatmaps/DifficultyCalculator.cs +++ b/osu.Game/Beatmaps/DifficultyCalculator.cs @@ -30,11 +30,15 @@ namespace osu.Game.Beatmaps public abstract class DifficultyCalculator : DifficultyCalculator where T : HitObject { + protected readonly Beatmap Beatmap; + protected List Objects; protected DifficultyCalculator(Beatmap beatmap) { - Objects = CreateBeatmapConverter().Convert(beatmap, true).HitObjects; + Beatmap = beatmap; + + Objects = CreateBeatmapConverter().Convert(beatmap).HitObjects; foreach (var h in Objects) h.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.Difficulty); diff --git a/osu.Game/Beatmaps/Drawables/BeatmapGroup.cs b/osu.Game/Beatmaps/Drawables/BeatmapGroup.cs index ad9a0a787b..9c62289bfa 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapGroup.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapGroup.cs @@ -11,6 +11,8 @@ namespace osu.Game.Beatmaps.Drawables { public class BeatmapGroup : IStateful { + public event Action StateChanged; + public BeatmapPanel SelectedPanel; /// @@ -23,19 +25,26 @@ namespace osu.Game.Beatmaps.Drawables /// public Action StartRequested; - public BeatmapSetHeader Header; + public Action DeleteRequested; - private BeatmapGroupState state; + public Action RestoreHiddenRequested; + + public Action HideDifficultyRequested; + + public BeatmapSetHeader Header; public List BeatmapPanels; public BeatmapSetInfo BeatmapSet; + private BeatmapGroupState state; public BeatmapGroupState State { get { return state; } set { + state = value; + switch (value) { case BeatmapGroupState.Expanded: @@ -54,7 +63,8 @@ namespace osu.Game.Beatmaps.Drawables panel.State = PanelSelectedState.Hidden; break; } - state = value; + + StateChanged?.Invoke(state); } } @@ -66,14 +76,17 @@ namespace osu.Game.Beatmaps.Drawables Header = new BeatmapSetHeader(beatmap) { GainedSelection = headerGainedSelection, + DeleteRequested = b => DeleteRequested(b), + RestoreHiddenRequested = b => RestoreHiddenRequested(b), RelativeSizeAxes = Axes.X, }; - BeatmapSet.Beatmaps = BeatmapSet.Beatmaps.OrderBy(b => b.StarDifficulty).ToList(); + BeatmapSet.Beatmaps = BeatmapSet.Beatmaps.Where(b => !b.Hidden).OrderBy(b => b.StarDifficulty).ToList(); BeatmapPanels = BeatmapSet.Beatmaps.Select(b => new BeatmapPanel(b) { Alpha = 0, GainedSelection = panelGainedSelection, + HideRequested = p => HideDifficultyRequested?.Invoke(p), StartRequested = p => { StartRequested?.Invoke(p.Beatmap); }, RelativeSizeAxes = Axes.X, }).ToList(); @@ -81,6 +94,7 @@ namespace osu.Game.Beatmaps.Drawables Header.AddDifficultyIcons(BeatmapPanels); } + private void headerGainedSelection(BeatmapSetHeader panel) { State = BeatmapGroupState.Expanded; diff --git a/osu.Game/Beatmaps/Drawables/BeatmapPanel.cs b/osu.Game/Beatmaps/Drawables/BeatmapPanel.cs index 429ecaf416..e216f1b83e 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapPanel.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapPanel.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; @@ -14,16 +15,20 @@ using OpenTK.Graphics; using osu.Framework.Input; using osu.Game.Graphics.Sprites; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; namespace osu.Game.Beatmaps.Drawables { - public class BeatmapPanel : Panel + public class BeatmapPanel : Panel, IHasContextMenu { public BeatmapInfo Beatmap; private readonly Sprite background; public Action GainedSelection; public Action StartRequested; + public Action EditRequested; + public Action HideRequested; + private readonly Triangles triangles; private readonly StarCounter starCounter; @@ -148,5 +153,12 @@ namespace osu.Game.Beatmaps.Drawables } }; } + + public MenuItem[] ContextMenuItems => new MenuItem[] + { + new OsuMenuItem("Play", MenuItemType.Highlighted, () => StartRequested?.Invoke(this)), + new OsuMenuItem("Edit", MenuItemType.Standard, () => EditRequested?.Invoke(this)), + new OsuMenuItem("Hide", MenuItemType.Destructive, () => HideRequested?.Invoke(Beatmap)), + }; } } diff --git a/osu.Game/Beatmaps/Drawables/BeatmapSetHeader.cs b/osu.Game/Beatmaps/Drawables/BeatmapSetHeader.cs index a2457ba78e..ee75b77747 100644 --- a/osu.Game/Beatmaps/Drawables/BeatmapSetHeader.cs +++ b/osu.Game/Beatmaps/Drawables/BeatmapSetHeader.cs @@ -3,22 +3,31 @@ using System; using System.Collections.Generic; +using System.Linq; using OpenTK; using OpenTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Beatmaps.Drawables { - public class BeatmapSetHeader : Panel + public class BeatmapSetHeader : Panel, IHasContextMenu { public Action GainedSelection; + + public Action DeleteRequested; + + public Action RestoreHiddenRequested; + private readonly SpriteText title; private readonly SpriteText artist; @@ -148,5 +157,23 @@ namespace osu.Game.Beatmaps.Drawables foreach (var p in panels) difficultyIcons.Add(new DifficultyIcon(p.Beatmap)); } + + public MenuItem[] ContextMenuItems + { + get + { + List items = new List(); + + if (State == PanelSelectedState.NotSelected) + items.Add(new OsuMenuItem("Expand", MenuItemType.Highlighted, () => State = PanelSelectedState.Selected)); + + if (beatmap.BeatmapSetInfo.Beatmaps.Any(b => b.Hidden)) + items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => RestoreHiddenRequested?.Invoke(beatmap.BeatmapSetInfo))); + + items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => DeleteRequested?.Invoke(beatmap.BeatmapSetInfo))); + + return items.ToArray(); + } + } } } \ No newline at end of file diff --git a/osu.Game/Beatmaps/Drawables/DifficultyColouredContainer.cs b/osu.Game/Beatmaps/Drawables/DifficultyColouredContainer.cs index 2614baa116..41b77f6584 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyColouredContainer.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyColouredContainer.cs @@ -33,7 +33,8 @@ namespace osu.Game.Beatmaps.Drawables Normal, Hard, Insane, - Expert + Expert, + ExpertPlus } private DifficultyRating getDifficultyRating(BeatmapInfo beatmap) @@ -44,7 +45,8 @@ namespace osu.Game.Beatmaps.Drawables if (rating < 2.25) return DifficultyRating.Normal; if (rating < 3.75) return DifficultyRating.Hard; if (rating < 5.25) return DifficultyRating.Insane; - return DifficultyRating.Expert; + if (rating < 6.75) return DifficultyRating.Expert; + return DifficultyRating.ExpertPlus; } private Color4 getColour(BeatmapInfo beatmap) @@ -55,12 +57,14 @@ namespace osu.Game.Beatmaps.Drawables return palette.Green; default: case DifficultyRating.Normal: - return palette.Yellow; + return palette.Blue; case DifficultyRating.Hard: - return palette.Pink; + return palette.Yellow; case DifficultyRating.Insane: - return palette.Purple; + return palette.Pink; case DifficultyRating.Expert: + return palette.Purple; + case DifficultyRating.ExpertPlus: return palette.Gray0; } } diff --git a/osu.Game/Beatmaps/Drawables/Panel.cs b/osu.Game/Beatmaps/Drawables/Panel.cs index d07be88392..d6ed306b39 100644 --- a/osu.Game/Beatmaps/Drawables/Panel.cs +++ b/osu.Game/Beatmaps/Drawables/Panel.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using osu.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -15,6 +16,8 @@ namespace osu.Game.Beatmaps.Drawables { public const float MAX_HEIGHT = 80; + public event Action StateChanged; + public override bool RemoveWhenNotAlive => false; private readonly Container nestedContainer; @@ -77,11 +80,15 @@ namespace osu.Game.Beatmaps.Drawables set { - if (state == value) return; + if (state == value) + return; var last = state; state = value; + ApplyState(last); + + StateChanged?.Invoke(State); } } diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index 22ad4f38ff..1c2f34a8fb 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -10,7 +10,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; -using osu.Game.Screens.Play; namespace osu.Game.Beatmaps { @@ -74,8 +73,6 @@ namespace osu.Game.Beatmaps public override string Description => "dummy"; - public override IEnumerable CreateGameplayKeys() => new List(); - public DummyRuleset(RulesetInfo rulesetInfo) : base(rulesetInfo) { diff --git a/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs b/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs index 093f607e0d..b51ea607dd 100644 --- a/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs @@ -280,7 +280,7 @@ namespace osu.Game.Beatmaps.Formats double time = double.Parse(split[0].Trim(), NumberFormatInfo.InvariantInfo); double beatLength = double.Parse(split[1].Trim(), NumberFormatInfo.InvariantInfo); - double speedMultiplier = beatLength < 0 ? -beatLength / 100.0 : 1; + double speedMultiplier = beatLength < 0 ? 100.0 / -beatLength : 1; TimeSignatures timeSignature = TimeSignatures.SimpleQuadruple; if (split.Length >= 3) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 6b07d5c967..44a6af841c 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -70,6 +70,8 @@ namespace osu.Game.Configuration // Update Set(OsuSetting.ReleaseStream, ReleaseStream.Lazer); + + Set(OsuSetting.Version, string.Empty); } public OsuConfigManager(Storage storage) : base(storage) @@ -106,6 +108,7 @@ namespace osu.Game.Configuration SnakingInSliders, SnakingOutSliders, ShowFpsDisplay, - ChatDisplayHeight + ChatDisplayHeight, + Version } } diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index 0713fa1a52..4ea4f4cdc3 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -19,12 +19,12 @@ namespace osu.Game.Graphics.Containers samplePopIn = audio.Sample.Get(@"UI/melodic-5"); samplePopOut = audio.Sample.Get(@"UI/melodic-4"); - StateChanged += OsuFocusedOverlayContainer_StateChanged; + StateChanged += onStateChanged; } - private void OsuFocusedOverlayContainer_StateChanged(VisibilityContainer arg1, Visibility arg2) + private void onStateChanged(Visibility visibility) { - switch (arg2) + switch (visibility) { case Visibility.Visible: samplePopIn?.Play(); diff --git a/osu.Game/Graphics/Cursor/MenuCursor.cs b/osu.Game/Graphics/Cursor/MenuCursor.cs index 9658cfdb09..ea02feef0e 100644 --- a/osu.Game/Graphics/Cursor/MenuCursor.cs +++ b/osu.Game/Graphics/Cursor/MenuCursor.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Input; using osu.Game.Configuration; using System; +using System.Diagnostics; using osu.Framework.Graphics.Textures; namespace osu.Game.Graphics.Cursor @@ -21,20 +22,31 @@ namespace osu.Game.Graphics.Cursor private bool dragging; + private bool startRotation; + protected override bool OnMouseMove(InputState state) { if (dragging) { - Vector2 offset = state.Mouse.Position - state.Mouse.PositionMouseDown ?? state.Mouse.Delta; - float degrees = (float)MathHelper.RadiansToDegrees(Math.Atan2(-offset.X, offset.Y)) + 24.3f; + Debug.Assert(state.Mouse.PositionMouseDown != null); - // Always rotate in the direction of least distance - float diff = (degrees - ActiveCursor.Rotation) % 360; - if (diff < -180) diff += 360; - if (diff > 180) diff -= 360; - degrees = ActiveCursor.Rotation + diff; + // don't start rotating until we're moved a minimum distance away from the mouse down location, + // else it can have an annoying effect. + startRotation |= Vector2.Distance(state.Mouse.Position, state.Mouse.PositionMouseDown.Value) > 30; - ActiveCursor.RotateTo(degrees, 600, Easing.OutQuint); + if (startRotation) + { + Vector2 offset = state.Mouse.Position - state.Mouse.PositionMouseDown.Value; + float degrees = (float)MathHelper.RadiansToDegrees(Math.Atan2(-offset.X, offset.Y)) + 24.3f; + + // Always rotate in the direction of least distance + float diff = (degrees - ActiveCursor.Rotation) % 360; + if (diff < -180) diff += 360; + if (diff > 180) diff -= 360; + degrees = ActiveCursor.Rotation + diff; + + ActiveCursor.RotateTo(degrees, 600, Easing.OutQuint); + } } return base.OnMouseMove(state); @@ -61,6 +73,7 @@ namespace osu.Game.Graphics.Cursor if (!state.Mouse.HasMainButtonPressed) { dragging = false; + startRotation = false; ((Cursor)ActiveCursor).AdditiveLayer.FadeOut(500, Easing.OutQuint); ActiveCursor.RotateTo(0, 600 * (1 + Math.Abs(ActiveCursor.Rotation / 720)), Easing.OutElasticHalf); diff --git a/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs index 9162fd6893..1ece10bc60 100644 --- a/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs +++ b/osu.Game/Graphics/Cursor/OsuContextMenuContainer.cs @@ -9,6 +9,6 @@ namespace osu.Game.Graphics.Cursor { public class OsuContextMenuContainer : ContextMenuContainer { - protected override ContextMenu CreateContextMenu() => new OsuContextMenu(); + protected override Menu CreateMenu() => new OsuContextMenu(); } } \ No newline at end of file diff --git a/osu.Game/Graphics/UserInterface/BarGraph.cs b/osu.Game/Graphics/UserInterface/BarGraph.cs index 87b4ab9e40..1a980e6bd5 100644 --- a/osu.Game/Graphics/UserInterface/BarGraph.cs +++ b/osu.Game/Graphics/UserInterface/BarGraph.cs @@ -44,19 +44,31 @@ namespace osu.Game.Graphics.UserInterface { List bars = Children.ToList(); foreach (var bar in value.Select((length, index) => new { Value = length, Bar = bars.Count > index ? bars[index] : null })) + { + float length = MaxValue ?? value.Max(); + if (length != 0) + length = bar.Value / length; + + float size = value.Count(); + if (size != 0) + size = 1.0f / size; + if (bar.Bar != null) { - bar.Bar.Length = bar.Value / (MaxValue ?? value.Max()); - bar.Bar.Size = (direction & BarDirection.Horizontal) > 0 ? new Vector2(1, 1.0f / value.Count()) : new Vector2(1.0f / value.Count(), 1); + bar.Bar.Length = length; + bar.Bar.Size = (direction & BarDirection.Horizontal) > 0 ? new Vector2(1, size) : new Vector2(size, 1); } else + { Add(new Bar { RelativeSizeAxes = Axes.Both, - Size = (direction & BarDirection.Horizontal) > 0 ? new Vector2(1, 1.0f / value.Count()) : new Vector2(1.0f / value.Count(), 1), - Length = bar.Value / (MaxValue ?? value.Max()), + Size = (direction & BarDirection.Horizontal) > 0 ? new Vector2(1, size) : new Vector2(size, 1), + Length = length, Direction = Direction, }); + } + } //I'm using ToList() here because Where() returns an Enumerable which can change it's elements afterwards RemoveRange(Children.Where((bar, index) => index >= value.Count()).ToList()); } diff --git a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs index b3e53280fb..65ece51a70 100644 --- a/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs +++ b/osu.Game/Graphics/UserInterface/BreadcrumbControl.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using OpenTK; using osu.Framework; using osu.Framework.Graphics; @@ -35,6 +36,8 @@ namespace osu.Game.Graphics.UserInterface private class BreadcrumbTabItem : OsuTabItem, IStateful { + public event Action StateChanged; + public readonly SpriteIcon Chevron; //don't allow clicking between transitions and don't make the chevron clickable @@ -42,6 +45,7 @@ namespace osu.Game.Graphics.UserInterface public override bool HandleInput => State == Visibility.Visible; private Visibility state; + public Visibility State { get { return state; } @@ -62,6 +66,8 @@ namespace osu.Game.Graphics.UserInterface this.FadeOut(transition_duration, Easing.OutQuint); this.ScaleTo(new Vector2(0.8f, 1f), transition_duration, Easing.OutQuint); } + + StateChanged?.Invoke(State); } } diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs index 5650fc6f48..abc1b036bc 100644 --- a/osu.Game/Graphics/UserInterface/OsuButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuButton.cs @@ -24,6 +24,8 @@ namespace osu.Game.Graphics.UserInterface private SampleChannel sampleClick; private SampleChannel sampleHover; + protected Triangles Triangles; + public OsuButton() { Height = 40; @@ -52,7 +54,7 @@ namespace osu.Game.Graphics.UserInterface AddRange(new Drawable[] { - new Triangles + Triangles = new Triangles { RelativeSizeAxes = Axes.Both, ColourDark = colours.BlueDarker, @@ -120,4 +122,4 @@ namespace osu.Game.Graphics.UserInterface } } } -} \ No newline at end of file +} diff --git a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs index d4882cce1e..4ce6c98744 100644 --- a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs @@ -1,52 +1,39 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK; using OpenTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.UserInterface; namespace osu.Game.Graphics.UserInterface { - public class OsuContextMenu : ContextMenu - where TItem : ContextMenuItem + public class OsuContextMenu : OsuMenu { - protected override Menu CreateMenu() => new CustomMenu(); + private const int fade_duration = 250; - public class CustomMenu : Menu + public OsuContextMenu() + : base(Direction.Vertical) { - private const int fade_duration = 250; - - public CustomMenu() + MaskingContainer.CornerRadius = 5; + MaskingContainer.EdgeEffect = new EdgeEffectParameters { - CornerRadius = 5; - ItemsContainer.Padding = new MarginPadding { Vertical = OsuContextMenuItem.MARGIN_VERTICAL }; - Masking = true; - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(0.1f), - Radius = 4, - }; - } + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.1f), + Radius = 4, + }; - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Background.Colour = colours.ContextMenuGray; - } - - protected override void AnimateOpen() => this.FadeIn(fade_duration, Easing.OutQuint); - protected override void AnimateClose() => this.FadeOut(fade_duration, Easing.OutQuint); - - protected override void UpdateContentHeight() - { - var actualHeight = (RelativeSizeAxes & Axes.Y) > 0 ? 1 : ContentHeight; - this.ResizeTo(new Vector2(1, State == MenuState.Opened ? actualHeight : 0), 300, Easing.OutQuint); - } + ItemsContainer.Padding = new MarginPadding { Vertical = DrawableOsuMenuItem.MARGIN_VERTICAL }; } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = colours.ContextMenuGray; + } + + protected override void AnimateOpen() => this.FadeIn(fade_duration, Easing.OutQuint); + protected override void AnimateClose() => this.FadeOut(fade_duration, Easing.OutQuint); } } \ No newline at end of file diff --git a/osu.Game/Graphics/UserInterface/OsuContextMenuItem.cs b/osu.Game/Graphics/UserInterface/OsuContextMenuItem.cs deleted file mode 100644 index 196e905bdc..0000000000 --- a/osu.Game/Graphics/UserInterface/OsuContextMenuItem.cs +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using OpenTK.Graphics; -using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input; -using osu.Game.Graphics.Sprites; - -namespace osu.Game.Graphics.UserInterface -{ - public class OsuContextMenuItem : ContextMenuItem - { - private const int transition_length = 80; - private const int margin_horizontal = 17; - public const int MARGIN_VERTICAL = 4; - private const int text_size = 17; - - private OsuSpriteText text; - private OsuSpriteText textBold; - - private SampleChannel sampleClick; - private SampleChannel sampleHover; - - private readonly MenuItemType type; - - protected override Container CreateTextContainer(string title) => new Container - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Children = new Drawable[] - { - text = new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - TextSize = text_size, - Text = title, - Margin = new MarginPadding { Horizontal = margin_horizontal, Vertical = MARGIN_VERTICAL }, - }, - textBold = new OsuSpriteText - { - AlwaysPresent = true, - Alpha = 0, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - TextSize = text_size, - Text = title, - Font = @"Exo2.0-Bold", - Margin = new MarginPadding { Horizontal = margin_horizontal, Vertical = MARGIN_VERTICAL }, - } - } - }; - - public OsuContextMenuItem(string title, MenuItemType type = MenuItemType.Standard) : base(title) - { - this.type = type; - } - - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - sampleHover = audio.Sample.Get(@"UI/generic-hover"); - sampleClick = audio.Sample.Get(@"UI/generic-click"); - - BackgroundColour = Color4.Transparent; - BackgroundColourHover = OsuColour.FromHex(@"172023"); - - updateTextColour(); - } - - private void updateTextColour() - { - switch (type) - { - case MenuItemType.Standard: - textBold.Colour = text.Colour = Color4.White; - break; - case MenuItemType.Destructive: - textBold.Colour = text.Colour = Color4.Red; - break; - case MenuItemType.Highlighted: - textBold.Colour = text.Colour = OsuColour.FromHex(@"ffcc22"); - break; - } - } - - protected override bool OnHover(InputState state) - { - sampleHover.Play(); - textBold.FadeIn(transition_length, Easing.OutQuint); - text.FadeOut(transition_length, Easing.OutQuint); - return base.OnHover(state); - } - - protected override void OnHoverLost(InputState state) - { - textBold.FadeOut(transition_length, Easing.OutQuint); - text.FadeIn(transition_length, Easing.OutQuint); - base.OnHoverLost(state); - } - - protected override bool OnClick(InputState state) - { - sampleClick.Play(); - return base.OnClick(state); - } - } -} \ No newline at end of file diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index f5a4219707..b69186e8b0 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -14,107 +14,185 @@ using OpenTK; namespace osu.Game.Graphics.UserInterface { - public class OsuDropdown : Dropdown + public class OsuDropdown : Dropdown, IHasAccentColour { - protected override DropdownHeader CreateHeader() => new OsuDropdownHeader { AccentColour = AccentColour }; - - protected override Menu CreateMenu() => new OsuMenu(); - - private Color4? accentColour; - public virtual Color4 AccentColour + private Color4 accentColour; + public Color4 AccentColour { - get { return accentColour.GetValueOrDefault(); } + get { return accentColour; } set { accentColour = value; - if (Header != null) - ((OsuDropdownHeader)Header).AccentColour = value; - foreach (var item in MenuItems.OfType()) - item.AccentColour = value; + updateAccentColour(); } } [BackgroundDependencyLoader] private void load(OsuColour colours) { - if (accentColour == null) - AccentColour = colours.PinkDarker; + if (accentColour == default(Color4)) + accentColour = colours.PinkDarker; + updateAccentColour(); + } - protected override DropdownMenuItem CreateMenuItem(string text, T value) => new OsuDropdownMenuItem(text, value) { AccentColour = AccentColour }; - - public class OsuDropdownMenuItem : DropdownMenuItem + private void updateAccentColour() { - public OsuDropdownMenuItem(string text, T current) : base(text, current) + var header = Header as IHasAccentColour; + if (header != null) header.AccentColour = accentColour; + + var menu = Menu as IHasAccentColour; + if (menu != null) menu.AccentColour = accentColour; + } + + protected override DropdownHeader CreateHeader() => new OsuDropdownHeader(); + + protected override DropdownMenu CreateMenu() => new OsuDropdownMenu(); + + #region OsuDropdownMenu + protected class OsuDropdownMenu : DropdownMenu, IHasAccentColour + { + // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring + public OsuDropdownMenu() { - Foreground.Padding = new MarginPadding(2); + CornerRadius = 4; + BackgroundColour = Color4.Black.Opacity(0.5f); - Masking = true; - CornerRadius = 6; + // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring + ItemsContainer.Padding = new MarginPadding(5); + } - Children = new[] + // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring + protected override void AnimateOpen() => this.FadeIn(300, Easing.OutQuint); + protected override void AnimateClose() => this.FadeOut(300, Easing.OutQuint); + + // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring + protected override void UpdateSize(Vector2 newSize) + { + if (Direction == Direction.Vertical) { - new FillFlowContainer - { - Direction = FillDirection.Horizontal, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - Chevron = new SpriteIcon - { - AlwaysPresent = true, - Icon = FontAwesome.fa_chevron_right, - Colour = Color4.Black, - Alpha = 0.5f, - Size = new Vector2(8), - Margin = new MarginPadding { Left = 3, Right = 3 }, - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - }, - Label = new OsuSpriteText { - Text = text, - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - } - } + Width = newSize.X; + this.ResizeHeightTo(newSize.Y, 300, Easing.OutQuint); + } + else + { + Height = newSize.Y; + this.ResizeWidthTo(newSize.X, 300, Easing.OutQuint); } - }; - } - - private Color4? accentColour; - - protected readonly SpriteIcon Chevron; - protected readonly OsuSpriteText Label; - - protected override void FormatForeground(bool hover = false) - { - base.FormatForeground(hover); - Chevron.Alpha = hover ? 1 : 0; } + private Color4 accentColour; public Color4 AccentColour { - get { return accentColour.GetValueOrDefault(); } + get { return accentColour; } set { accentColour = value; - BackgroundColourHover = BackgroundColourSelected = value; - FormatBackground(); - FormatForeground(); + foreach (var c in Children.OfType()) + c.AccentColour = value; } } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - BackgroundColour = Color4.Transparent; - BackgroundColourHover = accentColour ?? colours.PinkDarker; - BackgroundColourSelected = Color4.Black.Opacity(0.5f); - } - } + protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => new DrawableOsuDropdownMenuItem(item) { AccentColour = accentColour }; - public class OsuDropdownHeader : DropdownHeader + #region DrawableOsuDropdownMenuItem + protected class DrawableOsuDropdownMenuItem : DrawableDropdownMenuItem, IHasAccentColour + { + private Color4? accentColour; + public Color4 AccentColour + { + get { return accentColour ?? nonAccentSelectedColour; } + set + { + accentColour = value; + updateColours(); + } + } + + private void updateColours() + { + BackgroundColourHover = accentColour ?? nonAccentHoverColour; + BackgroundColourSelected = accentColour ?? nonAccentSelectedColour; + UpdateBackgroundColour(); + UpdateForegroundColour(); + } + + private Color4 nonAccentHoverColour; + private Color4 nonAccentSelectedColour; + + public DrawableOsuDropdownMenuItem(MenuItem item) + : base(item) + { + Foreground.Padding = new MarginPadding(2); + + Masking = true; + CornerRadius = 6; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = Color4.Transparent; + + nonAccentHoverColour = colours.PinkDarker; + nonAccentSelectedColour = Color4.Black.Opacity(0.5f); + updateColours(); + } + + protected override void UpdateForegroundColour() + { + base.UpdateForegroundColour(); + + var content = Foreground.Children.FirstOrDefault() as Content; + if (content != null) content.Chevron.Alpha = IsHovered ? 1 : 0; + } + + protected override Drawable CreateContent() => new Content(); + + protected new class Content : FillFlowContainer, IHasText + { + public string Text + { + get { return Label.Text; } + set { Label.Text = value; } + } + + public readonly OsuSpriteText Label; + public readonly SpriteIcon Chevron; + + public Content() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Horizontal; + + Children = new Drawable[] + { + Chevron = new SpriteIcon + { + AlwaysPresent = true, + Icon = FontAwesome.fa_chevron_right, + Colour = Color4.Black, + Alpha = 0.5f, + Size = new Vector2(8), + Margin = new MarginPadding { Left = 3, Right = 3 }, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + }, + Label = new OsuSpriteText + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + } + }; + } + } + } + #endregion + } + #endregion + + public class OsuDropdownHeader : DropdownHeader, IHasAccentColour { protected readonly SpriteText Text; protected override string Label @@ -125,14 +203,14 @@ namespace osu.Game.Graphics.UserInterface protected readonly SpriteIcon Icon; - private Color4? accentColour; + private Color4 accentColour; public virtual Color4 AccentColour { - get { return accentColour.GetValueOrDefault(); } + get { return accentColour; } set { accentColour = value; - BackgroundColourHover = value; + BackgroundColourHover = accentColour; } } @@ -167,7 +245,7 @@ namespace osu.Game.Graphics.UserInterface private void load(OsuColour colours) { BackgroundColour = Color4.Black.Opacity(0.5f); - BackgroundColourHover = accentColour ?? colours.PinkDarker; + BackgroundColourHover = colours.PinkDarker; } } } diff --git a/osu.Game/Graphics/UserInterface/OsuMenu.cs b/osu.Game/Graphics/UserInterface/OsuMenu.cs index e597bc44b5..3fd5481152 100644 --- a/osu.Game/Graphics/UserInterface/OsuMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuMenu.cs @@ -1,32 +1,170 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using OpenTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; +using osu.Game.Graphics.Sprites; +using OpenTK; namespace osu.Game.Graphics.UserInterface { public class OsuMenu : Menu { - public OsuMenu() + public OsuMenu(Direction direction, bool topLevelMenu = false) + : base(direction, topLevelMenu) { - CornerRadius = 4; - Background.Colour = Color4.Black.Opacity(0.5f); + BackgroundColour = Color4.Black.Opacity(0.5f); + MaskingContainer.CornerRadius = 4; ItemsContainer.Padding = new MarginPadding(5); } protected override void AnimateOpen() => this.FadeIn(300, Easing.OutQuint); - protected override void AnimateClose() => this.FadeOut(300, Easing.OutQuint); - protected override void UpdateContentHeight() + protected override void UpdateSize(Vector2 newSize) { - var actualHeight = (RelativeSizeAxes & Axes.Y) > 0 ? 1 : ContentHeight; - this.ResizeTo(new Vector2(1, State == MenuState.Opened ? actualHeight : 0), 300, Easing.OutQuint); + if (Direction == Direction.Vertical) + { + Width = newSize.X; + this.ResizeHeightTo(newSize.Y, 300, Easing.OutQuint); + } + else + { + Height = newSize.Y; + this.ResizeWidthTo(newSize.X, 300, Easing.OutQuint); + } + } + + protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => new DrawableOsuMenuItem(item); + + protected override Menu CreateSubMenu() => new OsuMenu(Direction.Vertical) + { + Anchor = Direction == Direction.Horizontal ? Anchor.BottomLeft : Anchor.TopRight + }; + + protected class DrawableOsuMenuItem : DrawableMenuItem + { + private const int margin_horizontal = 17; + private const int text_size = 17; + private const int transition_length = 80; + public const int MARGIN_VERTICAL = 4; + + private SampleChannel sampleClick; + private SampleChannel sampleHover; + + private TextContainer text; + + public DrawableOsuMenuItem(MenuItem item) + : base(item) + { + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleHover = audio.Sample.Get(@"UI/generic-hover"); + sampleClick = audio.Sample.Get(@"UI/generic-click"); + + BackgroundColour = Color4.Transparent; + BackgroundColourHover = OsuColour.FromHex(@"172023"); + + updateTextColour(); + } + + private void updateTextColour() + { + switch ((Item as OsuMenuItem)?.Type) + { + default: + case MenuItemType.Standard: + text.Colour = Color4.White; + break; + case MenuItemType.Destructive: + text.Colour = Color4.Red; + break; + case MenuItemType.Highlighted: + text.Colour = OsuColour.FromHex(@"ffcc22"); + break; + } + } + + protected override bool OnHover(InputState state) + { + sampleHover.Play(); + text.BoldText.FadeIn(transition_length, Easing.OutQuint); + text.NormalText.FadeOut(transition_length, Easing.OutQuint); + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + text.BoldText.FadeOut(transition_length, Easing.OutQuint); + text.NormalText.FadeIn(transition_length, Easing.OutQuint); + base.OnHoverLost(state); + } + + protected override bool OnClick(InputState state) + { + sampleClick.Play(); + return base.OnClick(state); + } + + protected sealed override Drawable CreateContent() => text = CreateTextContainer(); + protected virtual TextContainer CreateTextContainer() => new TextContainer(); + + protected class TextContainer : Container, IHasText + { + public string Text + { + get { return NormalText.Text; } + set + { + NormalText.Text = value; + BoldText.Text = value; + } + } + + public readonly SpriteText NormalText; + public readonly SpriteText BoldText; + + public TextContainer() + { + Anchor = Anchor.CentreLeft; + Origin = Anchor.CentreLeft; + + AutoSizeAxes = Axes.Both; + + Children = new Drawable[] + { + NormalText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + TextSize = text_size, + Margin = new MarginPadding { Horizontal = margin_horizontal, Vertical = MARGIN_VERTICAL }, + }, + BoldText = new OsuSpriteText + { + AlwaysPresent = true, + Alpha = 0, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + TextSize = text_size, + Font = @"Exo2.0-Bold", + Margin = new MarginPadding { Horizontal = margin_horizontal, Vertical = MARGIN_VERTICAL }, + } + }; + } + } } } } diff --git a/osu.Game/Graphics/UserInterface/OsuMenuItem.cs b/osu.Game/Graphics/UserInterface/OsuMenuItem.cs new file mode 100644 index 0000000000..1f78666d20 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/OsuMenuItem.cs @@ -0,0 +1,25 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Graphics.UserInterface; + +namespace osu.Game.Graphics.UserInterface +{ + public class OsuMenuItem : MenuItem + { + public readonly MenuItemType Type; + + public OsuMenuItem(string text, MenuItemType type = MenuItemType.Standard) + : base(text) + { + Type = type; + } + + public OsuMenuItem(string text, MenuItemType type, Action action) + : base(text, action) + { + Type = type; + } + } +} \ No newline at end of file diff --git a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs index b7ca25b95a..7f193a0e1a 100644 --- a/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuPasswordTextBox.cs @@ -1,11 +1,16 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using OpenTK; using OpenTK.Graphics; +using OpenTK.Input; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Framework.Platform; namespace osu.Game.Graphics.UserInterface { @@ -15,6 +20,49 @@ namespace osu.Game.Graphics.UserInterface public override bool AllowClipboardExport => false; + private readonly CapsWarning warning; + + private GameHost host; + + public OsuPasswordTextBox() + { + Add(warning = new CapsWarning + { + Size = new Vector2(20), + Origin = Anchor.CentreRight, + Anchor = Anchor.CentreRight, + Margin = new MarginPadding { Right = 10 }, + Alpha = 0, + }); + } + + [BackgroundDependencyLoader] + private void load(GameHost host) + { + this.host = host; + } + + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) + { + if (args.Key == Key.CapsLock) + updateCapsWarning(host.CapsLockEnabled); + return base.OnKeyDown(state, args); + } + + protected override void OnFocus(InputState state) + { + updateCapsWarning(host.CapsLockEnabled); + base.OnFocus(state); + } + + protected override void OnFocusLost(InputState state) + { + updateCapsWarning(false); + base.OnFocusLost(state); + } + + private void updateCapsWarning(bool visible) => warning.FadeTo(visible ? 1 : 0, 250, Easing.OutQuint); + public class PasswordMaskChar : Container { private readonly CircularContainer circle; @@ -51,5 +99,21 @@ namespace osu.Game.Graphics.UserInterface circle.ResizeTo(new Vector2(0.8f), 500, Easing.OutQuint); } } + + private class CapsWarning : SpriteIcon, IHasTooltip + { + public string TooltipText => @"Caps lock is active"; + + public CapsWarning() + { + Icon = FontAwesome.fa_warning; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colour) + { + Colour = colour.YellowLight; + } + } } } diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index 5ad412965c..89b1f4124b 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -37,34 +37,34 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load(OsuColour colours) { - if (accentColour == null) + if (accentColour == default(Color4)) AccentColour = colours.Blue; } - private Color4? accentColour; + private Color4 accentColour; public Color4 AccentColour { - get { return accentColour.GetValueOrDefault(); } + get { return accentColour; } set { accentColour = value; - var dropDown = Dropdown as OsuTabDropdown; - if (dropDown != null) - dropDown.AccentColour = value; - foreach (var item in TabContainer.Children.OfType()) - item.AccentColour = value; + var dropdown = Dropdown as IHasAccentColour; + if (dropdown != null) + dropdown.AccentColour = value; + foreach (var i in TabContainer.Children.OfType()) + i.AccentColour = value; } } - public class OsuTabItem : TabItem + public class OsuTabItem : TabItem, IHasAccentColour { protected readonly SpriteText Text; private readonly Box box; - private Color4? accentColour; + private Color4 accentColour; public Color4 AccentColour { - get { return accentColour.GetValueOrDefault(); } + get { return accentColour; } set { accentColour = value; @@ -103,7 +103,7 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load(OsuColour colours) { - if (accentColour == null) + if (accentColour == default(Color4)) AccentColour = colours.Blue; } @@ -140,38 +140,55 @@ namespace osu.Game.Graphics.UserInterface protected override void OnDeactivated() => fadeInactive(); } + // todo: this needs to go private class OsuTabDropdown : OsuDropdown { - protected override DropdownHeader CreateHeader() => new OsuTabDropdownHeader - { - AccentColour = AccentColour, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - }; - - protected override DropdownMenuItem CreateMenuItem(string text, T value) - { - var item = base.CreateMenuItem(text, value); - item.ForegroundColourHover = Color4.Black; - return item; - } - public OsuTabDropdown() { - DropdownMenu.Anchor = Anchor.TopRight; - DropdownMenu.Origin = Anchor.TopRight; - RelativeSizeAxes = Axes.X; - - DropdownMenu.Background.Colour = Color4.Black.Opacity(0.7f); - DropdownMenu.MaxHeight = 400; } + protected override DropdownMenu CreateMenu() => new OsuTabDropdownMenu(); + + protected override DropdownHeader CreateHeader() => new OsuTabDropdownHeader + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight + }; + + private class OsuTabDropdownMenu : OsuDropdownMenu + { + public OsuTabDropdownMenu() + { + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + + BackgroundColour = Color4.Black.Opacity(0.7f); + MaxHeight = 400; + } + + protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => new DrawableOsuTabDropdownMenuItem(item) { AccentColour = AccentColour }; + + private class DrawableOsuTabDropdownMenuItem : DrawableOsuDropdownMenuItem + { + public DrawableOsuTabDropdownMenuItem(MenuItem item) + : base(item) + { + ForegroundColourHover = Color4.Black; + } + } + } + + protected class OsuTabDropdownHeader : OsuDropdownHeader { public override Color4 AccentColour { - get { return base.AccentColour; } + get + { + return base.AccentColour; + } + set { base.AccentColour = value; @@ -179,18 +196,6 @@ namespace osu.Game.Graphics.UserInterface } } - protected override bool OnHover(InputState state) - { - Foreground.Colour = BackgroundColour; - return base.OnHover(state); - } - - protected override void OnHoverLost(InputState state) - { - Foreground.Colour = BackgroundColourHover; - base.OnHoverLost(state); - } - public OsuTabDropdownHeader() { RelativeSizeAxes = Axes.None; @@ -220,6 +225,18 @@ namespace osu.Game.Graphics.UserInterface Padding = new MarginPadding { Left = 5, Right = 5 }; } + + protected override bool OnHover(InputState state) + { + Foreground.Colour = BackgroundColour; + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + Foreground.Colour = BackgroundColourHover; + base.OnHoverLost(state); + } } } } diff --git a/osu.Game/Graphics/UserInterface/ProgressBar.cs b/osu.Game/Graphics/UserInterface/ProgressBar.cs new file mode 100644 index 0000000000..f22983a6e6 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/ProgressBar.cs @@ -0,0 +1,67 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using OpenTK.Graphics; + +namespace osu.Game.Graphics.UserInterface +{ + public class ProgressBar : SliderBar + { + public Action OnSeek; + + private readonly Box fill; + private readonly Box background; + + public Color4 FillColour + { + set { fill.Colour = value; } + } + + public Color4 BackgroundColour + { + set + { + background.Alpha = 1; + background.Colour = value; + } + } + + public double EndTime + { + set { CurrentNumber.MaxValue = value; } + } + + public double CurrentTime + { + set { CurrentNumber.Value = value; } + } + + public ProgressBar() + { + CurrentNumber.MinValue = 0; + CurrentNumber.MaxValue = 1; + RelativeSizeAxes = Axes.X; + + Children = new Drawable[] + { + background = new Box + { + Alpha = 0, + RelativeSizeAxes = Axes.Both + }, + fill = new Box { RelativeSizeAxes = Axes.Y } + }; + } + + protected override void UpdateValue(float value) + { + fill.Width = value * UsableWidth; + } + + protected override void OnUserChange() => OnSeek?.Invoke(Current); + } +} \ No newline at end of file diff --git a/osu.Game/Graphics/UserInterface/Volume/VolumeControl.cs b/osu.Game/Graphics/UserInterface/Volume/VolumeControl.cs index cd77fb9f5b..4c108e793a 100644 --- a/osu.Game/Graphics/UserInterface/Volume/VolumeControl.cs +++ b/osu.Game/Graphics/UserInterface/Volume/VolumeControl.cs @@ -3,11 +3,11 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input; using osu.Framework.Threading; using OpenTK; using osu.Framework.Audio; using osu.Framework.Allocation; +using osu.Game.Input.Bindings; namespace osu.Game.Graphics.UserInterface.Volume { @@ -64,15 +64,25 @@ namespace osu.Game.Graphics.UserInterface.Volume volumeMeterMusic.Bindable.ValueChanged -= volumeChanged; } - public void Adjust(InputState state) + public bool Adjust(GlobalAction action) { - if (State == Visibility.Hidden) + switch (action) { - Show(); - return; + case GlobalAction.DecreaseVolume: + if (State == Visibility.Hidden) + Show(); + else + volumeMeterMaster.Decrease(); + return true; + case GlobalAction.IncreaseVolume: + if (State == Visibility.Hidden) + Show(); + else + volumeMeterMaster.Increase(); + return true; } - volumeMeterMaster.TriggerOnWheel(state); + return false; } [BackgroundDependencyLoader] diff --git a/osu.Game/Graphics/UserInterface/Volume/VolumeControlReceptor.cs b/osu.Game/Graphics/UserInterface/Volume/VolumeControlReceptor.cs index c155871f33..c222fecb5d 100644 --- a/osu.Game/Graphics/UserInterface/Volume/VolumeControlReceptor.cs +++ b/osu.Game/Graphics/UserInterface/Volume/VolumeControlReceptor.cs @@ -3,32 +3,16 @@ using System; using osu.Framework.Graphics.Containers; -using osu.Framework.Input; -using OpenTK.Input; +using osu.Framework.Input.Bindings; +using osu.Game.Input.Bindings; namespace osu.Game.Graphics.UserInterface.Volume { - internal class VolumeControlReceptor : Container + internal class VolumeControlReceptor : Container, IKeyBindingHandler { - public Action ActionRequested; + public Func ActionRequested; - protected override bool OnWheel(InputState state) - { - ActionRequested?.Invoke(state); - return true; - } - - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) - { - switch (args.Key) - { - case Key.Up: - case Key.Down: - ActionRequested?.Invoke(state); - return true; - } - - return base.OnKeyDown(state, args); - } + public bool OnPressed(GlobalAction action) => ActionRequested?.Invoke(action) ?? false; + public bool OnReleased(GlobalAction action) => false; } } diff --git a/osu.Game/Graphics/UserInterface/Volume/VolumeMeter.cs b/osu.Game/Graphics/UserInterface/Volume/VolumeMeter.cs index 41fa60bec2..81c4fa9bae 100644 --- a/osu.Game/Graphics/UserInterface/Volume/VolumeMeter.cs +++ b/osu.Game/Graphics/UserInterface/Volume/VolumeMeter.cs @@ -4,15 +4,16 @@ using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input; using osu.Game.Graphics.Sprites; using OpenTK; using OpenTK.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Bindings; +using osu.Game.Input.Bindings; namespace osu.Game.Graphics.UserInterface.Volume { - internal class VolumeMeter : Container + internal class VolumeMeter : Container, IKeyBindingHandler { private readonly Box meterFill; public BindableDouble Bindable { get; } = new BindableDouble(); @@ -76,12 +77,35 @@ namespace osu.Game.Graphics.UserInterface.Volume } } - protected override bool OnWheel(InputState state) + public void Increase() { - Volume += 0.05f * state.Mouse.WheelDelta; - return true; + Volume += 0.05f; + } + + public void Decrease() + { + Volume -= 0.05f; } private void updateFill() => meterFill.ScaleTo(new Vector2(1, (float)Volume), 300, Easing.OutQuint); + + public bool OnPressed(GlobalAction action) + { + if (!IsHovered) return false; + + switch (action) + { + case GlobalAction.DecreaseVolume: + Decrease(); + return true; + case GlobalAction.IncreaseVolume: + Increase(); + return true; + } + + return false; + } + + public bool OnReleased(GlobalAction action) => false; } } \ No newline at end of file diff --git a/osu.Game/Input/Bindings/GlobalKeyBindingInputManager.cs b/osu.Game/Input/Bindings/GlobalKeyBindingInputManager.cs index 5ea66fa600..759396e195 100644 --- a/osu.Game/Input/Bindings/GlobalKeyBindingInputManager.cs +++ b/osu.Game/Input/Bindings/GlobalKeyBindingInputManager.cs @@ -26,7 +26,10 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings), new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar), new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings), - new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleDirect), + new KeyBinding(new[] { InputKey.Up }, GlobalAction.IncreaseVolume), + new KeyBinding(new[] { InputKey.MouseWheelUp }, GlobalAction.IncreaseVolume), + new KeyBinding(new[] { InputKey.Down }, GlobalAction.DecreaseVolume), + new KeyBinding(new[] { InputKey.MouseWheelDown }, GlobalAction.DecreaseVolume), }; protected override IEnumerable KeyBindingInputQueue => @@ -47,5 +50,9 @@ namespace osu.Game.Input.Bindings ToggleSettings, [Description("Toggle osu!direct")] ToggleDirect, + [Description("Increase Volume")] + IncreaseVolume, + [Description("Decrease Volume")] + DecreaseVolume, } } diff --git a/osu.Game/Input/Handlers/ReplayInputHandler.cs b/osu.Game/Input/Handlers/ReplayInputHandler.cs index 76e038048c..5f86495580 100644 --- a/osu.Game/Input/Handlers/ReplayInputHandler.cs +++ b/osu.Game/Input/Handlers/ReplayInputHandler.cs @@ -2,6 +2,8 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Collections.Generic; +using osu.Framework.Input; using osu.Framework.Input.Handlers; using osu.Framework.Platform; using OpenTK; @@ -29,5 +31,18 @@ namespace osu.Game.Input.Handlers public override bool IsActive => true; public override int Priority => 0; + + public class ReplayState : InputState + where T : struct + { + public List PressedActions; + + public override InputState Clone() + { + var clone = (ReplayState)base.Clone(); + clone.PressedActions = new List(PressedActions); + return clone; + } + } } } \ No newline at end of file diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs index f50acb4802..c5ba1683dd 100644 --- a/osu.Game/Input/KeyBindingStore.cs +++ b/osu.Game/Input/KeyBindingStore.cs @@ -18,11 +18,11 @@ namespace osu.Game.Input public KeyBindingStore(SQLiteConnection connection, RulesetStore rulesets, Storage storage = null) : base(connection, storage) { - foreach (var info in rulesets.Query()) + foreach (var info in rulesets.AllRulesets) { var ruleset = info.CreateInstance(); foreach (var variant in ruleset.AvailableVariants) - insertDefaults(ruleset.GetDefaultKeyBindings(), info.ID, variant); + insertDefaults(ruleset.GetDefaultKeyBindings(variant), info.ID, variant); } } diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 23268a9ca9..307afb2d2b 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -11,11 +11,11 @@ namespace osu.Game.Online.API /// An API request with a well-defined response type. /// /// Type of the response (used for deserialisation). - public class APIRequest : APIRequest + public abstract class APIRequest : APIRequest { protected override WebRequest CreateWebRequest() => new JsonWebRequest(Uri); - public APIRequest() + protected APIRequest() { base.Success += onSuccess; } @@ -28,10 +28,36 @@ namespace osu.Game.Online.API public new event APISuccessHandler Success; } + public abstract class APIDownloadRequest : APIRequest + { + protected override WebRequest CreateWebRequest() + { + var request = new WebRequest(Uri); + request.DownloadProgress += request_Progress; + return request; + } + + private void request_Progress(WebRequest request, long current, long total) => API.Scheduler.Add(delegate { Progress?.Invoke(current, total); }); + + protected APIDownloadRequest() + { + base.Success += onSuccess; + } + + private void onSuccess() + { + Success?.Invoke(WebRequest.ResponseData); + } + + public event APIProgressHandler Progress; + + public new event APISuccessHandler Success; + } + /// /// AN API request with no specified response type. /// - public class APIRequest + public abstract class APIRequest { /// /// The maximum amount of time before this request will fail. @@ -42,7 +68,7 @@ namespace osu.Game.Online.API protected virtual WebRequest CreateWebRequest() => new WebRequest(Uri); - protected virtual string Uri => $@"{api.Endpoint}/api/v2/{Target}"; + protected virtual string Uri => $@"{API.Endpoint}/api/v2/{Target}"; private double remainingTime => Math.Max(0, Timeout - (DateTime.Now.TotalMilliseconds() - (startTime ?? 0))); @@ -52,7 +78,7 @@ namespace osu.Game.Online.API public double StartTime => startTime ?? -1; - private APIAccess api; + protected APIAccess API; protected WebRequest WebRequest; public event APISuccessHandler Success; @@ -64,7 +90,7 @@ namespace osu.Game.Online.API public void Perform(APIAccess api) { - this.api = api; + API = api; if (checkAndProcessFailure()) return; @@ -109,9 +135,9 @@ namespace osu.Game.Online.API /// Whether we are in a failed or cancelled state. private bool checkAndProcessFailure() { - if (api == null || pendingFailure == null) return cancelled; + if (API == null || pendingFailure == null) return cancelled; - api.Scheduler.Add(pendingFailure); + API.Scheduler.Add(pendingFailure); pendingFailure = null; return true; } @@ -119,5 +145,6 @@ namespace osu.Game.Online.API public delegate void APIFailureHandler(Exception e); public delegate void APISuccessHandler(); + public delegate void APIProgressHandler(long current, long total); public delegate void APISuccessHandler(T content); } diff --git a/osu.Game/Online/API/Requests/GetBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapSetsRequest.cs index ca984d3511..e4763f73ee 100644 --- a/osu.Game/Online/API/Requests/GetBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/GetBeatmapSetsRequest.cs @@ -46,6 +46,9 @@ namespace osu.Game.Online.API.Requests [JsonProperty(@"favourite_count")] private int favouriteCount { get; set; } + [JsonProperty(@"id")] + private int onlineId { get; set; } + [JsonProperty(@"beatmaps")] private IEnumerable beatmaps { get; set; } @@ -53,6 +56,7 @@ namespace osu.Game.Online.API.Requests { return new BeatmapSetInfo { + OnlineBeatmapSetID = onlineId, Metadata = this, OnlineInfo = new BeatmapSetOnlineInfo { diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs index 01685cc7dc..77683ae857 100644 --- a/osu.Game/Online/Chat/Channel.cs +++ b/osu.Game/Online/Chat/Channel.cs @@ -26,9 +26,11 @@ namespace osu.Game.Online.Chat public readonly SortedList Messages = new SortedList(Comparer.Default); + private readonly List pendingMessages = new List(); + public Bindable Joined = new Bindable(); - public bool ReadOnly => Name != "#lazer"; + public bool ReadOnly => false; public const int MAX_HISTORY = 300; @@ -38,6 +40,16 @@ namespace osu.Game.Online.Chat } public event Action> NewMessagesArrived; + public event Action PendingMessageResolved; + public event Action MessageRemoved; + + public void AddLocalEcho(LocalEchoMessage message) + { + pendingMessages.Add(message); + Messages.Add(message); + + NewMessagesArrived?.Invoke(new[] { message }); + } public void AddNewMessages(params Message[] messages) { @@ -52,11 +64,42 @@ namespace osu.Game.Online.Chat private void purgeOldMessages() { - int messageCount = Messages.Count; + // never purge local echos + int messageCount = Messages.Count - pendingMessages.Count; if (messageCount > MAX_HISTORY) Messages.RemoveRange(0, messageCount - MAX_HISTORY); } + /// + /// Replace or remove a message from the channel. + /// + /// The local echo message (client-side). + /// The response message, or null if the message became invalid. + public void ReplaceMessage(LocalEchoMessage echo, Message final) + { + if (!pendingMessages.Remove(echo)) + throw new InvalidOperationException("Attempted to remove echo that wasn't present"); + + Messages.Remove(echo); + + if (final == null) + { + MessageRemoved?.Invoke(echo); + return; + } + + if (Messages.Contains(final)) + { + // message already inserted, so let's throw away this update. + // we may want to handle this better in the future, but for the time being api requests are single-threaded so order is assumed. + MessageRemoved?.Invoke(echo); + return; + } + + Messages.Add(final); + PendingMessageResolved?.Invoke(echo, final); + } + public override string ToString() => Name; } } diff --git a/osu.Game/Online/Chat/LocalEchoMessage.cs b/osu.Game/Online/Chat/LocalEchoMessage.cs new file mode 100644 index 0000000000..079ec58686 --- /dev/null +++ b/osu.Game/Online/Chat/LocalEchoMessage.cs @@ -0,0 +1,12 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Online.Chat +{ + public class LocalEchoMessage : Message + { + public LocalEchoMessage() : base(null) + { + } + } +} diff --git a/osu.Game/Online/Chat/Message.cs b/osu.Game/Online/Chat/Message.cs index 4c7e099647..509861868a 100644 --- a/osu.Game/Online/Chat/Message.cs +++ b/osu.Game/Online/Chat/Message.cs @@ -11,7 +11,7 @@ namespace osu.Game.Online.Chat public class Message : IComparable, IEquatable { [JsonProperty(@"message_id")] - public readonly long Id; + public readonly long? Id; //todo: this should be inside sender. [JsonProperty(@"sender_id")] @@ -37,14 +37,22 @@ namespace osu.Game.Online.Chat { } - public Message(long id) + public Message(long? id) { Id = id; } - public int CompareTo(Message other) => Id.CompareTo(other.Id); + public int CompareTo(Message other) + { + if (!Id.HasValue) + return other.Id.HasValue ? 1 : Timestamp.CompareTo(other.Timestamp); + if (!other.Id.HasValue) + return -1; - public bool Equals(Message other) => Id == other?.Id; + return Id.Value.CompareTo(other.Id.Value); + } + + public virtual bool Equals(Message other) => Id == other?.Id; public override int GetHashCode() => Id.GetHashCode(); } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index e3525e423c..c020675881 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -8,7 +8,6 @@ using osu.Game.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Overlays; -using osu.Framework.Input; using osu.Framework.Logging; using osu.Game.Graphics.UserInterface.Volume; using osu.Framework.Allocation; @@ -160,7 +159,7 @@ namespace osu.Game new VolumeControlReceptor { RelativeSizeAxes = Axes.Both, - ActionRequested = delegate(InputState state) { volume.Adjust(state); } + ActionRequested = action => volume.Adjust(action) }, mainContent = new Container { @@ -220,15 +219,28 @@ namespace osu.Game dependencies.Cache(settings); dependencies.Cache(social); + dependencies.Cache(direct); dependencies.Cache(chat); dependencies.Cache(userProfile); dependencies.Cache(musicController); dependencies.Cache(notificationOverlay); dependencies.Cache(dialogOverlay); - // ensure both overlays aren't presented at the same time - chat.StateChanged += (container, state) => social.State = state == Visibility.Visible ? Visibility.Hidden : social.State; - social.StateChanged += (container, state) => chat.State = state == Visibility.Visible ? Visibility.Hidden : chat.State; + // ensure only one of these overlays are open at once. + var singleDisplayOverlays = new OverlayContainer[] { chat, social, direct }; + foreach (var overlay in singleDisplayOverlays) + { + overlay.StateChanged += state => + { + if (state == Visibility.Hidden) return; + + foreach (var c in singleDisplayOverlays) + { + if (c == overlay) continue; + c.State = Visibility.Hidden; + } + }; + } LoadComponentAsync(Toolbar = new Toolbar { @@ -359,6 +371,13 @@ namespace osu.Game { base.UpdateAfterChildren(); + // we only want to apply these restrictions when we are inside a screen stack. + // the use case for not applying is in visual/unit tests. + bool applyRestrictions = !currentScreen?.AllowBeatmapRulesetChange ?? false; + + Ruleset.Disabled = applyRestrictions; + Beatmap.Disabled = applyRestrictions; + mainContent.Padding = new MarginPadding { Top = ToolbarOffset }; Cursor.State = currentScreen?.HasLocalCursorDisplayed == false ? Visibility.Visible : Visibility.Hidden; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index a5600d1ef7..a7136ce803 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -94,20 +94,22 @@ namespace osu.Game protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); + private SQLiteConnection connection; + [BackgroundDependencyLoader] private void load() { dependencies.Cache(this); dependencies.Cache(LocalConfig); - SQLiteConnection connection = Host.Storage.GetDatabase(@"client"); + connection = Host.Storage.GetDatabase(@"client"); connection.CreateTable(); dependencies.Cache(RulesetStore = new RulesetStore(connection)); dependencies.Cache(FileStore = new FileStore(connection, Host.Storage)); dependencies.Cache(BeatmapManager = new BeatmapManager(Host.Storage, FileStore, connection, RulesetStore, Host)); - dependencies.Cache(ScoreStore = new ScoreStore(Host.Storage, connection, Host, BeatmapManager)); + dependencies.Cache(ScoreStore = new ScoreStore(Host.Storage, connection, Host, BeatmapManager, RulesetStore)); dependencies.Cache(KeyBindingStore = new KeyBindingStore(connection, RulesetStore)); dependencies.Cache(new OsuColour()); @@ -150,14 +152,10 @@ namespace osu.Game Beatmap.ValueChanged += b => { - // compare to last baetmap as sometimes the two may share a track representation (optimisation, see WorkingBeatmap.TransferTo) + // compare to last beatmap as sometimes the two may share a track representation (optimisation, see WorkingBeatmap.TransferTo) if (lastBeatmap?.Track != b.Track) { - // this disposal is done to stop the audio track. - // it may not be exactly what we want for cases beatmaps are reused, as it will - // trigger a fresh load of contained resources. - lastBeatmap?.Dispose(); - + lastBeatmap?.Track?.Dispose(); Audio.Track.AddItem(b.Track); } @@ -237,6 +235,8 @@ namespace osu.Game LocalConfig.Save(); } + connection.Dispose(); + base.Dispose(isDisposing); } } diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index fcebca6fe3..afc6589f57 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -2,26 +2,24 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using OpenTK; +using OpenTK.Graphics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; -using OpenTK; -using OpenTK.Graphics; -using osu.Framework.Graphics.Effects; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Allocation; using osu.Game.Users; -using osu.Game.Graphics.Containers; namespace osu.Game.Overlays.Chat { public class ChatLine : Container { - public readonly Message Message; - - private static readonly Color4[] username_colours = { + private static readonly Color4[] username_colours = + { OsuColour.FromHex("588c7e"), OsuColour.FromHex("b2a367"), OsuColour.FromHex("c98f65"), @@ -69,6 +67,8 @@ namespace osu.Game.Overlays.Chat private Color4 customUsernameColour; + private OsuSpriteText timestamp; + public ChatLine(Message message) { Message = message; @@ -79,6 +79,26 @@ namespace osu.Game.Overlays.Chat Padding = new MarginPadding { Left = padding, Right = padding }; } + private Message message; + private OsuSpriteText username; + private OsuTextFlowContainer contentFlow; + + public Message Message + { + get { return message; } + set + { + if (message == value) return; + + message = value; + + if (!IsLoaded) + return; + + updateMessageContent(); + } + } + [BackgroundDependencyLoader(true)] private void load(OsuColour colours, UserProfileOverlay profile) { @@ -86,49 +106,54 @@ namespace osu.Game.Overlays.Chat loadProfile = u => profile?.ShowUser(u); } + private bool senderHasBackground => !string.IsNullOrEmpty(message.Sender.Colour); + protected override void LoadComplete() { base.LoadComplete(); - bool hasBackground = !string.IsNullOrEmpty(Message.Sender.Colour); - Drawable username = new OsuSpriteText + bool hasBackground = senderHasBackground; + + Drawable effectedUsername = username = new OsuSpriteText { Font = @"Exo2.0-BoldItalic", - Text = $@"{Message.Sender.Username}" + (hasBackground ? "" : ":"), - Colour = hasBackground ? customUsernameColour : username_colours[Message.UserId % username_colours.Length], + Colour = hasBackground ? customUsernameColour : username_colours[message.Sender.Id % username_colours.Length], TextSize = text_size, }; if (hasBackground) { // Background effect - username = username.WithEffect(new EdgeEffect + effectedUsername = new Container { + AutoSizeAxes = Axes.Both, + Masking = true, CornerRadius = 4, - Parameters = new EdgeEffectParameters - { - Radius = 1, - Colour = OsuColour.FromHex(Message.Sender.Colour), - Type = EdgeEffectType.Shadow, - } - }, d => - { - d.Padding = new MarginPadding { Left = 3, Right = 3, Bottom = 1, Top = -3 }; - d.Y = 3; - }) - // Drop shadow effect - .WithEffect(new EdgeEffect - { - CornerRadius = 4, - Parameters = new EdgeEffectParameters + EdgeEffect = new EdgeEffectParameters { Roundness = 1, Offset = new Vector2(0, 3), Radius = 3, Colour = Color4.Black.Opacity(0.3f), Type = EdgeEffectType.Shadow, + }, + // Drop shadow effect + Child = new Container + { + AutoSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 4, + EdgeEffect = new EdgeEffectParameters + { + Radius = 1, + Colour = OsuColour.FromHex(message.Sender.Colour), + Type = EdgeEffectType.Shadow, + }, + Padding = new MarginPadding { Left = 3, Right = 3, Bottom = 1, Top = -3 }, + Y = 3, + Child = username, } - }); + }; } Children = new Drawable[] @@ -138,23 +163,21 @@ namespace osu.Game.Overlays.Chat Size = new Vector2(message_padding, text_size), Children = new Drawable[] { - new OsuSpriteText + timestamp = new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Font = @"Exo2.0-SemiBold", - Text = $@"{Message.Timestamp.LocalDateTime:HH:mm:ss}", FixedWidth = true, TextSize = text_size * 0.75f, - Alpha = 0.4f, }, new ClickableContainer { AutoSizeAxes = Axes.Both, Origin = Anchor.TopRight, Anchor = Anchor.TopRight, - Child = username, - Action = () => loadProfile(Message.Sender), + Child = effectedUsername, + Action = () => loadProfile(message.Sender), }, } }, @@ -165,18 +188,27 @@ namespace osu.Game.Overlays.Chat Padding = new MarginPadding { Left = message_padding + padding }, Children = new Drawable[] { - new OsuTextFlowContainer(t => + contentFlow = new OsuTextFlowContainer(t => { t.TextSize = text_size; }) { - t.TextSize = text_size; - }) - { - Text = Message.Content, AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, } } } }; + + updateMessageContent(); + FinishTransforms(true); + } + + private void updateMessageContent() + { + this.FadeTo(message is LocalEchoMessage ? 0.4f : 1.0f, 500, Easing.OutQuint); + timestamp.FadeTo(message is LocalEchoMessage ? 0 : 1, 500, Easing.OutQuint); + + timestamp.Text = $@"{message.Timestamp.LocalDateTime:HH:mm:ss}"; + username.Text = $@"{message.Sender.Username}" + (senderHasBackground ? "" : ":"); + contentFlow.Text = message.Content; } } } diff --git a/osu.Game/Overlays/Chat/ChatTabControl.cs b/osu.Game/Overlays/Chat/ChatTabControl.cs index 4ff9169877..9f1028c168 100644 --- a/osu.Game/Overlays/Chat/ChatTabControl.cs +++ b/osu.Game/Overlays/Chat/ChatTabControl.cs @@ -16,15 +16,16 @@ using osu.Game.Online.Chat; using OpenTK; using OpenTK.Graphics; using osu.Framework.Configuration; +using System; namespace osu.Game.Overlays.Chat { public class ChatTabControl : OsuTabControl { - protected override TabItem CreateTabItem(Channel value) => new ChannelTabItem(value); - private const float shear_width = 10; + public Action OnRequestLeave; + public readonly Bindable ChannelSelectorActive = new Bindable(); private readonly ChannelTabItem.ChannelSelectorTabItem selectorTab; @@ -49,6 +50,20 @@ namespace osu.Game.Overlays.Chat ChannelSelectorActive.BindTo(selectorTab.Active); } + protected override void AddTabItem(TabItem item, bool addToDropdown = true) + { + if (selectorTab.Depth < float.MaxValue) + // performTabSort might've made selectorTab's position wonky, fix it + TabContainer.ChangeChildDepth(selectorTab, float.MaxValue); + + base.AddTabItem(item, addToDropdown); + + if (SelectedTab == null) + SelectTab(item); + } + + protected override TabItem CreateTabItem(Channel value) => new ChannelTabItem(value) { OnRequestClose = tabCloseRequested }; + protected override void SelectTab(TabItem tab) { if (tab is ChannelTabItem.ChannelSelectorTabItem) @@ -62,18 +77,38 @@ namespace osu.Game.Overlays.Chat base.SelectTab(tab); } + private void tabCloseRequested(TabItem tab) + { + int totalTabs = TabContainer.Count - 1; // account for selectorTab + int currentIndex = MathHelper.Clamp(TabContainer.IndexOf(tab), 1, totalTabs); + + if (tab == SelectedTab && totalTabs > 1) + // Select the tab after tab-to-be-removed's index, or the tab before if current == last + SelectTab(TabContainer[currentIndex == totalTabs ? currentIndex - 1 : currentIndex + 1]); + else if (totalTabs == 1 && !selectorTab.Active) + // Open channel selection overlay if all channel tabs will be closed after removing this tab + SelectTab(selectorTab); + + OnRequestLeave?.Invoke(tab.Value); + } + private class ChannelTabItem : TabItem { private Color4 backgroundInactive; private Color4 backgroundHover; private Color4 backgroundActive; + public override bool IsRemovable => !Pinned; + private readonly SpriteText text; private readonly SpriteText textBold; + private readonly ClickableContainer closeButton; private readonly Box box; private readonly Box highlightBox; private readonly SpriteIcon icon; + public Action OnRequestClose; + private void updateState() { if (Active) @@ -108,6 +143,9 @@ namespace osu.Game.Overlays.Chat protected override bool OnHover(InputState state) { + if (IsRemovable) + closeButton.FadeIn(200, Easing.OutQuint); + if (!Active) box.FadeColour(backgroundHover, transition_length, Easing.OutQuint); return true; @@ -115,6 +153,7 @@ namespace osu.Game.Overlays.Chat protected override void OnHoverLost(InputState state) { + closeButton.FadeOut(200, Easing.OutQuint); updateState(); } @@ -204,13 +243,69 @@ namespace osu.Game.Overlays.Chat Font = @"Exo2.0-Bold", TextSize = 18, }, - } - } + closeButton = new CloseButton + { + Alpha = 0, + Margin = new MarginPadding { Right = 20 }, + Origin = Anchor.CentreRight, + Anchor = Anchor.CentreRight, + Action = delegate + { + if (IsRemovable) OnRequestClose?.Invoke(this); + }, + }, + }, + }, }; } + public class CloseButton : ClickableContainer + { + private readonly SpriteIcon icon; + + public CloseButton() + { + Size = new Vector2(20); + + Child = icon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.75f), + Icon = FontAwesome.fa_close, + RelativeSizeAxes = Axes.Both, + }; + } + + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) + { + icon.ScaleTo(0.5f, 1000, Easing.OutQuint); + return base.OnMouseDown(state, args); + } + + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) + { + icon.ScaleTo(0.75f, 1000, Easing.OutElastic); + return base.OnMouseUp(state, args); + } + + protected override bool OnHover(InputState state) + { + icon.FadeColour(Color4.Red, 200, Easing.OutQuint); + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + icon.FadeColour(Color4.White, 200, Easing.OutQuint); + base.OnHoverLost(state); + } + } + public class ChannelSelectorTabItem : ChannelTabItem { + public override bool IsRemovable => false; + public ChannelSelectorTabItem(Channel value) : base(value) { Depth = float.MaxValue; diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 8a2fa95ed1..840edaece2 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using OpenTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -14,8 +16,19 @@ namespace osu.Game.Overlays.Chat { public class DrawableChannel : Container { + private class ChatLineContainer : FillFlowContainer + { + protected override int Compare(Drawable x, Drawable y) + { + var xC = (ChatLine)x; + var yC = (ChatLine)y; + + return xC.Message.CompareTo(yC.Message); + } + } + public readonly Channel Channel; - private readonly FillFlowContainer flow; + private readonly ChatLineContainer flow; private readonly ScrollContainer scroll; public DrawableChannel(Channel channel) @@ -32,20 +45,19 @@ namespace osu.Game.Overlays.Chat // Some chat lines have effects that slightly protrude to the bottom, // which we do not want to mask away, hence the padding. Padding = new MarginPadding { Bottom = 5 }, - Children = new Drawable[] + Child = flow = new ChatLineContainer { - flow = new FillFlowContainer - { - Direction = FillDirection.Vertical, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Left = 20, Right = 20 } - } - } + Padding = new MarginPadding { Left = 20, Right = 20 }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + }, } }; - channel.NewMessagesArrived += newMessagesArrived; + Channel.NewMessagesArrived += newMessagesArrived; + Channel.MessageRemoved += messageRemoved; + Channel.PendingMessageResolved += pendingMessageResolved; } [BackgroundDependencyLoader] @@ -63,14 +75,17 @@ namespace osu.Game.Overlays.Chat protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); + Channel.NewMessagesArrived -= newMessagesArrived; + Channel.MessageRemoved -= messageRemoved; + Channel.PendingMessageResolved -= pendingMessageResolved; } private void newMessagesArrived(IEnumerable newMessages) { + // Add up to last Channel.MAX_HISTORY messages var displayMessages = newMessages.Skip(Math.Max(0, newMessages.Count() - Channel.MAX_HISTORY)); - //up to last Channel.MAX_HISTORY messages flow.AddRange(displayMessages.Select(m => new ChatLine(m))); if (!IsLoaded) return; @@ -90,6 +105,24 @@ namespace osu.Game.Overlays.Chat } } + private void pendingMessageResolved(Message existing, Message updated) + { + var found = flow.Children.LastOrDefault(c => c.Message == existing); + if (found != null) + { + Trace.Assert(updated.Id.HasValue, "An updated message was returned with no ID."); + + flow.Remove(found); + found.Message = updated; + flow.Add(found); + } + } + + private void messageRemoved(Message removed) + { + flow.Children.FirstOrDefault(c => c.Message == removed)?.FadeColour(Color4.Red, 400).FadeOut(600).Expire(); + } + private void scrollToEnd() => ScheduleAfterChildren(() => scroll.ScrollToEnd()); } } diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index e7a4c1c0ab..b1deae8272 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -6,23 +6,23 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using OpenTK; +using OpenTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; using osu.Framework.Threading; +using osu.Game.Configuration; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Chat; -using osu.Game.Graphics.UserInterface; -using osu.Framework.Graphics.UserInterface; -using OpenTK.Graphics; -using osu.Framework.Input; -using osu.Game.Configuration; -using osu.Game.Graphics; using osu.Game.Overlays.Chat; -using osu.Game.Graphics.Containers; namespace osu.Game.Overlays { @@ -37,7 +37,7 @@ namespace osu.Game.Overlays private readonly LoadingAnimation loading; - private readonly FocusedTextBox inputTextBox; + private readonly FocusedTextBox textbox; private APIAccess api; @@ -130,7 +130,7 @@ namespace osu.Game.Overlays }, Children = new Drawable[] { - inputTextBox = new FocusedTextBox + textbox = new FocusedTextBox { RelativeSizeAxes = Axes.Both, Height = 1, @@ -160,6 +160,7 @@ namespace osu.Game.Overlays channelTabs = new ChatTabControl { RelativeSizeAxes = Axes.Both, + OnRequestLeave = removeChannel, }, } }, @@ -169,13 +170,13 @@ namespace osu.Game.Overlays channelTabs.Current.ValueChanged += newChannel => CurrentChannel = newChannel; channelTabs.ChannelSelectorActive.ValueChanged += value => channelSelection.State = value ? Visibility.Visible : Visibility.Hidden; - channelSelection.StateChanged += (overlay, state) => + channelSelection.StateChanged += state => { channelTabs.ChannelSelectorActive.Value = state == Visibility.Visible; if (state == Visibility.Visible) { - inputTextBox.HoldFocus = false; + textbox.HoldFocus = false; if (1f - chatHeight.Value < channel_selection_min_height) { chatContainer.ResizeHeightTo(1f - channel_selection_min_height, 800, Easing.OutQuint); @@ -186,7 +187,7 @@ namespace osu.Game.Overlays } else { - inputTextBox.HoldFocus = true; + textbox.HoldFocus = true; } }; } @@ -242,8 +243,8 @@ namespace osu.Game.Overlays protected override void OnFocus(InputState state) { - //this is necessary as inputTextBox is masked away and therefore can't get focus :( - GetContainingInputManager().ChangeFocus(inputTextBox); + //this is necessary as textbox is masked away and therefore can't get focus :( + GetContainingInputManager().ChangeFocus(textbox); base.OnFocus(state); } @@ -252,7 +253,7 @@ namespace osu.Game.Overlays this.MoveToY(0, transition_length, Easing.OutQuint); this.FadeIn(transition_length, Easing.OutQuint); - inputTextBox.HoldFocus = true; + textbox.HoldFocus = true; base.PopIn(); } @@ -261,7 +262,7 @@ namespace osu.Game.Overlays this.MoveToY(Height, transition_length, Easing.InSine); this.FadeOut(transition_length, Easing.InSine); - inputTextBox.HoldFocus = false; + textbox.HoldFocus = false; base.PopOut(); } @@ -305,6 +306,7 @@ namespace osu.Game.Overlays addChannel(channels.Find(c => c.Name == @"#lobby")); channelSelection.OnRequestJoin = addChannel; + channelSelection.OnRequestLeave = removeChannel; channelSelection.Sections = new[] { new ChannelSection @@ -332,11 +334,19 @@ namespace osu.Game.Overlays set { - if (currentChannel == value || value == null) return; + if (currentChannel == value) return; + + if (value == null) + { + currentChannel = null; + textbox.Current.Disabled = true; + currentChannelContainer.Clear(false); + return; + } currentChannel = value; - inputTextBox.Current.Disabled = currentChannel.ReadOnly; + textbox.Current.Disabled = currentChannel.ReadOnly; channelTabs.Current.Value = value; var loaded = loadedChannels.Find(d => d.Channel == value); @@ -391,6 +401,19 @@ namespace osu.Game.Overlays channel.Joined.Value = true; } + private void removeChannel(Channel channel) + { + if (channel == null) return; + + if (channel == CurrentChannel) CurrentChannel = null; + + careChannels.Remove(channel); + loadedChannels.Remove(loadedChannels.Find(c => c.Channel == channel)); + channelTabs.RemoveItem(channel); + + channel.Joined.Value = false; + } + private void fetchInitialMessages(Channel channel) { var req = new GetMessagesRequest(new List { channel }, null); @@ -414,6 +437,7 @@ namespace osu.Game.Overlays if (fetchReq != null) return; fetchReq = new GetMessagesRequest(careChannels, lastMessageId); + fetchReq.Success += delegate (List messages) { foreach (var group in messages.Where(m => m.TargetType == TargetType.Channel).GroupBy(m => m.TargetId)) @@ -424,6 +448,7 @@ namespace osu.Game.Overlays Debug.Write("success!"); fetchReq = null; }; + fetchReq.Failure += delegate { Debug.Write("failure!"); @@ -437,51 +462,42 @@ namespace osu.Game.Overlays { var postText = textbox.Text; + textbox.Text = string.Empty; + if (string.IsNullOrEmpty(postText)) return; + var target = currentChannel; + + if (target == null) return; + if (!api.IsLoggedIn) { - currentChannel?.AddNewMessages(new ErrorMessage("Please login to participate in chat!")); - textbox.Text = string.Empty; + target.AddNewMessages(new ErrorMessage("Please login to participate in chat!")); return; } - if (currentChannel == null) return; - if (postText[0] == '/') { // TODO: handle commands - currentChannel.AddNewMessages(new ErrorMessage("Chat commands are not supported yet!")); - textbox.Text = string.Empty; + target.AddNewMessages(new ErrorMessage("Chat commands are not supported yet!")); return; } - var message = new Message + var message = new LocalEchoMessage { Sender = api.LocalUser.Value, Timestamp = DateTimeOffset.Now, - TargetType = TargetType.Channel, //TODO: read this from currentChannel - TargetId = currentChannel.Id, + TargetType = TargetType.Channel, //TODO: read this from channel + TargetId = target.Id, Content = postText }; - textbox.ReadOnly = true; var req = new PostMessageRequest(message); - req.Failure += e => - { - textbox.FlashColour(Color4.Red, 1000); - textbox.ReadOnly = false; - }; - - req.Success += m => - { - currentChannel.AddNewMessages(m); - - textbox.ReadOnly = false; - textbox.Text = string.Empty; - }; + target.AddLocalEcho(message); + req.Failure += e => target.ReplaceMessage(message, null); + req.Success += m => target.ReplaceMessage(message, m); api.Queue(req); } diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index 012e93f10d..7853eefd2c 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -26,7 +26,7 @@ namespace osu.Game.Overlays dialogContainer.Add(currentDialog); currentDialog.Show(); - currentDialog.StateChanged += onDialogOnStateChanged; + currentDialog.StateChanged += state => onDialogOnStateChanged(dialog, state); State = Visibility.Visible; } diff --git a/osu.Game/Overlays/Direct/DirectGridPanel.cs b/osu.Game/Overlays/Direct/DirectGridPanel.cs index 98ab5e88f8..3a9e75bd38 100644 --- a/osu.Game/Overlays/Direct/DirectGridPanel.cs +++ b/osu.Game/Overlays/Direct/DirectGridPanel.cs @@ -12,6 +12,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; +using osu.Framework.Input; namespace osu.Game.Overlays.Direct { @@ -25,23 +26,11 @@ namespace osu.Game.Overlays.Direct public DirectGridPanel(BeatmapSetInfo beatmap) : base(beatmap) { Height = 140 + vertical_padding; //full height of all the elements plus vertical padding (autosize uses the image) - CornerRadius = 4; - Masking = true; - - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Offset = new Vector2(0f, 1f), - Radius = 3f, - Colour = Color4.Black.Opacity(0.25f), - }; } protected override void LoadComplete() { base.LoadComplete(); - - this.FadeInFromZero(200, Easing.Out); bottomPanel.LayoutDuration = 200; bottomPanel.LayoutEasing = Easing.Out; bottomPanel.Origin = Anchor.BottomLeft; @@ -50,14 +39,10 @@ namespace osu.Game.Overlays.Direct [BackgroundDependencyLoader] private void load(OsuColour colours, LocalisationEngine localisation) { - Children = new[] + Content.CornerRadius = 4; + + AddRange(new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - }, - CreateBackground(), new Box { RelativeSizeAxes = Axes.Both, @@ -185,7 +170,13 @@ namespace osu.Game.Overlays.Direct new Statistic(FontAwesome.fa_heart, SetInfo.OnlineInfo?.FavouriteCount ?? 0), }, }, - }; + }); + } + + protected override bool OnClick(InputState state) + { + StartDownload(); + return true; } } } diff --git a/osu.Game/Overlays/Direct/DirectListPanel.cs b/osu.Game/Overlays/Direct/DirectListPanel.cs index 5b45fc7725..b3502b0827 100644 --- a/osu.Game/Overlays/Direct/DirectListPanel.cs +++ b/osu.Game/Overlays/Direct/DirectListPanel.cs @@ -28,35 +28,15 @@ namespace osu.Game.Overlays.Direct { RelativeSizeAxes = Axes.X; Height = height; - CornerRadius = 5; - Masking = true; - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Offset = new Vector2(0f, 1f), - Radius = 3f, - Colour = Color4.Black.Opacity(0.25f), - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - this.FadeInFromZero(200, Easing.Out); } [BackgroundDependencyLoader] private void load(LocalisationEngine localisation) { - Children = new[] + Content.CornerRadius = 5; + + AddRange(new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - }, - CreateBackground(), new Box { RelativeSizeAxes = Axes.Both, @@ -144,10 +124,11 @@ namespace osu.Game.Overlays.Direct Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Size = new Vector2(height - vertical_padding * 2), + Action = StartDownload }, }, }, - }; + }); } private class DownloadButton : OsuClickableContainer diff --git a/osu.Game/Overlays/Direct/DirectPanel.cs b/osu.Game/Overlays/Direct/DirectPanel.cs index 2f048b0e3d..a642f72821 100644 --- a/osu.Game/Overlays/Direct/DirectPanel.cs +++ b/osu.Game/Overlays/Direct/DirectPanel.cs @@ -2,26 +2,220 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using System.IO; +using System.Threading.Tasks; using OpenTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using OpenTK.Graphics; +using osu.Framework.Input; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Framework.Logging; +using osu.Game.Beatmaps.IO; +using osu.Game.Overlays.Notifications; namespace osu.Game.Overlays.Direct { public abstract class DirectPanel : Container { - protected readonly BeatmapSetInfo SetInfo; + public readonly BeatmapSetInfo SetInfo; + + protected Box BlackBackground; + + private const double hover_transition_time = 400; + + private Container content; + + private APIAccess api; + private ProgressBar progressBar; + private BeatmapManager beatmaps; + private NotificationOverlay notifications; + + protected override Container Content => content; protected DirectPanel(BeatmapSetInfo setInfo) { SetInfo = setInfo; } + private readonly EdgeEffectParameters edgeEffectNormal = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Offset = new Vector2(0f, 1f), + Radius = 2f, + Colour = Color4.Black.Opacity(0.25f), + }; + + private readonly EdgeEffectParameters edgeEffectHovered = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Offset = new Vector2(0f, 5f), + Radius = 10f, + Colour = Color4.Black.Opacity(0.3f), + }; + + + [BackgroundDependencyLoader(permitNulls: true)] + private void load(APIAccess api, BeatmapManager beatmaps, OsuColour colours, NotificationOverlay notifications) + { + this.api = api; + this.beatmaps = beatmaps; + this.notifications = notifications; + + AddInternal(content = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + EdgeEffect = edgeEffectNormal, + Children = new[] + { + // temporary blackness until the actual background loads. + BlackBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + }, + CreateBackground(), + progressBar = new ProgressBar + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Height = 0, + Alpha = 0, + BackgroundColour = Color4.Black.Opacity(0.7f), + FillColour = colours.Blue, + Depth = -1, + }, + } + }); + } + + protected override bool OnHover(InputState state) + { + content.TweenEdgeEffectTo(edgeEffectHovered, hover_transition_time, Easing.OutQuint); + content.MoveToY(-4, hover_transition_time, Easing.OutQuint); + + return base.OnHover(state); + } + + protected override void OnHoverLost(InputState state) + { + content.TweenEdgeEffectTo(edgeEffectNormal, hover_transition_time, Easing.OutQuint); + content.MoveToY(0, hover_transition_time, Easing.OutQuint); + + base.OnHoverLost(state); + } + + // this should eventually be moved to a more central place, like BeatmapManager. + private DownloadBeatmapSetRequest downloadRequest; + + protected void StartDownload() + { + if (api == null) return; + + // we already have an active download running. + if (downloadRequest != null) + { + content.MoveToX(-5, 50, Easing.OutSine).Then() + .MoveToX(5, 100, Easing.InOutSine).Then() + .MoveToX(-5, 100, Easing.InOutSine).Then() + .MoveToX(0, 50, Easing.InSine).Then(); + return; + } + + if (!api.LocalUser.Value.IsSupporter) + { + notifications.Post(new SimpleNotification + { + Icon = FontAwesome.fa_superpowers, + Text = "You gotta be a supporter to download for now 'yo" + }); + return; + } + + progressBar.FadeIn(400, Easing.OutQuint); + progressBar.ResizeHeightTo(4, 400, Easing.OutQuint); + + progressBar.Current.Value = 0; + + ProgressNotification downloadNotification = new ProgressNotification + { + Text = $"Downloading {SetInfo.Metadata.Artist} - {SetInfo.Metadata.Title}", + }; + + downloadRequest = new DownloadBeatmapSetRequest(SetInfo); + downloadRequest.Failure += e => + { + progressBar.Current.Value = 0; + progressBar.FadeOut(500); + downloadNotification.State = ProgressNotificationState.Completed; + Logger.Error(e, "Failed to get beatmap download information"); + + downloadRequest = null; + }; + + downloadRequest.Progress += (current, total) => + { + float progress = (float)current / total; + + progressBar.Current.Value = progress; + + downloadNotification.State = ProgressNotificationState.Active; + downloadNotification.Progress = progress; + }; + + downloadRequest.Success += data => + { + progressBar.Current.Value = 1; + progressBar.FadeOut(500); + + downloadNotification.State = ProgressNotificationState.Completed; + + using (var stream = new MemoryStream(data)) + using (var archive = new OszArchiveReader(stream)) + beatmaps.Import(archive); + }; + + downloadNotification.CancelRequested += () => + { + downloadRequest.Cancel(); + downloadRequest = null; + return true; + }; + + notifications.Post(downloadNotification); + + // don't run in the main api queue as this is a long-running task. + Task.Run(() => downloadRequest.Perform(api)); + } + + public class DownloadBeatmapSetRequest : APIDownloadRequest + { + private readonly BeatmapSetInfo beatmapSet; + + public DownloadBeatmapSetRequest(BeatmapSetInfo beatmapSet) + { + this.beatmapSet = beatmapSet; + } + + protected override string Target => $@"beatmapsets/{beatmapSet.OnlineBeatmapSetID}/download"; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + this.FadeInFromZero(200, Easing.Out); + } + protected List GetDifficultyIcons() { var icons = new List(); @@ -38,7 +232,11 @@ namespace osu.Game.Overlays.Direct Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fill, - OnLoadComplete = d => d.FadeInFromZero(400, Easing.Out), + OnLoadComplete = d => + { + d.FadeInFromZero(400, Easing.Out); + BlackBackground.Delay(400).FadeOut(); + }, }) { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game/Overlays/DirectOverlay.cs b/osu.Game/Overlays/DirectOverlay.cs index b1c7dab778..f734e43826 100644 --- a/osu.Game/Overlays/DirectOverlay.cs +++ b/osu.Game/Overlays/DirectOverlay.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays private readonly FillFlowContainer resultCountsContainer; private readonly OsuSpriteText resultCountsText; - private readonly FillFlowContainer panels; + private FillFlowContainer panels; protected override Color4 BackgroundColour => OsuColour.FromHex(@"485e74"); protected override Color4 TrianglesColourLight => OsuColour.FromHex(@"465b71"); @@ -48,17 +48,6 @@ namespace osu.Game.Overlays if (beatmapSets?.Equals(value) ?? false) return; beatmapSets = value; - if (BeatmapSets == null) - { - foreach (var p in panels.Children) - { - p.FadeOut(200); - p.Expire(); - } - - return; - } - recreatePanels(Filter.DisplayStyleControl.DisplayStyle.Value); } } @@ -108,13 +97,6 @@ namespace osu.Game.Overlays }, } }, - panels = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(panel_padding), - Margin = new MarginPadding { Top = 10 }, - }, }; Filter.Search.Current.ValueChanged += text => { if (text != string.Empty) Header.Tabs.Current.Value = DirectTab.Search; }; @@ -161,11 +143,19 @@ namespace osu.Game.Overlays } [BackgroundDependencyLoader] - private void load(OsuColour colours, APIAccess api, RulesetStore rulesets) + private void load(OsuColour colours, APIAccess api, RulesetStore rulesets, BeatmapManager beatmaps) { this.api = api; this.rulesets = rulesets; resultCountsContainer.Colour = colours.Yellow; + + beatmaps.BeatmapSetAdded += setAdded; + } + + private void setAdded(BeatmapSetInfo set) + { + // if a new map was imported, we should remove it from search results (download completed etc.) + panels?.FirstOrDefault(p => p.SetInfo.OnlineBeatmapSetID == set.OnlineBeatmapSetID)?.FadeOut(400).Expire(); } private void updateResultCounts() @@ -185,18 +175,38 @@ namespace osu.Game.Overlays private void recreatePanels(PanelDisplayStyle displayStyle) { + if (panels != null) + { + panels.FadeOut(200); + panels.Expire(); + panels = null; + } + if (BeatmapSets == null) return; - panels.ChildrenEnumerable = BeatmapSets.Select(b => - { - switch (displayStyle) - { - case PanelDisplayStyle.Grid: - return new DirectGridPanel(b) { Width = 400 }; - default: - return new DirectListPanel(b); - } - }); + var newPanels = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(panel_padding), + Margin = new MarginPadding { Top = 10 }, + ChildrenEnumerable = BeatmapSets.Select(b => + { + switch (displayStyle) + { + case PanelDisplayStyle.Grid: + return new DirectGridPanel(b) { Width = 400 }; + default: + return new DirectListPanel(b); + } + }) + }; + + LoadComponentAsync(newPanels, p => + { + if (panels != null) ScrollFlow.Remove(panels); + ScrollFlow.Add(panels = newPanels); + }); } private GetBeatmapSetsRequest getSetsRequest; diff --git a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs index 7dd9919e5d..8ebd4ac545 100644 --- a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs +++ b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs @@ -3,21 +3,29 @@ using osu.Framework.Input.Bindings; using osu.Game.Graphics; +using osu.Game.Overlays.Settings; namespace osu.Game.Overlays.KeyBinding { - public class GlobalKeyBindingsSection : KeyBindingsSection + public class GlobalKeyBindingsSection : SettingsSection { - private readonly string name; + public override FontAwesome Icon => FontAwesome.fa_osu_hot; + public override string Header => "Global"; - public override FontAwesome Icon => FontAwesome.fa_osu_mod_nofail; - public override string Header => name; - - public GlobalKeyBindingsSection(KeyBindingInputManager manager, string name) + public GlobalKeyBindingsSection(KeyBindingInputManager manager) { - this.name = name; + Add(new DefaultBindingsSubsection(manager)); + } - Defaults = manager.DefaultKeyBindings; + private class DefaultBindingsSubsection : KeyBindingsSubsection + { + protected override string Header => string.Empty; + + public DefaultBindingsSubsection(KeyBindingInputManager manager) + : base(null) + { + Defaults = manager.DefaultKeyBindings; + } } } } \ No newline at end of file diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index a3361743a1..046e56573f 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -1,7 +1,6 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -22,7 +21,7 @@ namespace osu.Game.Overlays.KeyBinding { internal class KeyBindingRow : Container, IFilterable { - private readonly Enum action; + private readonly object action; private readonly IEnumerable bindings; private const float transition_time = 150; @@ -50,7 +49,7 @@ namespace osu.Game.Overlays.KeyBinding public string[] FilterTerms => new[] { text.Text }.Concat(bindings.Select(b => b.KeyCombination.ReadableString())).ToArray(); - public KeyBindingRow(Enum action, IEnumerable bindings) + public KeyBindingRow(object action, IEnumerable bindings) { this.action = action; this.bindings = bindings; @@ -110,16 +109,27 @@ namespace osu.Game.Overlays.KeyBinding buttons.Add(new KeyButton(b)); } + public void RestoreDefaults() + { + int i = 0; + foreach (var d in Defaults) + { + var button = buttons[i++]; + button.UpdateKeyCombination(d); + store.Update(button.KeyBinding); + } + } + protected override bool OnHover(InputState state) { - this.FadeEdgeEffectTo(1, transition_time, Easing.OutQuint); + FadeEdgeEffectTo(1, transition_time, Easing.OutQuint); return base.OnHover(state); } protected override void OnHoverLost(InputState state) { - this.FadeEdgeEffectTo(0, transition_time, Easing.OutQuint); + FadeEdgeEffectTo(0, transition_time, Easing.OutQuint); base.OnHoverLost(state); } @@ -130,46 +140,57 @@ namespace osu.Game.Overlays.KeyBinding public bool AllowMainMouseButtons; + public IEnumerable Defaults; + private bool isModifier(Key k) => k < Key.F1; protected override bool OnClick(InputState state) => true; protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) { - if (HasFocus) - { - if (bindTarget.IsHovered) - { - if (!AllowMainMouseButtons) - { - switch (args.Button) - { - case MouseButton.Left: - case MouseButton.Right: - return true; - } - } + if (!HasFocus || !bindTarget.IsHovered) + return base.OnMouseDown(state, args); - bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(state)); - return true; + if (!AllowMainMouseButtons) + { + switch (args.Button) + { + case MouseButton.Left: + case MouseButton.Right: + return true; } } - return base.OnMouseDown(state, args); + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(state)); + return true; } protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) { - if (HasFocus && !state.Mouse.Buttons.Any()) + // don't do anything until the last button is released. + if (!HasFocus || state.Mouse.Buttons.Any()) + return base.OnMouseUp(state, args); + + if (bindTarget.IsHovered) + finalise(); + else + updateBindTarget(); + return true; + } + + protected override bool OnWheel(InputState state) + { + if (HasFocus) { if (bindTarget.IsHovered) + { + bindTarget.UpdateKeyCombination(KeyCombination.FromInputState(state)); finalise(); - else - updateBindTarget(); - return true; + return true; + } } - return base.OnMouseUp(state, args); + return base.OnWheel(state); } protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) @@ -196,13 +217,10 @@ namespace osu.Game.Overlays.KeyBinding protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) { - if (HasFocus) - { - finalise(); - return true; - } + if (!HasFocus) return base.OnKeyUp(state, args); - return base.OnKeyUp(state, args); + finalise(); + return true; } private void finalise() diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSection.cs deleted file mode 100644 index 44c28ee9c8..0000000000 --- a/osu.Game/Overlays/KeyBinding/KeyBindingsSection.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . -// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Allocation; -using osu.Game.Input; -using osu.Game.Overlays.Settings; -using osu.Game.Rulesets; -using OpenTK; - -namespace osu.Game.Overlays.KeyBinding -{ - public abstract class KeyBindingsSection : SettingsSection - { - protected IEnumerable Defaults; - - protected RulesetInfo Ruleset; - - protected KeyBindingsSection() - { - FlowContent.Spacing = new Vector2(0, 1); - } - - [BackgroundDependencyLoader] - private void load(KeyBindingStore store) - { - var enumType = Defaults?.FirstOrDefault()?.Action?.GetType(); - - if (enumType == null) return; - - // for now let's just assume a variant of zero. - // this will need to be implemented in a better way in the future. - int? variant = null; - if (Ruleset != null) - variant = 0; - - var bindings = store.Query(Ruleset?.ID, variant); - - foreach (Enum v in Enum.GetValues(enumType)) - // one row per valid action. - Add(new KeyBindingRow(v, bindings.Where(b => b.Action.Equals((int)(object)v))) - { - AllowMainMouseButtons = Ruleset != null - }); - } - } -} \ No newline at end of file diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs new file mode 100644 index 0000000000..bd69403831 --- /dev/null +++ b/osu.Game/Overlays/KeyBinding/KeyBindingsSubsection.cs @@ -0,0 +1,72 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Input; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets; +using OpenTK; +using osu.Game.Graphics; + +namespace osu.Game.Overlays.KeyBinding +{ + public abstract class KeyBindingsSubsection : SettingsSubsection + { + protected IEnumerable Defaults; + + protected RulesetInfo Ruleset; + + private readonly int? variant; + + protected KeyBindingsSubsection(int? variant) + { + this.variant = variant; + + FlowContent.Spacing = new Vector2(0, 1); + } + + [BackgroundDependencyLoader] + private void load(KeyBindingStore store) + { + var bindings = store.Query(Ruleset?.ID, variant); + + foreach (var defaultGroup in Defaults.GroupBy(d => d.Action)) + { + // one row per valid action. + Add(new KeyBindingRow(defaultGroup.Key, bindings.Where(b => b.Action.Equals((int)defaultGroup.Key))) + { + AllowMainMouseButtons = Ruleset != null, + Defaults = defaultGroup.Select(d => d.KeyCombination) + }); + } + + Add(new ResetButton + { + Action = () => Children.OfType().ForEach(k => k.RestoreDefaults()) + }); + } + } + + internal class ResetButton : OsuButton + { + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Text = "Reset"; + RelativeSizeAxes = Axes.X; + Margin = new MarginPadding { Top = 5 }; + Height = 20; + + Content.CornerRadius = 5; + + BackgroundColour = colours.PinkDark; + Triangles.ColourDark = colours.PinkDarker; + Triangles.ColourLight = colours.Pink; + } + } +} diff --git a/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs b/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs index 20941115e3..885e149cb2 100644 --- a/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs +++ b/osu.Game/Overlays/KeyBinding/RulesetBindingsSection.cs @@ -2,20 +2,26 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Game.Graphics; +using osu.Game.Overlays.Settings; using osu.Game.Rulesets; namespace osu.Game.Overlays.KeyBinding { - public class RulesetBindingsSection : KeyBindingsSection + public class RulesetBindingsSection : SettingsSection { - public override FontAwesome Icon => FontAwesome.fa_osu_mod_nofail; - public override string Header => Ruleset.Name; + public override FontAwesome Icon => FontAwesome.fa_osu_hot; + public override string Header => ruleset.Name; + + private readonly RulesetInfo ruleset; public RulesetBindingsSection(RulesetInfo ruleset) { - Ruleset = ruleset; + this.ruleset = ruleset; - Defaults = ruleset.CreateInstance().GetDefaultKeyBindings(); + var r = ruleset.CreateInstance(); + + foreach (var variant in r.AvailableVariants) + Add(new VariantBindingsSubsection(ruleset, variant)); } } } \ No newline at end of file diff --git a/osu.Game/Overlays/KeyBinding/VariantBindingsSubsection.cs b/osu.Game/Overlays/KeyBinding/VariantBindingsSubsection.cs new file mode 100644 index 0000000000..dca5f53b4a --- /dev/null +++ b/osu.Game/Overlays/KeyBinding/VariantBindingsSubsection.cs @@ -0,0 +1,24 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Rulesets; + +namespace osu.Game.Overlays.KeyBinding +{ + public class VariantBindingsSubsection : KeyBindingsSubsection + { + protected override string Header => variantName; + private readonly string variantName; + + public VariantBindingsSubsection(RulesetInfo ruleset, int variant) + : base(variant) + { + Ruleset = ruleset; + + var rulesetInstance = ruleset.CreateInstance(); + + variantName = rulesetInstance.GetVariantName(variant); + Defaults = rulesetInstance.GetDefaultKeyBindings(variant); + } + } +} \ No newline at end of file diff --git a/osu.Game/Overlays/KeyBindingOverlay.cs b/osu.Game/Overlays/KeyBindingOverlay.cs index 7d6ef7ffa6..72c653030c 100644 --- a/osu.Game/Overlays/KeyBindingOverlay.cs +++ b/osu.Game/Overlays/KeyBindingOverlay.cs @@ -17,9 +17,9 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader(permitNulls: true)] private void load(RulesetStore rulesets, GlobalKeyBindingInputManager global) { - AddSection(new GlobalKeyBindingsSection(global, "Global")); + AddSection(new GlobalKeyBindingsSection(global)); - foreach (var ruleset in rulesets.Query()) + foreach (var ruleset in rulesets.AllRulesets) AddSection(new RulesetBindingsSection(ruleset)); } diff --git a/osu.Game/Overlays/MainSettings.cs b/osu.Game/Overlays/MainSettings.cs index b4d9cac045..4fe86d62fd 100644 --- a/osu.Game/Overlays/MainSettings.cs +++ b/osu.Game/Overlays/MainSettings.cs @@ -53,7 +53,7 @@ namespace osu.Game.Overlays private const float hidden_width = 120; - private void keyBindingOverlay_StateChanged(VisibilityContainer container, Visibility visibility) + private void keyBindingOverlay_StateChanged(Visibility visibility) { switch (visibility) { diff --git a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs index 56b26e7176..3ac8af7b2b 100644 --- a/osu.Game/Overlays/MedalSplash/DrawableMedal.cs +++ b/osu.Game/Overlays/MedalSplash/DrawableMedal.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using osu.Framework; using OpenTK; using osu.Framework.Allocation; @@ -19,6 +20,8 @@ namespace osu.Game.Overlays.MedalSplash private const float scale_when_unlocked = 0.76f; private const float scale_when_full = 0.6f; + public event Action StateChanged; + private readonly Medal medal; private readonly Container medalContainer; private readonly Sprite medalSprite, medalGlow; @@ -132,6 +135,8 @@ namespace osu.Game.Overlays.MedalSplash state = value; updateState(); + + StateChanged?.Invoke(State); } } diff --git a/osu.Game/Overlays/Music/CollectionsDropdown.cs b/osu.Game/Overlays/Music/CollectionsDropdown.cs index 0c0a636be8..3b11d9f52a 100644 --- a/osu.Game/Overlays/Music/CollectionsDropdown.cs +++ b/osu.Game/Overlays/Music/CollectionsDropdown.cs @@ -15,15 +15,37 @@ namespace osu.Game.Overlays.Music { public class CollectionsDropdown : OsuDropdown { - protected override DropdownHeader CreateHeader() => new CollectionsHeader { AccentColour = AccentColour }; - protected override Menu CreateMenu() => new CollectionsMenu(); - [BackgroundDependencyLoader] private void load(OsuColour colours) { AccentColour = colours.Gray6; } + protected override DropdownHeader CreateHeader() => new CollectionsHeader(); + + protected override DropdownMenu CreateMenu() => new CollectionsMenu(); + + private class CollectionsMenu : OsuDropdownMenu + { + public CollectionsMenu() + { + CornerRadius = 5; + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.3f), + Radius = 3, + Offset = new Vector2(0f, 1f), + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = colours.Gray4; + } + } + private class CollectionsHeader : OsuDropdownHeader { [BackgroundDependencyLoader] @@ -48,26 +70,5 @@ namespace osu.Game.Overlays.Music }; } } - - private class CollectionsMenu : OsuMenu - { - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Background.Colour = colours.Gray4; - } - - public CollectionsMenu() - { - CornerRadius = 5; - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(0.3f), - Radius = 3, - Offset = new Vector2(0f, 1f), - }; - } - } } } diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 4145a8d1f0..2aaa182685 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -16,7 +17,7 @@ using OpenTK; namespace osu.Game.Overlays.Music { - internal class PlaylistItem : Container, IFilterable + internal class PlaylistItem : Container, IFilterable, IDraggable { private const float fade_duration = 100; @@ -33,6 +34,8 @@ namespace osu.Game.Overlays.Music public Action OnSelect; + public bool IsDraggable => handle.IsHovered; + private bool selected; public bool Selected { @@ -68,15 +71,9 @@ namespace osu.Game.Overlays.Music Children = new Drawable[] { - handle = new SpriteIcon + handle = new PlaylistItemHandle { - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - Size = new Vector2(12), - Colour = colours.Gray5, - Icon = FontAwesome.fa_bars, - Alpha = 0f, - Margin = new MarginPadding { Left = 5, Top = 2 }, + Colour = colours.Gray5 }, text = new OsuTextFlowContainer { @@ -114,19 +111,19 @@ namespace osu.Game.Overlays.Music }); } - protected override bool OnHover(Framework.Input.InputState state) + protected override bool OnHover(InputState state) { handle.FadeIn(fade_duration); return base.OnHover(state); } - protected override void OnHoverLost(Framework.Input.InputState state) + protected override void OnHoverLost(InputState state) { handle.FadeOut(fade_duration); } - protected override bool OnClick(Framework.Input.InputState state) + protected override bool OnClick(InputState state) { OnSelect?.Invoke(BeatmapSetInfo); return true; @@ -148,5 +145,27 @@ namespace osu.Game.Overlays.Music this.FadeTo(matching ? 1 : 0, 200); } } + + private class PlaylistItemHandle : SpriteIcon + { + + public PlaylistItemHandle() + { + Anchor = Anchor.TopLeft; + Origin = Anchor.TopLeft; + Size = new Vector2(12); + Icon = FontAwesome.fa_bars; + Alpha = 0f; + Margin = new MarginPadding { Left = 5, Top = 2 }; + } + } + } + + public interface IDraggable : IDrawable + { + /// + /// Whether this can be dragged in its current state. + /// + bool IsDraggable { get; } } } diff --git a/osu.Game/Overlays/Music/PlaylistList.cs b/osu.Game/Overlays/Music/PlaylistList.cs index 3dd514edeb..360e2ad843 100644 --- a/osu.Game/Overlays/Music/PlaylistList.cs +++ b/osu.Game/Overlays/Music/PlaylistList.cs @@ -4,105 +4,252 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; +using OpenTK; namespace osu.Game.Overlays.Music { - internal class PlaylistList : Container + internal class PlaylistList : CompositeDrawable { - private readonly FillFlowContainer items; - - public IEnumerable BeatmapSets - { - set - { - items.Children = value.Select(item => new PlaylistItem(item) { OnSelect = itemSelected }).ToList(); - } - } - - public BeatmapSetInfo FirstVisibleSet => items.Children.FirstOrDefault(i => i.MatchingFilter)?.BeatmapSetInfo; - - private void itemSelected(BeatmapSetInfo b) - { - OnSelect?.Invoke(b); - } - public Action OnSelect; - private readonly SearchContainer search; - - public void Filter(string searchTerm) => search.SearchTerm = searchTerm; - - public BeatmapSetInfo SelectedItem - { - get { return items.Children.FirstOrDefault(i => i.Selected)?.BeatmapSetInfo; } - set - { - foreach (PlaylistItem s in items.Children) - s.Selected = s.BeatmapSetInfo.ID == value?.ID; - } - } + private readonly ItemsScrollContainer items; public PlaylistList() { - Children = new Drawable[] + InternalChild = items = new ItemsScrollContainer { - new OsuScrollContainer - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - search = new SearchContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - items = new ItemSearchContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - }, - } - } - }, - }, + RelativeSizeAxes = Axes.Both, + OnSelect = set => OnSelect?.Invoke(set) }; } - public void AddBeatmapSet(BeatmapSetInfo beatmapSet) + public new MarginPadding Padding { - items.Add(new PlaylistItem(beatmapSet) { OnSelect = itemSelected }); + get { return base.Padding; } + set { base.Padding = value; } } - public void RemoveBeatmapSet(BeatmapSetInfo beatmapSet) + public IEnumerable BeatmapSets { set { items.Sets = value; } } + + public BeatmapSetInfo FirstVisibleSet => items.FirstVisibleSet; + public BeatmapSetInfo NextSet => items.NextSet; + public BeatmapSetInfo PreviousSet => items.PreviousSet; + + public BeatmapSetInfo SelectedSet { - PlaylistItem itemToRemove = items.Children.FirstOrDefault(item => item.BeatmapSetInfo.ID == beatmapSet.ID); - if (itemToRemove != null) items.Remove(itemToRemove); + get { return items.SelectedSet; } + set { items.SelectedSet = value; } } - private class ItemSearchContainer : FillFlowContainer, IHasFilterableChildren + public void AddBeatmapSet(BeatmapSetInfo beatmapSet) => items.AddBeatmapSet(beatmapSet); + public bool RemoveBeatmapSet(BeatmapSetInfo beatmapSet) => items.RemoveBeatmapSet(beatmapSet); + + public void Filter(string searchTerm) => items.SearchTerm = searchTerm; + + private class ItemsScrollContainer : OsuScrollContainer { - public string[] FilterTerms => new string[] { }; - public bool MatchingFilter + public Action OnSelect; + + private readonly SearchContainer search; + private readonly FillFlowContainer items; + + public ItemsScrollContainer() + { + Children = new Drawable[] + { + search = new SearchContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + items = new ItemSearchContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + } + } + }; + } + + public IEnumerable Sets { set { - if (value) - InvalidateLayout(); + items.Clear(); + value.ForEach(AddBeatmapSet); } } - public IEnumerable FilterableChildren => Children; - - public ItemSearchContainer() + public string SearchTerm { - LayoutDuration = 200; - LayoutEasing = Easing.OutQuint; + get { return search.SearchTerm; } + set { search.SearchTerm = value; } + } + + public void AddBeatmapSet(BeatmapSetInfo beatmapSet) + { + items.Add(new PlaylistItem(beatmapSet) + { + OnSelect = set => OnSelect?.Invoke(set), + Depth = items.Count + }); + } + + public bool RemoveBeatmapSet(BeatmapSetInfo beatmapSet) + { + var itemToRemove = items.FirstOrDefault(i => i.BeatmapSetInfo.ID == beatmapSet.ID); + if (itemToRemove == null) + return false; + return items.Remove(itemToRemove); + } + + public BeatmapSetInfo SelectedSet + { + get { return items.FirstOrDefault(i => i.Selected)?.BeatmapSetInfo; } + set + { + foreach (PlaylistItem s in items.Children) + s.Selected = s.BeatmapSetInfo.ID == value?.ID; + } + } + + public BeatmapSetInfo FirstVisibleSet => items.FirstOrDefault(i => i.MatchingFilter)?.BeatmapSetInfo; + public BeatmapSetInfo NextSet => (items.SkipWhile(i => !i.Selected).Skip(1).FirstOrDefault() ?? items.FirstOrDefault())?.BeatmapSetInfo; + public BeatmapSetInfo PreviousSet => (items.TakeWhile(i => !i.Selected).LastOrDefault() ?? items.LastOrDefault())?.BeatmapSetInfo; + + private Vector2 nativeDragPosition; + private PlaylistItem draggedItem; + + protected override bool OnDragStart(InputState state) + { + nativeDragPosition = state.Mouse.NativeState.Position; + draggedItem = items.FirstOrDefault(d => d.IsDraggable); + return draggedItem != null || base.OnDragStart(state); + } + + protected override bool OnDrag(InputState state) + { + nativeDragPosition = state.Mouse.NativeState.Position; + if (draggedItem == null) + return base.OnDrag(state); + return true; + } + + protected override bool OnDragEnd(InputState state) + { + nativeDragPosition = state.Mouse.NativeState.Position; + var handled = draggedItem != null || base.OnDragEnd(state); + draggedItem = null; + + return handled; + } + + protected override void Update() + { + base.Update(); + + if (draggedItem == null) + return; + + updateScrollPosition(); + updateDragPosition(); + } + + private void updateScrollPosition() + { + const float start_offset = 10; + const double max_power = 50; + const double exp_base = 1.05; + + var localPos = ToLocalSpace(nativeDragPosition); + + if (localPos.Y < start_offset) + { + if (Current <= 0) + return; + + var power = Math.Min(max_power, Math.Abs(start_offset - localPos.Y)); + ScrollBy(-(float)Math.Pow(exp_base, power)); + } + else if (localPos.Y > DrawHeight - start_offset) + { + if (IsScrolledToEnd()) + return; + + var power = Math.Min(max_power, Math.Abs(DrawHeight - start_offset - localPos.Y)); + ScrollBy((float)Math.Pow(exp_base, power)); + } + } + + private void updateDragPosition() + { + var itemsPos = items.ToLocalSpace(nativeDragPosition); + + int srcIndex = (int)draggedItem.Depth; + + // Find the last item with position < mouse position. Note we can't directly use + // the item positions as they are being transformed + float heightAccumulator = 0; + int dstIndex = 0; + for (; dstIndex < items.Count; dstIndex++) + { + // Using BoundingBox here takes care of scale, paddings, etc... + heightAccumulator += items[dstIndex].BoundingBox.Height; + if (heightAccumulator > itemsPos.Y) + break; + } + + dstIndex = MathHelper.Clamp(dstIndex, 0, items.Count - 1); + + if (srcIndex == dstIndex) + return; + + if (srcIndex < dstIndex) + { + for (int i = srcIndex + 1; i <= dstIndex; i++) + items.ChangeChildDepth(items[i], i - 1); + } + else + { + for (int i = dstIndex; i < srcIndex; i++) + items.ChangeChildDepth(items[i], i + 1); + } + + items.ChangeChildDepth(draggedItem, dstIndex); + } + + + private class ItemSearchContainer : FillFlowContainer, IHasFilterableChildren + { + public string[] FilterTerms => new string[] { }; + public bool MatchingFilter + { + set + { + if (value) + InvalidateLayout(); + } + } + + // Compare with reversed ChildID and Depth + protected override int Compare(Drawable x, Drawable y) => base.Compare(y, x); + + public IEnumerable FilterableChildren => Children; + + public ItemSearchContainer() + { + LayoutDuration = 200; + LayoutEasing = Easing.OutQuint; + } } } } -} \ No newline at end of file +} diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 83e92c5554..d05ad85726 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -92,7 +92,7 @@ namespace osu.Game.Overlays.Music protected override void LoadComplete() { base.LoadComplete(); - beatmapBacking.ValueChanged += b => list.SelectedItem = b?.BeatmapSetInfo; + beatmapBacking.ValueChanged += b => list.SelectedSet = b?.BeatmapSetInfo; beatmapBacking.TriggerChange(); } @@ -126,24 +126,24 @@ namespace osu.Game.Overlays.Music public void PlayPrevious() { - var currentID = beatmapBacking.Value?.BeatmapSetInfo.ID ?? -1; - var available = BeatmapSets.Reverse(); - - var playable = available.SkipWhile(b => b.ID != currentID).Skip(1).FirstOrDefault() ?? available.FirstOrDefault(); + var playable = list.PreviousSet; if (playable != null) + { playSpecified(playable.Beatmaps[0]); + list.SelectedSet = playable; + } } public void PlayNext() { - var currentID = beatmapBacking.Value?.BeatmapSetInfo.ID ?? -1; - var available = BeatmapSets; - - var playable = available.SkipWhile(b => b.ID != currentID).Skip(1).FirstOrDefault() ?? available.FirstOrDefault(); + var playable = list.NextSet; if (playable != null) + { playSpecified(playable.Beatmaps[0]); + list.SelectedSet = playable; + } } private void playSpecified(BeatmapInfo info) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index d970089942..0a06439c3e 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -15,7 +15,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; -using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Localisation; using osu.Framework.Threading; @@ -205,7 +204,7 @@ namespace osu.Game.Overlays beatmapBacking.BindTo(game.Beatmap); - playlist.StateChanged += (c, s) => playlistButton.FadeColour(s == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint); + playlist.StateChanged += s => playlistButton.FadeColour(s == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint); } protected override void LoadComplete() @@ -349,23 +348,23 @@ namespace osu.Game.Overlays playerContainer.Add(new AsyncLoadWrapper(new Background(beatmap) { - OnLoadComplete = d => + OnLoadComplete = newBackground => { switch (direction) { case TransformDirection.Next: - d.Position = new Vector2(400, 0); - d.MoveToX(0, 500, Easing.OutCubic); + newBackground.Position = new Vector2(400, 0); + newBackground.MoveToX(0, 500, Easing.OutCubic); currentBackground.MoveToX(-400, 500, Easing.OutCubic); break; case TransformDirection.Prev: - d.Position = new Vector2(-400, 0); - d.MoveToX(0, 500, Easing.OutCubic); + newBackground.Position = new Vector2(-400, 0); + newBackground.MoveToX(0, 500, Easing.OutCubic); currentBackground.MoveToX(400, 500, Easing.OutCubic); break; } currentBackground.Expire(); - currentBackground = d; + currentBackground = newBackground; } }) { @@ -434,49 +433,5 @@ namespace osu.Game.Overlays sprite.Texture = beatmap?.Background ?? textures.Get(@"Backgrounds/bg4"); } } - - private class ProgressBar : SliderBar - { - public Action OnSeek; - - private readonly Box fill; - - public Color4 FillColour - { - set { fill.Colour = value; } - } - - public double EndTime - { - set { CurrentNumber.MaxValue = value; } - } - - public double CurrentTime - { - set { CurrentNumber.Value = value; } - } - - public ProgressBar() - { - CurrentNumber.MinValue = 0; - CurrentNumber.MaxValue = 1; - RelativeSizeAxes = Axes.X; - - Children = new Drawable[] - { - fill = new Box - { - RelativeSizeAxes = Axes.Y - } - }; - } - - protected override void UpdateValue(float value) - { - fill.Width = value * UsableWidth; - } - - protected override void OnUserChange() => OnSeek?.Invoke(Current); - } } } diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 7eabb592c6..260214a14f 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -22,7 +22,7 @@ namespace osu.Game.Overlays private ScrollContainer scrollContainer; private FlowContainer sections; - [BackgroundDependencyLoader(permitNulls: true)] + [BackgroundDependencyLoader] private void load() { Width = width; @@ -72,6 +72,13 @@ namespace osu.Game.Overlays private int runningDepth; + private void notificationClosed() + { + // hide ourselves if all notifications have been dismissed. + if (sections.Select(c => c.DisplayedCount).Sum() == 0) + State = Visibility.Hidden; + } + public void Post(Notification notification) { Schedule(() => @@ -81,6 +88,8 @@ namespace osu.Game.Overlays ++runningDepth; notification.Depth = notification.DisplayOnTop ? runningDepth : -runningDepth; + notification.Closed += notificationClosed; + var hasCompletionTarget = notification as IHasCompletionTarget; if (hasCompletionTarget != null) hasCompletionTarget.CompletionTarget = Post; diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 14446a468c..422051364e 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -19,9 +19,9 @@ namespace osu.Game.Overlays.Notifications public abstract class Notification : Container { /// - /// Use requested close. + /// User requested close. /// - public Action Closed; + public event Action Closed; /// /// Run on user activating the notification. Return true to close. @@ -142,12 +142,12 @@ namespace osu.Game.Overlays.Notifications NotificationContent.MoveToX(0, 500, Easing.OutQuint); } - private bool wasClosed; + public bool WasClosed; public virtual void Close() { - if (wasClosed) return; - wasClosed = true; + if (WasClosed) return; + WasClosed = true; Closed?.Invoke(); this.FadeOut(100); diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index efd3b39ee2..09768ba0ea 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -24,10 +24,9 @@ namespace osu.Game.Overlays.Notifications private FlowContainer notifications; - public void Add(Notification notification) - { - notifications.Add(notification); - } + public int DisplayedCount => notifications.Count(n => !n.WasClosed); + + public void Add(Notification notification) => notifications.Add(notification); public IEnumerable AcceptTypes; diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index f42b4b6cb3..a31291e1b8 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -152,11 +152,14 @@ namespace osu.Game.Overlays.Notifications break; case ProgressNotificationState.Active: case ProgressNotificationState.Queued: - State = ProgressNotificationState.Cancelled; + if (CancelRequested?.Invoke() != false) + State = ProgressNotificationState.Cancelled; break; } } + public Func CancelRequested { get; set; } + /// /// The function to post completion notifications back to. /// diff --git a/osu.Game/Overlays/Notifications/SimpleNotification.cs b/osu.Game/Overlays/Notifications/SimpleNotification.cs index e10cc26546..daf1ac838d 100644 --- a/osu.Game/Overlays/Notifications/SimpleNotification.cs +++ b/osu.Game/Overlays/Notifications/SimpleNotification.cs @@ -4,17 +4,16 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using OpenTK; namespace osu.Game.Overlays.Notifications { public class SimpleNotification : Notification { - private string text; + private string text = string.Empty; public string Text { get { return text; } @@ -36,7 +35,7 @@ namespace osu.Game.Overlays.Notifications } } - private readonly SpriteText textDrawable; + private readonly TextFlowContainer textDrawable; private readonly SpriteIcon iconDrawable; protected Box IconBackgound; @@ -59,9 +58,8 @@ namespace osu.Game.Overlays.Notifications } }); - Content.Add(textDrawable = new OsuSpriteText + Content.Add(textDrawable = new TextFlowContainer(t => t.TextSize = 16) { - TextSize = 16, Colour = OsuColour.Gray(128), AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, diff --git a/osu.Game/Overlays/SearchableList/SlimEnumDropdown.cs b/osu.Game/Overlays/SearchableList/SlimEnumDropdown.cs index 38e3e44911..2870607519 100644 --- a/osu.Game/Overlays/SearchableList/SlimEnumDropdown.cs +++ b/osu.Game/Overlays/SearchableList/SlimEnumDropdown.cs @@ -12,8 +12,9 @@ namespace osu.Game.Overlays.SearchableList { public class SlimEnumDropdown : OsuEnumDropdown { - protected override DropdownHeader CreateHeader() => new SlimDropdownHeader { AccentColour = AccentColour }; - protected override Menu CreateMenu() => new SlimMenu(); + protected override DropdownHeader CreateHeader() => new SlimDropdownHeader(); + + protected override DropdownMenu CreateMenu() => new SlimMenu(); private class SlimDropdownHeader : OsuDropdownHeader { @@ -31,11 +32,11 @@ namespace osu.Game.Overlays.SearchableList } } - private class SlimMenu : OsuMenu + private class SlimMenu : OsuDropdownMenu { public SlimMenu() { - Background.Colour = Color4.Black.Opacity(0.7f); + BackgroundColour = Color4.Black.Opacity(0.7f); } } } diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index 7ae45159d9..e62050fae1 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -257,9 +257,9 @@ namespace osu.Game.Overlays.Settings.Sections.General private class UserDropdown : OsuEnumDropdown { - protected override DropdownHeader CreateHeader() => new UserDropdownHeader { AccentColour = AccentColour }; - protected override Menu CreateMenu() => new UserDropdownMenu(); - protected override DropdownMenuItem CreateMenuItem(string text, UserAction value) => new UserDropdownMenuItem(text, value) { AccentColour = AccentColour }; + protected override DropdownHeader CreateHeader() => new UserDropdownHeader(); + + protected override DropdownMenu CreateMenu() => new UserDropdownMenu(); public Color4 StatusColour { @@ -277,6 +277,49 @@ namespace osu.Game.Overlays.Settings.Sections.General AccentColour = colours.Gray5; } + private class UserDropdownMenu : OsuDropdownMenu + { + public UserDropdownMenu() + { + Masking = true; + CornerRadius = 5; + + Margin = new MarginPadding { Bottom = 5 }; + + EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Colour = Color4.Black.Opacity(0.25f), + Radius = 4, + }; + + ItemsContainer.Padding = new MarginPadding(); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = colours.Gray3; + } + + protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => new DrawableUserDropdownMenuItem(item); + + private class DrawableUserDropdownMenuItem : DrawableOsuDropdownMenuItem + { + public DrawableUserDropdownMenuItem(MenuItem item) + : base(item) + { + Foreground.Padding = new MarginPadding { Top = 5, Bottom = 5, Left = 10, Right = 5 }; + CornerRadius = 5; + } + + protected override Drawable CreateContent() => new Content + { + Label = { Margin = new MarginPadding { Left = UserDropdownHeader.LABEL_LEFT_MARGIN - 11 } } + }; + } + } + private class UserDropdownHeader : OsuDropdownHeader { public const float LABEL_LEFT_MARGIN = 20; @@ -324,38 +367,9 @@ namespace osu.Game.Overlays.Settings.Sections.General } } - private class UserDropdownMenu : OsuMenu - { - public UserDropdownMenu() - { - Margin = new MarginPadding { Bottom = 5 }; - CornerRadius = 5; - ItemsContainer.Padding = new MarginPadding(0); - Masking = true; - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Colour = Color4.Black.Opacity(0.25f), - Radius = 4, - }; - } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Background.Colour = colours.Gray3; - } - } - private class UserDropdownMenuItem : OsuDropdownMenuItem - { - public UserDropdownMenuItem(string text, UserAction current) : base(text, current) - { - Foreground.Padding = new MarginPadding { Top = 5, Bottom = 5, Left = 10, Right = 5 }; - Label.Margin = new MarginPadding { Left = UserDropdownHeader.LABEL_LEFT_MARGIN - 11 }; - CornerRadius = 5; - } - } + } private enum UserAction diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 9d13a2ae2f..233ca7be60 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -13,6 +13,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance { private OsuButton importButton; private OsuButton deleteButton; + private OsuButton restoreButton; protected override string Header => "General"; @@ -41,6 +42,20 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Task.Run(() => beatmaps.DeleteAll()).ContinueWith(t => Schedule(() => deleteButton.Enabled.Value = true)); } }, + restoreButton = new OsuButton + { + RelativeSizeAxes = Axes.X, + Text = "Restore all hidden difficulties", + Action = () => + { + restoreButton.Enabled.Value = false; + Task.Run(() => + { + foreach (var b in beatmaps.QueryBeatmaps(b => b.Hidden)) + beatmaps.Restore(b); + }).ContinueWith(t => Schedule(() => restoreButton.Enabled.Value = true)); + } + }, }; } } diff --git a/osu.Game/Overlays/Settings/SettingsSubsection.cs b/osu.Game/Overlays/Settings/SettingsSubsection.cs index ac6d2fa239..0fbb5b92f7 100644 --- a/osu.Game/Overlays/Settings/SettingsSubsection.cs +++ b/osu.Game/Overlays/Settings/SettingsSubsection.cs @@ -7,14 +7,15 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Sprites; using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; namespace osu.Game.Overlays.Settings { public abstract class SettingsSubsection : FillFlowContainer, IHasFilterableChildren { - protected override Container Content => content; + protected override Container Content => FlowContent; - private readonly Container content; + protected readonly FillFlowContainer FlowContent; protected abstract string Header { get; } @@ -33,6 +34,19 @@ namespace osu.Game.Overlays.Settings RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; Direction = FillDirection.Vertical; + + FlowContent = new FillFlowContainer + { + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }; + } + + [BackgroundDependencyLoader] + private void load() + { AddRangeInternal(new Drawable[] { new OsuSpriteText @@ -41,13 +55,7 @@ namespace osu.Game.Overlays.Settings Margin = new MarginPadding { Bottom = 10 }, Font = @"Exo2.0-Black", }, - content = new FillFlowContainer - { - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 5), - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - }, + FlowContent }); } } diff --git a/osu.Game/Overlays/Settings/Sidebar.cs b/osu.Game/Overlays/Settings/Sidebar.cs index b22a5ab126..55167188a3 100644 --- a/osu.Game/Overlays/Settings/Sidebar.cs +++ b/osu.Game/Overlays/Settings/Sidebar.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using System.Linq; using osu.Framework; using OpenTK; @@ -19,6 +20,9 @@ namespace osu.Game.Overlays.Settings private readonly FillFlowContainer content; internal const float DEFAULT_WIDTH = ToolbarButton.WIDTH; internal const int EXPANDED_WIDTH = 200; + + public event Action StateChanged; + protected override Container Content => content; public Sidebar() @@ -102,6 +106,8 @@ namespace osu.Game.Overlays.Settings this.ResizeTo(new Vector2(EXPANDED_WIDTH, Height), 500, Easing.OutQuint); break; } + + StateChanged?.Invoke(State); } } diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index f9ec2f68b5..ddef5d1001 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -79,6 +79,7 @@ namespace osu.Game.Overlays }, SectionsContainer = new SettingsSectionsContainer { + Masking = true, RelativeSizeAxes = Axes.Both, ExpandableHeader = CreateHeader(), FixedHeader = searchTextBox = new SearchTextBox diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 32344ca7eb..e36db1f9da 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -58,6 +58,7 @@ namespace osu.Game.Overlays.Toolbar AutoSizeAxes = Axes.X, Children = new Drawable[] { + new ToolbarDirectButton(), new ToolbarChatButton(), new ToolbarSocialButton(), new ToolbarMusicButton(), diff --git a/osu.Game/Overlays/Toolbar/ToolbarDirectButton.cs b/osu.Game/Overlays/Toolbar/ToolbarDirectButton.cs new file mode 100644 index 0000000000..dacb6d67b8 --- /dev/null +++ b/osu.Game/Overlays/Toolbar/ToolbarDirectButton.cs @@ -0,0 +1,22 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Game.Graphics; + +namespace osu.Game.Overlays.Toolbar +{ + internal class ToolbarDirectButton : ToolbarOverlayToggleButton + { + public ToolbarDirectButton() + { + SetIcon(FontAwesome.fa_osu_chevron_down_o); + } + + [BackgroundDependencyLoader] + private void load(DirectOverlay direct) + { + StateContainer = direct; + } + } +} \ No newline at end of file diff --git a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs index 6d61a096b2..c530e8d7ff 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs @@ -45,7 +45,7 @@ namespace osu.Game.Overlays.Toolbar stateContainer.StateChanged -= stateChanged; } - private void stateChanged(VisibilityContainer c, Visibility state) + private void stateChanged(Visibility state) { switch (state) { diff --git a/osu.Game/Overlays/WaveOverlayContainer.cs b/osu.Game/Overlays/WaveOverlayContainer.cs index fd89dcfbc4..4f9783a762 100644 --- a/osu.Game/Overlays/WaveOverlayContainer.cs +++ b/osu.Game/Overlays/WaveOverlayContainer.cs @@ -167,6 +167,8 @@ namespace osu.Game.Overlays private class Wave : Container, IStateful { + public event Action StateChanged; + public float FinalPosition; public Wave() @@ -200,6 +202,7 @@ namespace osu.Game.Overlays } private Visibility state; + public Visibility State { get { return state; } @@ -216,6 +219,8 @@ namespace osu.Game.Overlays this.MoveToY(FinalPosition, APPEAR_DURATION, easing_show); break; } + + StateChanged?.Invoke(State); } } } diff --git a/osu.Game/Rulesets/Beatmaps/BeatmapConverter.cs b/osu.Game/Rulesets/Beatmaps/BeatmapConverter.cs index aafa576d4b..e79d9eee4b 100644 --- a/osu.Game/Rulesets/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Rulesets/Beatmaps/BeatmapConverter.cs @@ -26,21 +26,19 @@ namespace osu.Game.Rulesets.Beatmaps /// Converts a Beatmap using this Beatmap Converter. /// /// The un-converted Beatmap. - /// Whether to assume the beatmap is for the current ruleset. /// The converted Beatmap. - public Beatmap Convert(Beatmap original, bool isForCurrentRuleset) + public Beatmap Convert(Beatmap original) { // We always operate on a clone of the original beatmap, to not modify it game-wide - return ConvertBeatmap(new Beatmap(original), isForCurrentRuleset); + return ConvertBeatmap(new Beatmap(original)); } /// /// Performs the conversion of a Beatmap using this Beatmap Converter. /// /// The un-converted Beatmap. - /// Whether to assume the beatmap is for the current ruleset. /// The converted Beatmap. - protected virtual Beatmap ConvertBeatmap(Beatmap original, bool isForCurrentRuleset) + protected virtual Beatmap ConvertBeatmap(Beatmap original) { return new Beatmap { diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 8cfedc9599..3fba168216 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -106,6 +106,9 @@ namespace osu.Game.Rulesets.Objects.Drawables /// Whether a hit was processed. protected bool UpdateJudgement(bool userTriggered) { + if (Judgement == null) + return false; + var partial = Judgement as IPartialJudgement; // Never re-process non-partial hits diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index ffe40e4f2e..479cacc52b 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Objects.Legacy TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); DifficultyControlPoint difficultyPoint = controlPointInfo.DifficultyPointAt(StartTime); - double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier / difficultyPoint.SpeedMultiplier; + double scoringDistance = base_scoring_distance * difficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier; Velocity = scoringDistance / timingPoint.BeatLength; } diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index 60da35fd91..f0d68c0467 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Input; using osu.Framework.MathUtils; using osu.Game.Input.Handlers; @@ -18,7 +17,7 @@ namespace osu.Game.Rulesets.Replays /// The ReplayHandler will take a replay and handle the propagation of updates to the input stack. /// It handles logic of any frames which *must* be executed. /// - public class FramedReplayInputHandler : ReplayInputHandler + public abstract class FramedReplayInputHandler : ReplayInputHandler { private readonly Replay replay; @@ -31,7 +30,7 @@ namespace osu.Game.Rulesets.Replays private int nextFrameIndex => MathHelper.Clamp(currentFrameIndex + (currentDirection > 0 ? 1 : -1), 0, Frames.Count - 1); - public FramedReplayInputHandler(Replay replay) + protected FramedReplayInputHandler(Replay replay) { this.replay = replay; } @@ -51,7 +50,7 @@ namespace osu.Game.Rulesets.Replays { } - private Vector2? position + protected Vector2? Position { get { @@ -62,23 +61,7 @@ namespace osu.Game.Rulesets.Replays } } - public override List GetPendingStates() - { - var buttons = new HashSet(); - if (CurrentFrame?.MouseLeft ?? false) - buttons.Add(MouseButton.Left); - if (CurrentFrame?.MouseRight ?? false) - buttons.Add(MouseButton.Right); - - return new List - { - new InputState - { - Mouse = new ReplayMouseState(ToScreenSpace(position ?? Vector2.Zero), buttons), - Keyboard = new ReplayKeyboardState(new List()) - } - }; - } + public override List GetPendingStates() => new List(); public bool AtLastFrame => currentFrameIndex == Frames.Count - 1; public bool AtFirstFrame => currentFrameIndex == 0; @@ -133,10 +116,9 @@ namespace osu.Game.Rulesets.Replays protected class ReplayMouseState : MouseState { - public ReplayMouseState(Vector2 position, IEnumerable list) + public ReplayMouseState(Vector2 position) { Position = position; - list.ForEach(b => SetPressed(b, true)); } } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index de1291db7d..3d89afb7ff 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -2,8 +2,8 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.Linq; using System.Collections.Generic; +using System.Linq; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Game.Beatmaps; @@ -57,8 +57,6 @@ namespace osu.Game.Rulesets public abstract string Description { get; } - public abstract IEnumerable CreateGameplayKeys(); - public virtual SettingsSubsection CreateSettings() => null; /// @@ -77,5 +75,12 @@ namespace osu.Game.Rulesets /// A variant. /// A list of valid s. public virtual IEnumerable GetDefaultKeyBindings(int variant = 0) => new KeyBinding[] { }; + + /// + /// Gets the name for a key binding variant. This is used for display in the settings overlay. + /// + /// The variant. + /// A descriptive name of the variant. + public virtual string GetVariantName(int variant) => string.Empty; } } diff --git a/osu.Game/Rulesets/Scoring/ScoreStore.cs b/osu.Game/Rulesets/Scoring/ScoreStore.cs index e69fec4b54..c06d31e38f 100644 --- a/osu.Game/Rulesets/Scoring/ScoreStore.cs +++ b/osu.Game/Rulesets/Scoring/ScoreStore.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Scoring var version = sr.ReadInt32(); /* score.FileChecksum = */ var beatmapHash = sr.ReadString(); - score.Beatmap = beatmaps.QueryBeatmap(b => b.Hash == beatmapHash); + score.Beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == beatmapHash); /* score.PlayerName = */ sr.ReadString(); /* var localScoreChecksum = */ @@ -137,7 +137,7 @@ namespace osu.Game.Rulesets.Scoring frames.Add(new ReplayFrame( lastTime, float.Parse(split[1]), - 384 - float.Parse(split[2]), + float.Parse(split[2]), (ReplayButtonState)int.Parse(split[3]) )); } diff --git a/osu.Game/Rulesets/Timing/LinearScrollingContainer.cs b/osu.Game/Rulesets/Timing/LinearScrollingContainer.cs index db497b7664..f8e87bc022 100644 --- a/osu.Game/Rulesets/Timing/LinearScrollingContainer.cs +++ b/osu.Game/Rulesets/Timing/LinearScrollingContainer.cs @@ -10,12 +10,10 @@ namespace osu.Game.Rulesets.Timing /// internal class LinearScrollingContainer : ScrollingContainer { - private readonly Axes scrollingAxes; private readonly MultiplierControlPoint controlPoint; - public LinearScrollingContainer(Axes scrollingAxes, MultiplierControlPoint controlPoint) + public LinearScrollingContainer(MultiplierControlPoint controlPoint) { - this.scrollingAxes = scrollingAxes; this.controlPoint = controlPoint; } @@ -23,8 +21,8 @@ namespace osu.Game.Rulesets.Timing { base.Update(); - if ((scrollingAxes & Axes.X) > 0) X = (float)(controlPoint.StartTime - Time.Current); - if ((scrollingAxes & Axes.Y) > 0) Y = (float)(controlPoint.StartTime - Time.Current); + if ((ScrollingAxes & Axes.X) > 0) X = (float)(controlPoint.StartTime - Time.Current); + if ((ScrollingAxes & Axes.Y) > 0) Y = (float)(controlPoint.StartTime - Time.Current); } } } diff --git a/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs b/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs index 3b9b8d2d57..4f1a85cf2d 100644 --- a/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs +++ b/osu.Game/Rulesets/Timing/MultiplierControlPoint.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Timing /// /// The multiplier which this provides. /// - public double Multiplier => 1000 / TimingPoint.BeatLength / DifficultyPoint.SpeedMultiplier; + public double Multiplier => 1000 / TimingPoint.BeatLength * DifficultyPoint.SpeedMultiplier; /// /// The that provides the timing information for this . @@ -62,4 +62,4 @@ namespace osu.Game.Rulesets.Timing public int CompareTo(MultiplierControlPoint other) => StartTime.CompareTo(other?.StartTime); } -} \ No newline at end of file +} diff --git a/osu.Game/Rulesets/Timing/ScrollingContainer.cs b/osu.Game/Rulesets/Timing/ScrollingContainer.cs index 9bb32ead77..6e77c49e3d 100644 --- a/osu.Game/Rulesets/Timing/ScrollingContainer.cs +++ b/osu.Game/Rulesets/Timing/ScrollingContainer.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using System.Linq; using osu.Framework.Caching; using osu.Framework.Configuration; @@ -45,50 +46,34 @@ namespace osu.Game.Rulesets.Timing RelativePositionAxes = Axes.Both; } - public override void InvalidateFromChild(Invalidation invalidation) + protected override int Compare(Drawable x, Drawable y) { - // We only want to re-compute our size when a child's size or position has changed - if ((invalidation & Invalidation.RequiredParentSizeToFit) == 0) - { - base.InvalidateFromChild(invalidation); - return; - } + var hX = (DrawableHitObject)x; + var hY = (DrawableHitObject)y; + int result = hY.HitObject.StartTime.CompareTo(hX.HitObject.StartTime); + if (result != 0) + return result; + return base.Compare(y, x); + } + + public override void Add(DrawableHitObject drawable) + { durationBacking.Invalidate(); - - base.InvalidateFromChild(invalidation); + base.Add(drawable); } - private double computeDuration() + public override bool Remove(DrawableHitObject drawable) { - if (!Children.Any()) - return 0; - - double baseDuration = Children.Max(c => (c.HitObject as IHasEndTime)?.EndTime ?? c.HitObject.StartTime) - ControlPoint.StartTime; - - // If we have a singular hit object at the timing section's start time, let's set a sane default duration - if (baseDuration == 0) - baseDuration = 1; - - // This container needs to resize such that it completely encloses the hit objects to avoid masking optimisations. This is done by converting the largest - // absolutely-sized element along the scrolling axes and adding a corresponding duration value. This introduces a bit of error, but will never under-estimate.ion. - - // Find the largest element that is absolutely-sized along ScrollingAxes - float maxAbsoluteSize = Children.Where(c => (c.RelativeSizeAxes & ScrollingAxes) == 0) - .Select(c => (ScrollingAxes & Axes.X) > 0 ? c.Width : c.Height) - .DefaultIfEmpty().Max(); - - float ourAbsoluteSize = (ScrollingAxes & Axes.X) > 0 ? DrawWidth : DrawHeight; - - // Add the extra duration to account for the absolute size - baseDuration *= 1 + maxAbsoluteSize / ourAbsoluteSize; - - return baseDuration; + durationBacking.Invalidate(); + return base.Remove(drawable); } + // Todo: This may underestimate the size of the hit object in some cases, but won't be too much of a problem for now + private double computeDuration() => Math.Max(0, Children.Select(c => (c.HitObject as IHasEndTime)?.EndTime ?? c.HitObject.StartTime).DefaultIfEmpty().Max() - ControlPoint.StartTime) + 1000; + /// - /// The maximum duration of any one hit object inside this . This is calculated as the maximum - /// duration of all hit objects relative to this 's . + /// An approximate total duration of this scrolling container. /// public double Duration => durationBacking.IsValid ? durationBacking : (durationBacking.Value = computeDuration()); @@ -96,6 +81,8 @@ namespace osu.Game.Rulesets.Timing { base.Update(); + RelativeChildOffset = new Vector2((ScrollingAxes & Axes.X) > 0 ? (float)ControlPoint.StartTime : 0, (ScrollingAxes & Axes.Y) > 0 ? (float)ControlPoint.StartTime : 0); + // We want our size and position-space along the scrolling axes to span our duration to completely enclose all the hit objects Size = new Vector2((ScrollingAxes & Axes.X) > 0 ? (float)Duration : Size.X, (ScrollingAxes & Axes.Y) > 0 ? (float)Duration : Size.Y); // And we need to make sure the hit object's position-space doesn't change due to our resizing diff --git a/osu.Game/Rulesets/Timing/SpeedAdjustmentContainer.cs b/osu.Game/Rulesets/Timing/SpeedAdjustmentContainer.cs index 5d6c03b9de..d3bd7685da 100644 --- a/osu.Game/Rulesets/Timing/SpeedAdjustmentContainer.cs +++ b/osu.Game/Rulesets/Timing/SpeedAdjustmentContainer.cs @@ -1,7 +1,6 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -27,12 +26,16 @@ namespace osu.Game.Rulesets.Timing public readonly BindableBool Reversed = new BindableBool(); protected override Container Content => content; - private Container content; + private readonly Container content; /// /// The axes which the content of this container will scroll through. /// - public Axes ScrollingAxes { get; internal set; } + public Axes ScrollingAxes + { + get { return scrollingContainer.ScrollingAxes; } + set { scrollingContainer.ScrollingAxes = value; } + } public override bool RemoveWhenNotAlive => false; @@ -41,7 +44,7 @@ namespace osu.Game.Rulesets.Timing /// public readonly MultiplierControlPoint ControlPoint; - private ScrollingContainer scrollingContainer; + private readonly ScrollingContainer scrollingContainer; /// /// Creates a new . @@ -51,17 +54,10 @@ namespace osu.Game.Rulesets.Timing { ControlPoint = controlPoint; RelativeSizeAxes = Axes.Both; - } - [BackgroundDependencyLoader] - private void load() - { scrollingContainer = CreateScrollingContainer(); - - scrollingContainer.ScrollingAxes = ScrollingAxes; scrollingContainer.ControlPoint = ControlPoint; scrollingContainer.VisibleTimeRange.BindTo(VisibleTimeRange); - scrollingContainer.RelativeChildOffset = new Vector2((ScrollingAxes & Axes.X) > 0 ? (float)ControlPoint.StartTime : 0, (ScrollingAxes & Axes.Y) > 0 ? (float)ControlPoint.StartTime : 0); AddInternal(content = scrollingContainer); } @@ -103,11 +99,6 @@ namespace osu.Game.Rulesets.Timing base.Add(drawable); } - /// - /// Whether a falls within this s affecting timespan. - /// - public bool CanContain(DrawableHitObject hitObject) => CanContain(hitObject.HitObject.StartTime); - /// /// Whether a point in time falls within this s affecting timespan. /// @@ -117,6 +108,6 @@ namespace osu.Game.Rulesets.Timing /// Creates the which contains the scrolling s of this container. /// /// The . - protected virtual ScrollingContainer CreateScrollingContainer() => new LinearScrollingContainer(ScrollingAxes, ControlPoint); + protected virtual ScrollingContainer CreateScrollingContainer() => new LinearScrollingContainer(ControlPoint); } -} \ No newline at end of file +} diff --git a/osu.Game/Rulesets/UI/RulesetContainer.cs b/osu.Game/Rulesets/UI/RulesetContainer.cs index 8512d4b5e1..e2701faca0 100644 --- a/osu.Game/Rulesets/UI/RulesetContainer.cs +++ b/osu.Game/Rulesets/UI/RulesetContainer.cs @@ -9,7 +9,6 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Screens.Play; using System; using System.Collections.Generic; using System.Diagnostics; @@ -43,12 +42,12 @@ namespace osu.Game.Rulesets.UI /// /// The input manager for this RulesetContainer. /// - internal readonly PlayerInputManager InputManager = new PlayerInputManager(); + internal IHasReplayHandler ReplayInputManager => KeyBindingInputManager as IHasReplayHandler; /// /// The key conversion input manager for this RulesetContainer. /// - protected readonly PassThroughInputManager KeyConversionInputManager; + public PassThroughInputManager KeyBindingInputManager; /// /// Whether we are currently providing the local user a gameplay cursor. @@ -58,7 +57,7 @@ namespace osu.Game.Rulesets.UI /// /// Whether we have a replay loaded currently. /// - public bool HasReplayLoaded => InputManager.ReplayInputHandler != null; + public bool HasReplayLoaded => ReplayInputManager?.ReplayInputHandler != null; public abstract IEnumerable Objects { get; } @@ -76,8 +75,13 @@ namespace osu.Game.Rulesets.UI internal RulesetContainer(Ruleset ruleset) { Ruleset = ruleset; - KeyConversionInputManager = CreateKeyBindingInputManager(); - KeyConversionInputManager.RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + KeyBindingInputManager = CreateInputManager(); + KeyBindingInputManager.RelativeSizeAxes = Axes.Both; } /// @@ -92,12 +96,12 @@ namespace osu.Game.Rulesets.UI public abstract ScoreProcessor CreateScoreProcessor(); /// - /// Creates a key conversion input manager. + /// Creates a key conversion input manager. An exception will be thrown if a valid is not returned. /// /// The input manager. - public virtual PassThroughInputManager CreateKeyBindingInputManager() => new PassThroughInputManager(); + public abstract PassThroughInputManager CreateInputManager(); - protected virtual FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => new FramedReplayInputHandler(replay); + protected virtual FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => null; public Replay Replay { get; private set; } @@ -105,10 +109,13 @@ namespace osu.Game.Rulesets.UI /// Sets a replay to be used, overriding local input. /// /// The replay, null for local input. - public void SetReplay(Replay replay) + public virtual void SetReplay(Replay replay) { + if (ReplayInputManager == null) + throw new InvalidOperationException($"A {nameof(KeyBindingInputManager)} which supports replay loading is not available"); + Replay = replay; - InputManager.ReplayInputHandler = replay != null ? CreateReplayInputHandler(replay) : null; + ReplayInputManager.ReplayInputHandler = replay != null ? CreateReplayInputHandler(replay) : null; } } @@ -139,16 +146,30 @@ namespace osu.Game.Rulesets.UI protected IEnumerable Mods; /// + /// The this was created with. + /// + protected readonly WorkingBeatmap WorkingBeatmap; + + /// + /// Whether the specified beatmap is assumed to be specific to the current ruleset. + /// + protected readonly bool IsForCurrentRuleset; + + /// + /// Whether to assume the beatmap passed into this is for the current ruleset. /// Creates a hit renderer for a beatmap. /// /// The ruleset being repesented. - /// The beatmap to create the hit renderer for. + /// The beatmap to create the hit renderer for. /// Whether to assume the beatmap is for the current ruleset. - internal RulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset) : base(ruleset) + internal RulesetContainer(Ruleset ruleset, WorkingBeatmap workingBeatmap, bool isForCurrentRuleset) + : base(ruleset) { - Debug.Assert(beatmap != null, "RulesetContainer initialized with a null beatmap."); + Debug.Assert(workingBeatmap != null, "RulesetContainer initialized with a null beatmap."); - Mods = beatmap.Mods.Value; + WorkingBeatmap = workingBeatmap; + IsForCurrentRuleset = isForCurrentRuleset; + Mods = workingBeatmap.Mods.Value; RelativeSizeAxes = Axes.Both; @@ -156,11 +177,11 @@ namespace osu.Game.Rulesets.UI BeatmapProcessor processor = CreateBeatmapProcessor(); // Check if the beatmap can be converted - if (!converter.CanConvert(beatmap.Beatmap)) + if (!converter.CanConvert(workingBeatmap.Beatmap)) throw new BeatmapInvalidForRulesetException($"{nameof(Beatmap)} can not be converted for the current ruleset (converter: {converter})."); // Convert the beatmap - Beatmap = converter.Convert(beatmap.Beatmap, isForCurrentRuleset); + Beatmap = converter.Convert(workingBeatmap.Beatmap); // Apply difficulty adjustments from mods before using Difficulty. foreach (var mod in Mods.OfType()) @@ -173,8 +194,6 @@ namespace osu.Game.Rulesets.UI // Post-process the beatmap processor.PostProcess(Beatmap); - ApplyBeatmap(); - // Add mods, should always be the last thing applied to give full control to mods applyMods(Mods); } @@ -192,11 +211,6 @@ namespace osu.Game.Rulesets.UI mod.ApplyToRulesetContainer(this); } - /// - /// Called when the beatmap of this hit renderer has been set. Used to apply any default values from the beatmap. - /// - protected virtual void ApplyBeatmap() { } - /// /// Creates a processor to perform post-processing operations /// on HitObjects in converted Beatmaps. @@ -237,7 +251,7 @@ namespace osu.Game.Rulesets.UI public Playfield Playfield { get; private set; } protected override Container Content => content; - private readonly Container content; + private Container content; private readonly List> drawableObjects = new List>(); @@ -250,24 +264,28 @@ namespace osu.Game.Rulesets.UI protected RulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap, bool isForCurrentRuleset) : base(ruleset, beatmap, isForCurrentRuleset) { - InputManager.Add(content = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new[] { KeyConversionInputManager } - }); - - AddInternal(InputManager); } [BackgroundDependencyLoader] private void load() { - KeyConversionInputManager.Add(Playfield = CreatePlayfield()); + KeyBindingInputManager.Add(content = new Container + { + RelativeSizeAxes = Axes.Both, + }); + + AddInternal(KeyBindingInputManager); + KeyBindingInputManager.Add(Playfield = CreatePlayfield()); loadObjects(); + } - if (InputManager?.ReplayInputHandler != null) - InputManager.ReplayInputHandler.ToScreenSpace = Playfield.ScaledContent.ToScreenSpace; + public override void SetReplay(Replay replay) + { + base.SetReplay(replay); + + if (ReplayInputManager?.ReplayInputHandler != null) + ReplayInputManager.ReplayInputHandler.ToScreenSpace = input => Playfield.ScaledContent.ToScreenSpace(input); } /// diff --git a/osu.Game/Screens/Play/PlayerInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs similarity index 53% rename from osu.Game/Screens/Play/PlayerInputManager.cs rename to osu.Game/Rulesets/UI/RulesetInputManager.cs index f5e57f9e9d..0419070b42 100644 --- a/osu.Game/Screens/Play/PlayerInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -1,20 +1,57 @@ -// Copyright (c) 2007-2017 ppy Pty Ltd . +// Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using OpenTK.Input; +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Input; +using osu.Framework.Input.Bindings; using osu.Framework.Timing; using osu.Game.Configuration; +using osu.Game.Input.Bindings; using osu.Game.Input.Handlers; +using osu.Game.Screens.Play; +using OpenTK.Input; -namespace osu.Game.Screens.Play +namespace osu.Game.Rulesets.UI { - public class PlayerInputManager : PassThroughInputManager + public abstract class RulesetInputManager : DatabasedKeyBindingInputManager, ICanAttachKeyCounter, IHasReplayHandler + where T : struct { - private ManualClock clock; - private IFrameBasedClock parentClock; + protected RulesetInputManager(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique) : base(ruleset, variant, unique) + { + } + + #region Action mapping (for replays) + + private List lastPressedActions = new List(); + + protected override void HandleNewState(InputState state) + { + base.HandleNewState(state); + + var replayState = state as ReplayInputHandler.ReplayState; + + if (replayState == null) return; + + // Here we handle states specifically coming from a replay source. + // These have extra action information rather than keyboard keys or mouse buttons. + + List newActions = replayState.PressedActions; + + foreach (var released in lastPressedActions.Except(newActions)) + PropagateReleased(KeyBindingInputQueue, released); + + foreach (var pressed in newActions.Except(lastPressedActions)) + PropagatePressed(KeyBindingInputQueue, pressed); + + lastPressedActions = newActions; + } + + #endregion + + #region IHasReplayHandler private ReplayInputHandler replayInputHandler; public ReplayInputHandler ReplayInputHandler @@ -35,13 +72,12 @@ namespace osu.Game.Screens.Play } } - private Bindable mouseDisabled; + #endregion - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - mouseDisabled = config.GetBindable(OsuSetting.MouseDisableButtons); - } + #region Clock control + + private ManualClock clock; + private IFrameBasedClock parentClock; protected override void LoadComplete() { @@ -58,13 +94,13 @@ namespace osu.Game.Screens.Play } /// - /// Whether we running up-to-date with our parent clock. + /// Whether we are running up-to-date with our parent clock. /// If not, we will need to keep processing children until we catch up. /// private bool requireMoreUpdateLoops; /// - /// Whether we in a valid state (ie. should we keep processing children frames). + /// Whether we are in a valid state (ie. should we keep processing children frames). /// This should be set to false when the replay is, for instance, waiting for future frames to arrive. /// private bool validState; @@ -118,6 +154,18 @@ namespace osu.Game.Screens.Play base.Update(); } + #endregion + + #region Setting application (disables etc.) + + private Bindable mouseDisabled; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + mouseDisabled = config.GetBindable(OsuSetting.MouseDisableButtons); + } + protected override void TransformState(InputState state) { base.TransformState(state); @@ -136,5 +184,49 @@ namespace osu.Game.Screens.Play } } } + + #endregion + + #region Key Counter Attachment + + public void Attach(KeyCounterCollection keyCounter) + { + var receptor = new ActionReceptor(keyCounter); + Add(receptor); + keyCounter.SetReceptor(receptor); + + keyCounter.AddRange(DefaultKeyBindings.Select(b => b.GetAction()).Distinct().Select(b => new KeyCounterAction(b))); + } + + public class ActionReceptor : KeyCounterCollection.Receptor, IKeyBindingHandler + { + public ActionReceptor(KeyCounterCollection target) + : base(target) + { + } + + public bool OnPressed(T action) => Target.Children.OfType>().Any(c => c.OnPressed(action)); + + public bool OnReleased(T action) => Target.Children.OfType>().Any(c => c.OnReleased(action)); + } + + #endregion + } + + /// + /// Expose the in a capable . + /// + public interface IHasReplayHandler + { + ReplayInputHandler ReplayInputHandler { get; set; } + } + + /// + /// Supports attaching a . + /// Keys will be populated automatically and a receptor will be injected inside. + /// + public interface ICanAttachKeyCounter + { + void Attach(KeyCounterCollection keyCounter); } } diff --git a/osu.Game/Rulesets/UI/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/ScrollingPlayfield.cs index f8e090d613..1d13e2abb1 100644 --- a/osu.Game/Rulesets/UI/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/ScrollingPlayfield.cs @@ -55,14 +55,14 @@ namespace osu.Game.Rulesets.UI }; /// - /// Whether to reverse the scrolling direction is reversed. + /// Whether to reverse the scrolling direction is reversed. Note that this does _not_ invert the hit objects. /// - public readonly BindableBool Reversed = new BindableBool(); + protected readonly BindableBool Reversed = new BindableBool(); /// /// The container that contains the s and s. /// - internal new readonly ScrollingHitObjectContainer HitObjects; + public new readonly ScrollingHitObjectContainer HitObjects; /// /// Creates a new . @@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.UI /// /// A container that provides the foundation for sorting s into s. /// - internal class ScrollingHitObjectContainer : HitObjectContainer + public class ScrollingHitObjectContainer : HitObjectContainer { /// /// Gets or sets the range of time that is visible by the length of the scrolling axes. @@ -151,11 +151,10 @@ namespace osu.Game.Rulesets.UI /// public readonly BindableBool Reversed = new BindableBool(); - /// - /// Hit objects that are to be re-processed on the next update. - /// - private readonly List queuedHitObjects = new List(); - private readonly Container speedAdjustments; + private readonly SortedContainer speedAdjustments; + public IReadOnlyList SpeedAdjustments => speedAdjustments; + + private readonly SpeedAdjustmentContainer defaultSpeedAdjustment; private readonly Axes scrollingAxes; @@ -167,11 +166,15 @@ namespace osu.Game.Rulesets.UI { this.scrollingAxes = scrollingAxes; - AddInternal(speedAdjustments = new Container { RelativeSizeAxes = Axes.Both }); + AddInternal(speedAdjustments = new SortedContainer { RelativeSizeAxes = Axes.Both }); + + // Default speed adjustment + AddSpeedAdjustment(defaultSpeedAdjustment = new SpeedAdjustmentContainer(new MultiplierControlPoint(0))); } /// - /// Adds a to this container. + /// Adds a to this container, re-sorting all hit objects + /// in the last that occurred (time-wise) before it. /// /// The . public void AddSpeedAdjustment(SpeedAdjustmentContainer speedAdjustment) @@ -179,9 +182,51 @@ namespace osu.Game.Rulesets.UI speedAdjustment.ScrollingAxes = scrollingAxes; speedAdjustment.VisibleTimeRange.BindTo(VisibleTimeRange); speedAdjustment.Reversed.BindTo(Reversed); + + if (speedAdjustments.Count > 0) + { + // We need to re-sort all hit objects in the speed adjustment container prior to figure out if they + // should now lie within this one + var existingAdjustment = adjustmentContainerAt(speedAdjustment.ControlPoint.StartTime); + for (int i = 0; i < existingAdjustment.Count; i++) + { + DrawableHitObject hitObject = existingAdjustment[i]; + + if (!speedAdjustment.CanContain(hitObject.HitObject.StartTime)) + continue; + + existingAdjustment.Remove(hitObject); + speedAdjustment.Add(hitObject); + + i--; + } + } + speedAdjustments.Add(speedAdjustment); } + /// + /// Removes a from this container, re-sorting all hit objects + /// which it contained into new s. + /// + /// The to remove. + public void RemoveSpeedAdjustment(SpeedAdjustmentContainer speedAdjustment) + { + if (speedAdjustment == defaultSpeedAdjustment) + throw new InvalidOperationException($"The default {nameof(SpeedAdjustmentContainer)} must not be removed."); + + if (!speedAdjustments.Remove(speedAdjustment)) + return; + + while (speedAdjustment.Count > 0) + { + DrawableHitObject hitObject = speedAdjustment[0]; + + speedAdjustment.Remove(hitObject); + Add(hitObject); + } + } + public override IEnumerable Objects => speedAdjustments.SelectMany(s => s.Children); /// @@ -194,39 +239,10 @@ namespace osu.Game.Rulesets.UI if (!(hitObject is IScrollingHitObject)) throw new InvalidOperationException($"Hit objects added to a {nameof(ScrollingHitObjectContainer)} must implement {nameof(IScrollingHitObject)}."); - queuedHitObjects.Add(hitObject); + adjustmentContainerAt(hitObject.HitObject.StartTime).Add(hitObject); } - public override bool Remove(DrawableHitObject hitObject) => speedAdjustments.Any(s => s.Remove(hitObject)) || queuedHitObjects.Remove(hitObject); - - protected override void Update() - { - base.Update(); - - // Todo: At the moment this is going to re-process every single Update, however this will only be a null-op - // when there are no SpeedAdjustmentContainers available. This should probably error or something, but it's okay for now. - - for (int i = queuedHitObjects.Count - 1; i >= 0; i--) - { - var hitObject = queuedHitObjects[i]; - - var target = adjustmentContainerFor(hitObject); - if (target == null) - continue; - - target.Add(hitObject); - queuedHitObjects.RemoveAt(i); - } - } - - /// - /// Finds the which provides the speed adjustment active at the start time - /// of a hit object. If there is no active at the start time of the hit object, - /// then the first (time-wise) speed adjustment is returned. - /// - /// The hit object to find the active for. - /// The active at 's start time. Null if there are no speed adjustments. - private SpeedAdjustmentContainer adjustmentContainerFor(DrawableHitObject hitObject) => speedAdjustments.FirstOrDefault(c => c.CanContain(hitObject)) ?? speedAdjustments.LastOrDefault(); + public override bool Remove(DrawableHitObject hitObject) => speedAdjustments.Any(s => s.Remove(hitObject)); /// /// Finds the which provides the speed adjustment active at a time. @@ -234,7 +250,21 @@ namespace osu.Game.Rulesets.UI /// /// The time to find the active at. /// The active at . Null if there are no speed adjustments. - private SpeedAdjustmentContainer adjustmentContainerAt(double time) => speedAdjustments.FirstOrDefault(c => c.CanContain(time)) ?? speedAdjustments.LastOrDefault(); + private SpeedAdjustmentContainer adjustmentContainerAt(double time) => speedAdjustments.FirstOrDefault(c => c.CanContain(time)) ?? defaultSpeedAdjustment; + + private class SortedContainer : Container + { + protected override int Compare(Drawable x, Drawable y) + { + var sX = (SpeedAdjustmentContainer)x; + var sY = (SpeedAdjustmentContainer)y; + + int result = sY.ControlPoint.StartTime.CompareTo(sX.ControlPoint.StartTime); + if (result != 0) + return result; + return base.Compare(y, x); + } + } } } -} \ No newline at end of file +} diff --git a/osu.Game/Rulesets/UI/ScrollingRulesetContainer.cs b/osu.Game/Rulesets/UI/ScrollingRulesetContainer.cs index 64e865473d..ff156f788c 100644 --- a/osu.Game/Rulesets/UI/ScrollingRulesetContainer.cs +++ b/osu.Game/Rulesets/UI/ScrollingRulesetContainer.cs @@ -39,17 +39,6 @@ namespace osu.Game.Rulesets.UI [BackgroundDependencyLoader] private void load() - { - DefaultControlPoints.ForEach(c => applySpeedAdjustment(c, Playfield)); - } - - private void applySpeedAdjustment(MultiplierControlPoint controlPoint, ScrollingPlayfield playfield) - { - playfield.HitObjects.AddSpeedAdjustment(CreateSpeedAdjustmentContainer(controlPoint)); - playfield.NestedPlayfields.ForEach(p => applySpeedAdjustment(controlPoint, p)); - } - - protected override void ApplyBeatmap() { // Calculate default multiplier control points var lastTimingPoint = new TimingControlPoint(); @@ -95,6 +84,14 @@ namespace osu.Game.Rulesets.UI // If we have no control points, add a default one if (DefaultControlPoints.Count == 0) DefaultControlPoints.Add(new MultiplierControlPoint()); + + DefaultControlPoints.ForEach(c => applySpeedAdjustment(c, Playfield)); + } + + private void applySpeedAdjustment(MultiplierControlPoint controlPoint, ScrollingPlayfield playfield) + { + playfield.HitObjects.AddSpeedAdjustment(CreateSpeedAdjustmentContainer(controlPoint)); + playfield.NestedPlayfields.ForEach(p => applySpeedAdjustment(controlPoint, p)); } /// diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 9f33d624e2..be9098e3be 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -8,6 +8,11 @@ using osu.Framework.Screens; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Select; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Edit.Menus; namespace osu.Game.Screens.Edit { @@ -17,6 +22,175 @@ namespace osu.Game.Screens.Edit protected override BackgroundScreen CreateBackground() => new BackgroundScreenCustom(@"Backgrounds/bg4"); + internal override bool ShowOverlays => false; + + public Editor() + { + Add(new Container + { + RelativeSizeAxes = Axes.X, + Height = 40, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = OsuColour.FromHex("111") + }, + new EditorMenuBar + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + X = 100, + Items = new[] + { + new EditorMenuBarItem("File") + { + Items = new[] + { + new EditorMenuItem("Clear all notes"), + new EditorMenuItem("Open difficulty..."), + new EditorMenuItem("Save"), + new EditorMenuItem("Create new difficulty..."), + new EditorMenuItemSpacer(), + new EditorMenuItem("Revert to saved"), + new EditorMenuItem("Revert to saved (full"), + new EditorMenuItemSpacer(), + new EditorMenuItem("Test beatmap"), + new EditorMenuItem("Open AiMod"), + new EditorMenuItemSpacer(), + new EditorMenuItem("Upload Beatmap..."), + new EditorMenuItem("Export package"), + new EditorMenuItem("Export map package"), + new EditorMenuItem("Import from..."), + new EditorMenuItemSpacer(), + new EditorMenuItem("Open song folder"), + new EditorMenuItem("Open .osu in Notepad"), + new EditorMenuItem("Open .osb in Notepad"), + new EditorMenuItemSpacer(), + new EditorMenuItem("Exit", MenuItemType.Standard, Exit) + } + }, + new EditorMenuBarItem("Edit") + { + Items = new[] + { + new EditorMenuItem("Undo"), + new EditorMenuItem("Redo"), + new EditorMenuItemSpacer(), + new EditorMenuItem("Cut"), + new EditorMenuItem("Copy"), + new EditorMenuItem("Paste"), + new EditorMenuItem("Delete"), + new EditorMenuItemSpacer(), + new EditorMenuItem("Select all"), + new EditorMenuItem("Clone"), + new EditorMenuItemSpacer(), + new EditorMenuItem("Reverse selection"), + new EditorMenuItem("Flip horizontally"), + new EditorMenuItem("Flip vertically"), + new EditorMenuItem("Rotate 90deg clockwise"), + new EditorMenuItem("Rotate 90deg anticlockwise"), + new EditorMenuItem("Rotate by..."), + new EditorMenuItem("Scale by..."), + new EditorMenuItemSpacer(), + new EditorMenuItem("Reset selected objects' samples"), + new EditorMenuItem("Reset all samples", MenuItemType.Destructive), + new EditorMenuItem("Reset combo colours", MenuItemType.Destructive), + new EditorMenuItem("Reset breaks", MenuItemType.Destructive), + new EditorMenuItemSpacer(), + new EditorMenuItem("Nudge backward"), + new EditorMenuItem("Nudge forward") + } + }, + new EditorMenuBarItem("View") + { + Items = new[] + { + new EditorMenuItem("Compose"), + new EditorMenuItem("Design"), + new EditorMenuItem("Timing"), + new EditorMenuItemSpacer(), + new EditorMenuItem("Song setup..."), + new EditorMenuItem("Timing setup..."), + new EditorMenuItemSpacer(), + new EditorMenuItem("Volume"), + new EditorMenuItem("Grid level"), + new EditorMenuItem("Show video"), + new EditorMenuItem("Show sample name"), + new EditorMenuItem("Snaking sliders"), + new EditorMenuItem("Hit animations"), + new EditorMenuItem("Follow points"), + new EditorMenuItem("Stacking") + } + }, + new EditorMenuBarItem("Compose") + { + Items = new[] + { + new EditorMenuItem("Snap divisor"), + new EditorMenuItem("Audio rate"), + new EditorMenuItem("Grid snapping"), + new EditorMenuItemSpacer(), + new EditorMenuItem("Create polygon cricles..."), + new EditorMenuItem("Convert slider to stream"), + new EditorMenuItem("Enable live mapping mode"), + new EditorMenuItem("Sample import") + } + }, + new EditorMenuBarItem("Design") + { + Items = new[] + { + new EditorMenuItem("Move all elements in time...") + } + }, + new EditorMenuBarItem("Timing") + { + Items = new[] + { + new EditorMenuItem("Time signature"), + new EditorMenuItem("Metronome clicks"), + new EditorMenuItemSpacer(), + new EditorMenuItem("Add timing section"), + new EditorMenuItem("Add inheriting section"), + new EditorMenuItem("Reset current section"), + new EditorMenuItem("Delete timing section"), + new EditorMenuItem("Resnap current section"), + new EditorMenuItemSpacer(), + new EditorMenuItem("Timing setup..."), + new EditorMenuItemSpacer(), + new EditorMenuItem("Resnap all notes", MenuItemType.Destructive), + new EditorMenuItem("Move all notes in time...", MenuItemType.Destructive), + new EditorMenuItem("Recalculate slider lengths", MenuItemType.Destructive), + new EditorMenuItem("Delete all timing sections", MenuItemType.Destructive), + new EditorMenuItemSpacer(), + new EditorMenuItem("Set current position as preview point") + } + }, + new EditorMenuBarItem("Web") + { + Items = new[] + { + new EditorMenuItem("This Beatmap's information page"), + new EditorMenuItem("This Beatmap's thread"), + new EditorMenuItem("Quick reply") + } + }, + new EditorMenuBarItem("Help") + { + Items = new[] + { + new EditorMenuItem("Show in-game help"), + new EditorMenuItem("View FAQ") + } + } + } + } + } + }); + } + protected override void OnResuming(Screen last) { Beatmap.Value.Track?.Stop(); diff --git a/osu.Game/Screens/Edit/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Menus/EditorMenuBar.cs new file mode 100644 index 0000000000..bb349b1531 --- /dev/null +++ b/osu.Game/Screens/Edit/Menus/EditorMenuBar.cs @@ -0,0 +1,122 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using OpenTK; +using OpenTK.Graphics; + +namespace osu.Game.Screens.Edit.Menus +{ + public class EditorMenuBar : OsuMenu + { + public EditorMenuBar() + : base(Direction.Horizontal, true) + { + ItemsContainer.Padding = new MarginPadding(0); + BackgroundColour = Color4.Transparent; + } + + protected override Framework.Graphics.UserInterface.Menu CreateSubMenu() => new SubMenu(); + + protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => new DrawableEditorBarMenuItem(item); + + private class DrawableEditorBarMenuItem : DrawableOsuMenuItem + { + private Color4 openedForegroundColour; + private Color4 openedBackgroundColour; + + public DrawableEditorBarMenuItem(MenuItem item) + : base(item) + { + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + ForegroundColour = ForegroundColourHover = colours.BlueLight; + BackgroundColour = BackgroundColourHover = Color4.Transparent; + openedForegroundColour = Color4.White; + openedBackgroundColour = colours.Gray3; + } + + protected override void UpdateBackgroundColour() + { + if (State == MenuItemState.Selected) + Background.FadeColour(openedBackgroundColour); + else + base.UpdateBackgroundColour(); + } + + protected override void UpdateForegroundColour() + { + if (State == MenuItemState.Selected) + Foreground.FadeColour(openedForegroundColour); + else + base.UpdateForegroundColour(); + } + + protected override Drawable CreateBackground() => new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Height = 2, + Masking = true, + CornerRadius = 4, + Child = new Box { RelativeSizeAxes = Axes.Both } + } + }; + } + + private class SubMenu : OsuMenu + { + public SubMenu() + : base(Direction.Vertical) + { + OriginPosition = new Vector2(5, 1); + ItemsContainer.Padding = new MarginPadding { Top = 5, Bottom = 5 }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = colours.Gray3; + } + + protected override Framework.Graphics.UserInterface.Menu CreateSubMenu() => new SubMenu(); + + protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => new DrawableSubMenuItem(item); + + private class DrawableSubMenuItem : DrawableOsuMenuItem + { + public DrawableSubMenuItem(MenuItem item) + : base(item) + { + } + + protected override bool OnHover(InputState state) + { + if (Item is EditorMenuItemSpacer) + return true; + return base.OnHover(state); + } + + protected override bool OnClick(InputState state) + { + if (Item is EditorMenuItemSpacer) + return true; + return base.OnClick(state); + } + } + } + } +} diff --git a/osu.Game/Screens/Edit/Menus/EditorMenuBarItem.cs b/osu.Game/Screens/Edit/Menus/EditorMenuBarItem.cs new file mode 100644 index 0000000000..201bc6e5c3 --- /dev/null +++ b/osu.Game/Screens/Edit/Menus/EditorMenuBarItem.cs @@ -0,0 +1,15 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Graphics.UserInterface; + +namespace osu.Game.Screens.Edit.Menus +{ + public class EditorMenuBarItem : MenuItem + { + public EditorMenuBarItem(string text) + : base(text) + { + } + } +} diff --git a/osu.Game/Screens/Edit/Menus/EditorMenuItem.cs b/osu.Game/Screens/Edit/Menus/EditorMenuItem.cs new file mode 100644 index 0000000000..c7e36522cf --- /dev/null +++ b/osu.Game/Screens/Edit/Menus/EditorMenuItem.cs @@ -0,0 +1,23 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using System; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Screens.Edit.Menus +{ + public class EditorMenuItem : OsuMenuItem + { + private const int min_text_length = 40; + + public EditorMenuItem(string text, MenuItemType type = MenuItemType.Standard) + : base(text.PadRight(min_text_length), type) + { + } + + public EditorMenuItem(string text, MenuItemType type, Action action) + : base(text.PadRight(min_text_length), type, action) + { + } + } +} diff --git a/osu.Game/Screens/Edit/Menus/EditorMenuItemSpacer.cs b/osu.Game/Screens/Edit/Menus/EditorMenuItemSpacer.cs new file mode 100644 index 0000000000..5060165ef7 --- /dev/null +++ b/osu.Game/Screens/Edit/Menus/EditorMenuItemSpacer.cs @@ -0,0 +1,13 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +namespace osu.Game.Screens.Edit.Menus +{ + public class EditorMenuItemSpacer : EditorMenuItem + { + public EditorMenuItemSpacer() + : base(" ") + { + } + } +} diff --git a/osu.Game/Screens/Menu/Button.cs b/osu.Game/Screens/Menu/Button.cs index 0898c079ce..aca169c3dc 100644 --- a/osu.Game/Screens/Menu/Button.cs +++ b/osu.Game/Screens/Menu/Button.cs @@ -28,6 +28,8 @@ namespace osu.Game.Screens.Menu /// public class Button : BeatSyncedContainer, IStateful { + public event Action StateChanged; + private readonly Container iconText; private readonly Container box; private readonly Box boxHoverLayer; @@ -266,6 +268,8 @@ namespace osu.Game.Screens.Menu this.FadeOut(explode_duration / 4f * 3); break; } + + StateChanged?.Invoke(State); } } } diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 71f2a16c09..e4dbe00a80 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -22,6 +22,8 @@ namespace osu.Game.Screens.Menu { public class ButtonSystem : Container, IStateful { + public event Action StateChanged; + public Action OnEdit; public Action OnExit; public Action OnDirect; @@ -294,6 +296,8 @@ namespace osu.Game.Screens.Menu backButton.State = state == MenuState.Play ? ButtonState.Expanded : ButtonState.Contracted; settingsButton.State = state == MenuState.TopLevel ? ButtonState.Expanded : ButtonState.Contracted; } + + StateChanged?.Invoke(State); } } diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 6e79d2b427..ae10d8828b 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -69,19 +69,6 @@ namespace osu.Game.Screens sampleExit = audio.Sample.Get(@"UI/melodic-1"); } - protected override void Update() - { - if (!IsCurrentScreen) return; - - if (ParentScreen != null) - { - // we only want to apply these restrictions when we are inside a screen stack. - // the use case for not applying is in visual/unit tests. - Ruleset.Disabled = !AllowBeatmapRulesetChange; - Beatmap.Disabled = !AllowBeatmapRulesetChange; - } - } - protected override void OnResuming(Screen last) { base.OnResuming(last); diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index cf6bc03c60..ddcf031bd4 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -92,7 +92,7 @@ namespace osu.Game.Screens.Play public virtual void BindRulesetContainer(RulesetContainer rulesetContainer) { - rulesetContainer.InputManager.Add(KeyCounter.GetReceptor()); + (rulesetContainer.KeyBindingInputManager as ICanAttachKeyCounter)?.Attach(KeyCounter); replayLoaded = rulesetContainer.HasReplayLoaded; diff --git a/osu.Game/Screens/Play/KeyCounterAction.cs b/osu.Game/Screens/Play/KeyCounterAction.cs new file mode 100644 index 0000000000..a6baeeafb6 --- /dev/null +++ b/osu.Game/Screens/Play/KeyCounterAction.cs @@ -0,0 +1,30 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Input.Bindings; + +namespace osu.Game.Screens.Play +{ + public class KeyCounterAction : KeyCounter, IKeyBindingHandler + where T : struct + { + public T Action { get; } + + public KeyCounterAction(T action) : base($"B{(int)(object)action + 1}") + { + Action = action; + } + + public bool OnPressed(T action) + { + if (action.Equals(Action)) IsLit = true; + return false; + } + + public bool OnReleased(T action) + { + if (action.Equals(Action)) IsLit = false; + return false; + } + } +} \ No newline at end of file diff --git a/osu.Game/Screens/Play/KeyCounterCollection.cs b/osu.Game/Screens/Play/KeyCounterCollection.cs index 63c29a8f78..0f6c5984c4 100644 --- a/osu.Game/Screens/Play/KeyCounterCollection.cs +++ b/osu.Game/Screens/Play/KeyCounterCollection.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -117,27 +118,36 @@ namespace osu.Game.Screens.Play return receptor ?? (receptor = new Receptor(this)); } + public void SetReceptor(Receptor receptor) + { + if (this.receptor != null) + throw new InvalidOperationException("Cannot set a new receptor when one is already active"); + + this.receptor = receptor; + } + public class Receptor : Drawable { - private readonly KeyCounterCollection target; + protected readonly KeyCounterCollection Target; public Receptor(KeyCounterCollection target) { RelativeSizeAxes = Axes.Both; - this.target = target; + Depth = float.MinValue; + Target = target; } public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => true; public override bool HandleInput => true; - protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) => target.Children.Any(c => c.TriggerOnKeyDown(state, args)); + protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) => Target.Children.Any(c => c.TriggerOnKeyDown(state, args)); - protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) => target.Children.Any(c => c.TriggerOnKeyUp(state, args)); + protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) => Target.Children.Any(c => c.TriggerOnKeyUp(state, args)); - protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => target.Children.Any(c => c.TriggerOnMouseDown(state, args)); + protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => Target.Children.Any(c => c.TriggerOnMouseDown(state, args)); - protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) => target.Children.Any(c => c.TriggerOnMouseUp(state, args)); + protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) => Target.Children.Any(c => c.TriggerOnMouseUp(state, args)); } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 00f1bd1b6b..615e04d6c2 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -23,6 +23,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Ranking; using osu.Framework.Audio.Sample; using osu.Game.Beatmaps; +using osu.Game.Online.API; namespace osu.Game.Screens.Play { @@ -50,6 +51,8 @@ namespace osu.Game.Screens.Play private RulesetInfo ruleset; + private APIAccess api; + private ScoreProcessor scoreProcessor; protected RulesetContainer RulesetContainer; @@ -68,16 +71,17 @@ namespace osu.Game.Screens.Play private bool loadedSuccessfully => RulesetContainer?.Objects.Any() == true; - [BackgroundDependencyLoader(permitNulls: true)] - private void load(AudioManager audio, OsuConfigManager config, OsuGame osu) + [BackgroundDependencyLoader] + private void load(AudioManager audio, OsuConfigManager config, APIAccess api) { + this.api = api; + dimLevel = config.GetBindable(OsuSetting.DimLevel); + mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); sampleRestart = audio.Sample.Get(@"Gameplay/restart"); - Ruleset rulesetInstance; - WorkingBeatmap working = Beatmap.Value; Beatmap beatmap; @@ -88,8 +92,8 @@ namespace osu.Game.Screens.Play if (beatmap == null) throw new InvalidOperationException("Beatmap was not loaded"); - ruleset = osu?.Ruleset.Value ?? beatmap.BeatmapInfo.Ruleset; - rulesetInstance = ruleset.CreateInstance(); + ruleset = Ruleset.Value ?? beatmap.BeatmapInfo.Ruleset; + var rulesetInstance = ruleset.CreateInstance(); try { @@ -192,7 +196,6 @@ namespace osu.Game.Screens.Play scoreProcessor = RulesetContainer.CreateScoreProcessor(); - hudOverlay.KeyCounter.AddRange(rulesetInstance.CreateGameplayKeys()); hudOverlay.BindProcessor(scoreProcessor); hudOverlay.BindRulesetContainer(RulesetContainer); @@ -238,7 +241,7 @@ namespace osu.Game.Screens.Play Ruleset = ruleset }; scoreProcessor.PopulateScore(score); - score.User = RulesetContainer.Replay?.User ?? (Game as OsuGame)?.API?.LocalUser?.Value; + score.User = RulesetContainer.Replay?.User ?? api.LocalUser.Value; Push(new Results(score)); }); } diff --git a/osu.Game/Screens/Play/SkipButton.cs b/osu.Game/Screens/Play/SkipButton.cs index 3cf371f1e1..6519a8db36 100644 --- a/osu.Game/Screens/Play/SkipButton.cs +++ b/osu.Game/Screens/Play/SkipButton.cs @@ -133,6 +133,8 @@ namespace osu.Game.Screens.Play private class FadeContainer : Container, IStateful { + public event Action StateChanged; + private Visibility state; private ScheduledDelegate scheduledHide; @@ -144,8 +146,10 @@ namespace osu.Game.Screens.Play } set { - var lastState = state; + if (state == value) + return; + var lastState = state; state = value; scheduledHide?.Cancel(); @@ -164,6 +168,8 @@ namespace osu.Game.Screens.Play this.FadeOut(1000, Easing.OutExpo); break; } + + StateChanged?.Invoke(State); } } diff --git a/osu.Game/Screens/Play/SquareGraph.cs b/osu.Game/Screens/Play/SquareGraph.cs index f3bb523611..81dbf3eca4 100644 --- a/osu.Game/Screens/Play/SquareGraph.cs +++ b/osu.Game/Screens/Play/SquareGraph.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using System.Collections.Generic; using System.Linq; using osu.Framework; @@ -170,6 +171,8 @@ namespace osu.Game.Screens.Play private const float padding = 2; public const float WIDTH = cube_size + padding; + public event Action StateChanged; + private readonly List drawableRows = new List(); private float filled; @@ -186,6 +189,7 @@ namespace osu.Game.Screens.Play } private ColumnState state; + public ColumnState State { get { return state; } @@ -196,6 +200,8 @@ namespace osu.Game.Screens.Play if (IsLoaded) fillActive(); + + StateChanged?.Invoke(State); } } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 264636b258..94ae740c74 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -107,17 +107,44 @@ namespace osu.Game.Screens.Select }); } - public void RemoveBeatmap(BeatmapSetInfo beatmapSet) + public void RemoveBeatmap(BeatmapSetInfo beatmapSet) => removeGroup(groups.Find(b => b.BeatmapSet.ID == beatmapSet.ID)); + + internal void UpdateBeatmap(BeatmapInfo beatmap) { - Schedule(delegate + // todo: this method should not run more than once for the same BeatmapSetInfo. + var set = manager.Refresh(beatmap.BeatmapSet); + + // todo: this method should be smarter as to not recreate panels that haven't changed, etc. + var group = groups.Find(b => b.BeatmapSet.ID == set.ID); + + if (group == null) + return; + + var newGroup = createGroup(set); + + int i = groups.IndexOf(group); + groups.RemoveAt(i); + groups.Insert(i, newGroup); + + if (selectedGroup == group && newGroup.BeatmapPanels.Count == 0) + selectedGroup = null; + + Filter(null, false); + + //check if we can/need to maintain our current selection. + if (selectedGroup == group && newGroup.BeatmapPanels.Count > 0) { - removeGroup(groups.Find(b => b.BeatmapSet.ID == beatmapSet.ID)); - }); + var newSelection = + newGroup.BeatmapPanels.Find(p => p.Beatmap.ID == selectedPanel?.Beatmap.ID) ?? + newGroup.BeatmapPanels[Math.Min(newGroup.BeatmapPanels.Count - 1, group.BeatmapPanels.IndexOf(selectedPanel))]; + + selectGroup(newGroup, newSelection); + } } public void SelectBeatmap(BeatmapInfo beatmap, bool animated = true) { - if (beatmap == null) + if (beatmap == null || beatmap.Hidden) { SelectNext(); return; @@ -140,6 +167,12 @@ namespace osu.Game.Screens.Select public Action StartRequested; + public Action DeleteRequested; + + public Action RestoreRequested; + + public Action HideDifficultyRequested; + public void SelectNext(int direction = 1, bool skipDifficulties = true) { if (groups.All(g => g.State == BeatmapGroupState.Hidden)) @@ -191,7 +224,8 @@ namespace osu.Game.Screens.Select if (!visibleGroups.Any()) return; - randomSelectedBeatmaps.Push(new KeyValuePair(selectedGroup, selectedGroup.SelectedPanel)); + if (selectedGroup != null) + randomSelectedBeatmaps.Push(new KeyValuePair(selectedGroup, selectedGroup.SelectedPanel)); BeatmapGroup group; @@ -305,6 +339,9 @@ namespace osu.Game.Screens.Select { SelectionChanged = (g, p) => selectGroup(g, p), StartRequested = b => StartRequested?.Invoke(), + DeleteRequested = b => DeleteRequested?.Invoke(b), + RestoreHiddenRequested = s => RestoreRequested?.Invoke(s), + HideDifficultyRequested = b => HideDifficultyRequested?.Invoke(b), State = BeatmapGroupState.Collapsed }; } diff --git a/osu.Game/Screens/Select/BeatmapDeleteDialog.cs b/osu.Game/Screens/Select/BeatmapDeleteDialog.cs index 96caf2f236..aa37705cdf 100644 --- a/osu.Game/Screens/Select/BeatmapDeleteDialog.cs +++ b/osu.Game/Screens/Select/BeatmapDeleteDialog.cs @@ -1,7 +1,6 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -19,23 +18,18 @@ namespace osu.Game.Screens.Select manager = beatmapManager; } - public BeatmapDeleteDialog(WorkingBeatmap beatmap) + public BeatmapDeleteDialog(BeatmapSetInfo beatmap) { - if (beatmap == null) throw new ArgumentNullException(nameof(beatmap)); + BodyText = $@"{beatmap.Metadata?.Artist} - {beatmap.Metadata?.Title}"; Icon = FontAwesome.fa_trash_o; HeaderText = @"Confirm deletion of"; - BodyText = $@"{beatmap.Metadata?.Artist} - {beatmap.Metadata?.Title}"; Buttons = new PopupDialogButton[] { new PopupDialogOkButton { Text = @"Yes. Totally. Delete it.", - Action = () => - { - beatmap.Dispose(); - manager.Delete(beatmap.BeatmapSetInfo); - }, + Action = () => manager.Delete(beatmap), }, new PopupDialogCancelButton { diff --git a/osu.Game/Screens/Select/Leaderboards/LeaderboardScore.cs b/osu.Game/Screens/Select/Leaderboards/LeaderboardScore.cs index fb919f9548..4f81b2871d 100644 --- a/osu.Game/Screens/Select/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Screens/Select/Leaderboards/LeaderboardScore.cs @@ -1,6 +1,7 @@ // Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; using OpenTK; using OpenTK.Graphics; using osu.Framework; @@ -23,6 +24,8 @@ namespace osu.Game.Screens.Select.Leaderboards { public static readonly float HEIGHT = 60; + public event Action StateChanged; + public readonly int RankPosition; public readonly Score Score; @@ -43,11 +46,14 @@ namespace osu.Game.Screens.Select.Leaderboards private readonly FillFlowContainer modsContainer; private Visibility state; + public Visibility State { get { return state; } set { + if (state == value) + return; state = value; switch (state) @@ -90,6 +96,8 @@ namespace osu.Game.Screens.Select.Leaderboards break; } + + StateChanged?.Invoke(State); } } diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 662e1d55a2..7e03707d18 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -54,13 +54,11 @@ namespace osu.Game.Screens.Select ValidForResume = false; Push(new Editor()); }, Key.Number3); - - Beatmap.ValueChanged += beatmap_ValueChanged; } - private void beatmap_ValueChanged(WorkingBeatmap beatmap) + protected override void UpdateBeatmap(WorkingBeatmap beatmap) { - if (!IsCurrentScreen) return; + base.UpdateBeatmap(beatmap); beatmap.Mods.BindTo(modSelect.SelectedMods); diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index aa554152f9..f97c4fe420 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -106,6 +106,9 @@ namespace osu.Game.Screens.Select Origin = Anchor.CentreRight, SelectionChanged = carouselSelectionChanged, BeatmapsChanged = carouselBeatmapsLoaded, + DeleteRequested = b => promptDelete(b), + RestoreRequested = s => { foreach (var b in s.Beatmaps) manager.Restore(b); }, + HideDifficultyRequested = b => manager.Hide(b), StartRequested = () => carouselRaisedStart(), }); Add(FilterControl = new FilterControl @@ -163,7 +166,7 @@ namespace osu.Game.Screens.Select Footer.AddButton(@"random", colours.Green, triggerRandom, Key.F2); Footer.AddButton(@"options", colours.Blue, BeatmapOptions.ToggleVisibility, Key.F3); - BeatmapOptions.AddButton(@"Delete", @"Beatmap", FontAwesome.fa_trash, colours.Pink, promptDelete, Key.Number4, float.MaxValue); + BeatmapOptions.AddButton(@"Delete", @"Beatmap", FontAwesome.fa_trash, colours.Pink, () => promptDelete(Beatmap.Value.BeatmapSetInfo), Key.Number4, float.MaxValue); } if (manager == null) @@ -174,6 +177,8 @@ namespace osu.Game.Screens.Select manager.BeatmapSetAdded += onBeatmapSetAdded; manager.BeatmapSetRemoved += onBeatmapSetRemoved; + manager.BeatmapHidden += onBeatmapHidden; + manager.BeatmapRestored += onBeatmapRestored; dialogOverlay = dialog; @@ -190,6 +195,9 @@ namespace osu.Game.Screens.Select carousel.AllowSelection = !Beatmap.Disabled; } + private void onBeatmapRestored(BeatmapInfo b) => carousel.UpdateBeatmap(b); + private void onBeatmapHidden(BeatmapInfo b) => carousel.UpdateBeatmap(b); + private void carouselBeatmapsLoaded() { if (Beatmap.Value.BeatmapSetInfo?.DeletePending == false) @@ -236,7 +244,7 @@ namespace osu.Game.Screens.Select ensurePlayingSelected(preview); } - changeBackground(Beatmap.Value); + UpdateBeatmap(Beatmap.Value); }; selectionChangedDebounce?.Cancel(); @@ -304,7 +312,7 @@ namespace osu.Game.Screens.Select { if (Beatmap != null && !Beatmap.Value.BeatmapSetInfo.DeletePending) { - changeBackground(Beatmap); + UpdateBeatmap(Beatmap); ensurePlayingSelected(); } @@ -345,12 +353,19 @@ namespace osu.Game.Screens.Select { manager.BeatmapSetAdded -= onBeatmapSetAdded; manager.BeatmapSetRemoved -= onBeatmapSetRemoved; + manager.BeatmapHidden -= onBeatmapHidden; + manager.BeatmapRestored -= onBeatmapRestored; } initialAddSetsTask?.Cancel(); } - private void changeBackground(WorkingBeatmap beatmap) + /// + /// Allow components in SongSelect to update their loaded beatmap details. + /// This is a debounced call (unlike directly binding to WorkingBeatmap.ValueChanged). + /// + /// The working beatmap. + protected virtual void UpdateBeatmap(WorkingBeatmap beatmap) { var backgroundModeBeatmap = Background as BackgroundScreenBeatmap; if (backgroundModeBeatmap != null) @@ -378,10 +393,7 @@ namespace osu.Game.Screens.Select } } - private void addBeatmapSet(BeatmapSetInfo beatmapSet) - { - carousel.AddBeatmap(beatmapSet); - } + private void addBeatmapSet(BeatmapSetInfo beatmapSet) => carousel.AddBeatmap(beatmapSet); private void removeBeatmapSet(BeatmapSetInfo beatmapSet) { @@ -390,10 +402,12 @@ namespace osu.Game.Screens.Select Beatmap.SetDefault(); } - private void promptDelete() + private void promptDelete(BeatmapSetInfo beatmap) { - if (Beatmap != null && !Beatmap.IsDefault) - dialogOverlay?.Push(new BeatmapDeleteDialog(Beatmap)); + if (beatmap == null) + return; + + dialogOverlay?.Push(new BeatmapDeleteDialog(beatmap)); } protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) @@ -409,7 +423,8 @@ namespace osu.Game.Screens.Select case Key.Delete: if (state.Keyboard.ShiftPressed) { - promptDelete(); + if (!Beatmap.IsDefault) + promptDelete(Beatmap.Value.BeatmapSetInfo); return true; } break; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 16812c3467..66a892406f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -91,7 +91,7 @@ - + @@ -101,12 +101,14 @@ + - + + @@ -114,6 +116,7 @@ + @@ -127,8 +130,14 @@ + - + + + + + + @@ -235,6 +244,7 @@ + @@ -318,7 +328,6 @@ -