diff --git a/appveyor.yml b/appveyor.yml index b26a895788..21c15724e6 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,3 +1,4 @@ +# 2017-09-14 clone_depth: 1 version: '{branch}-{build}' configuration: Debug diff --git a/osu-framework b/osu-framework index f039a8cb70..7347c386dc 160000 --- a/osu-framework +++ b/osu-framework @@ -1 +1 @@ -Subproject commit f039a8cb707296238d22b6c382af862725c05928 +Subproject commit 7347c386dcd10eb799b1ce1512536879328109f9 diff --git a/osu.Desktop.Tests/Visual/TestCaseStoryboard.cs b/osu.Desktop.Tests/Visual/TestCaseStoryboard.cs new file mode 100644 index 0000000000..878198e8d2 --- /dev/null +++ b/osu.Desktop.Tests/Visual/TestCaseStoryboard.cs @@ -0,0 +1,90 @@ +// 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.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Timing; +using osu.Game; +using osu.Game.Beatmaps; +using osu.Game.Overlays; +using osu.Game.Storyboards.Drawables; + +namespace osu.Desktop.Tests.Visual +{ + internal class TestCaseStoryboard : OsuTestCase + { + public override string Description => @"Tests storyboards."; + + private readonly Bindable beatmapBacking = new Bindable(); + + private readonly Container storyboardContainer; + private DrawableStoryboard storyboard; + + public TestCaseStoryboard() + { + Clock = new FramedClock(); + + Add(new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + }, + storyboardContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }, + }, + }); + Add(new MusicController + { + Origin = Anchor.TopRight, + Anchor = Anchor.TopRight, + State = Visibility.Visible, + }); + + AddStep("Restart", restart); + AddToggleStep("Passing", passing => { if (storyboard != null) storyboard.Passing = passing; }); + } + + [BackgroundDependencyLoader] + private void load(OsuGameBase game) + { + beatmapBacking.BindTo(game.Beatmap); + beatmapBacking.ValueChanged += beatmapChanged; + } + + private void beatmapChanged(WorkingBeatmap working) + => loadStoryboard(working); + + private void restart() + { + var track = beatmapBacking.Value.Track; + + track.Reset(); + loadStoryboard(beatmapBacking.Value); + track.Start(); + } + + private void loadStoryboard(WorkingBeatmap working) + { + if (storyboard != null) + storyboardContainer.Remove(storyboard); + + var decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true }; + decoupledClock.ChangeSource(working.Track); + storyboardContainer.Clock = decoupledClock; + + storyboardContainer.Add(storyboard = working.Beatmap.Storyboard.CreateDrawable()); + storyboard.Passing = false; + } + } +} diff --git a/osu.Desktop.Tests/osu.Desktop.Tests.csproj b/osu.Desktop.Tests/osu.Desktop.Tests.csproj index e40ee5b049..f894b25f06 100644 --- a/osu.Desktop.Tests/osu.Desktop.Tests.csproj +++ b/osu.Desktop.Tests/osu.Desktop.Tests.csproj @@ -92,6 +92,7 @@ + diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 5eebad47ef..ef57265a06 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -69,7 +69,8 @@ none true bin\Release\ - CuttingEdge NoUpdate + + prompt 4 true @@ -104,6 +105,7 @@ prompt AllRules.ruleset --tests + false diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 15953fcd82..458c2304f2 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps.ControlPoints; using osu.Game.IO.Serialization; +using osu.Game.Storyboards; namespace osu.Game.Beatmaps { @@ -40,6 +41,11 @@ namespace osu.Game.Beatmaps /// public double TotalBreakTime => Breaks.Sum(b => b.Duration); + /// + /// The Beatmap's Storyboard. + /// + public Storyboard Storyboard = new Storyboard(); + /// /// Constructs a new beatmap. /// @@ -51,6 +57,7 @@ namespace osu.Game.Beatmaps Breaks = original?.Breaks ?? Breaks; ComboColors = original?.ComboColors ?? ComboColors; HitObjects = original?.HitObjects ?? HitObjects; + Storyboard = original?.Storyboard ?? Storyboard; } } diff --git a/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs b/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs index b51ea607dd..21fee0f465 100644 --- a/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/OsuLegacyDecoder.cs @@ -10,6 +10,10 @@ using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Legacy; using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Storyboards; +using OpenTK; +using osu.Framework.Graphics; +using osu.Framework.IO.File; namespace osu.Game.Beatmaps.Formats { @@ -238,42 +242,231 @@ namespace osu.Game.Beatmaps.Formats } } - private void handleEvents(Beatmap beatmap, string line) + private void handleEvents(Beatmap beatmap, string line, ref StoryboardSprite storyboardSprite, ref CommandTimelineGroup timelineGroup) { + var depth = 0; + while (line.StartsWith(" ") || line.StartsWith("_")) + { + ++depth; + line = line.Substring(1); + } + decodeVariables(ref line); string[] split = line.Split(','); - EventType type; - if (!Enum.TryParse(split[0], out type)) - throw new InvalidDataException($@"Unknown event type {split[0]}"); - - // Todo: Implement the rest - switch (type) + if (depth == 0) { - case EventType.Video: - case EventType.Background: - string filename = split[2].Trim('"'); + storyboardSprite = null; - if (type == EventType.Background) - beatmap.BeatmapInfo.Metadata.BackgroundFile = filename; + EventType type; + if (!Enum.TryParse(split[0], out type)) + throw new InvalidDataException($@"Unknown event type {split[0]}"); - break; - case EventType.Break: - var breakEvent = new BreakPeriod - { - StartTime = double.Parse(split[1], NumberFormatInfo.InvariantInfo), - EndTime = double.Parse(split[2], NumberFormatInfo.InvariantInfo) - }; + switch (type) + { + case EventType.Video: + case EventType.Background: + string filename = split[2].Trim('"'); - if (!breakEvent.HasEffect) - return; + if (type == EventType.Background) + beatmap.BeatmapInfo.Metadata.BackgroundFile = filename; - beatmap.Breaks.Add(breakEvent); - break; + break; + case EventType.Break: + var breakEvent = new BreakPeriod + { + StartTime = double.Parse(split[1], NumberFormatInfo.InvariantInfo), + EndTime = double.Parse(split[2], NumberFormatInfo.InvariantInfo) + }; + + if (!breakEvent.HasEffect) + return; + + beatmap.Breaks.Add(breakEvent); + break; + case EventType.Sprite: + { + var layer = parseLayer(split[1]); + var origin = parseOrigin(split[2]); + var path = cleanFilename(split[3]); + var x = float.Parse(split[4], NumberFormatInfo.InvariantInfo); + var y = float.Parse(split[5], NumberFormatInfo.InvariantInfo); + storyboardSprite = new StoryboardSprite(path, origin, new Vector2(x, y)); + beatmap.Storyboard.GetLayer(layer).Add(storyboardSprite); + } + break; + case EventType.Animation: + { + var layer = parseLayer(split[1]); + var origin = parseOrigin(split[2]); + var path = cleanFilename(split[3]); + var x = float.Parse(split[4], NumberFormatInfo.InvariantInfo); + var y = float.Parse(split[5], NumberFormatInfo.InvariantInfo); + var frameCount = int.Parse(split[6]); + var frameDelay = double.Parse(split[7], NumberFormatInfo.InvariantInfo); + var loopType = split.Length > 8 ? (AnimationLoopType)Enum.Parse(typeof(AnimationLoopType), split[8]) : AnimationLoopType.LoopForever; + storyboardSprite = new StoryboardAnimation(path, origin, new Vector2(x, y), frameCount, frameDelay, loopType); + beatmap.Storyboard.GetLayer(layer).Add(storyboardSprite); + } + break; + case EventType.Sample: + { + var time = double.Parse(split[1], CultureInfo.InvariantCulture); + var layer = parseLayer(split[2]); + var path = cleanFilename(split[3]); + var volume = split.Length > 4 ? float.Parse(split[4], CultureInfo.InvariantCulture) : 100; + beatmap.Storyboard.GetLayer(layer).Add(new StoryboardSample(path, time, volume)); + } + break; + } + } + else + { + if (depth < 2) + timelineGroup = storyboardSprite?.TimelineGroup; + + var commandType = split[0]; + switch (commandType) + { + case "T": + { + var triggerName = split[1]; + var startTime = split.Length > 2 ? double.Parse(split[2], CultureInfo.InvariantCulture) : double.MinValue; + var endTime = split.Length > 3 ? double.Parse(split[3], CultureInfo.InvariantCulture) : double.MaxValue; + var groupNumber = split.Length > 4 ? int.Parse(split[4]) : 0; + timelineGroup = storyboardSprite?.AddTrigger(triggerName, startTime, endTime, groupNumber); + } + break; + case "L": + { + var startTime = double.Parse(split[1], CultureInfo.InvariantCulture); + var loopCount = int.Parse(split[2]); + timelineGroup = storyboardSprite?.AddLoop(startTime, loopCount); + } + break; + default: + { + if (string.IsNullOrEmpty(split[3])) + split[3] = split[2]; + + var easing = (Easing)int.Parse(split[1]); + var startTime = double.Parse(split[2], CultureInfo.InvariantCulture); + var endTime = double.Parse(split[3], CultureInfo.InvariantCulture); + + switch (commandType) + { + case "F": + { + var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); + var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + timelineGroup?.Alpha.Add(easing, startTime, endTime, startValue, endValue); + } + break; + case "S": + { + var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); + var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + timelineGroup?.Scale.Add(easing, startTime, endTime, new Vector2(startValue), new Vector2(endValue)); + } + break; + case "V": + { + var startX = float.Parse(split[4], CultureInfo.InvariantCulture); + var startY = float.Parse(split[5], CultureInfo.InvariantCulture); + var endX = split.Length > 6 ? float.Parse(split[6], CultureInfo.InvariantCulture) : startX; + var endY = split.Length > 7 ? float.Parse(split[7], CultureInfo.InvariantCulture) : startY; + timelineGroup?.Scale.Add(easing, startTime, endTime, new Vector2(startX, startY), new Vector2(endX, endY)); + } + break; + case "R": + { + var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); + var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + timelineGroup?.Rotation.Add(easing, startTime, endTime, MathHelper.RadiansToDegrees(startValue), MathHelper.RadiansToDegrees(endValue)); + } + break; + case "M": + { + var startX = float.Parse(split[4], CultureInfo.InvariantCulture); + var startY = float.Parse(split[5], CultureInfo.InvariantCulture); + var endX = split.Length > 6 ? float.Parse(split[6], CultureInfo.InvariantCulture) : startX; + var endY = split.Length > 7 ? float.Parse(split[7], CultureInfo.InvariantCulture) : startY; + timelineGroup?.X.Add(easing, startTime, endTime, startX, endX); + timelineGroup?.Y.Add(easing, startTime, endTime, startY, endY); + } + break; + case "MX": + { + var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); + var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + timelineGroup?.X.Add(easing, startTime, endTime, startValue, endValue); + } + break; + case "MY": + { + var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); + var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; + timelineGroup?.Y.Add(easing, startTime, endTime, startValue, endValue); + } + break; + case "C": + { + var startRed = float.Parse(split[4], CultureInfo.InvariantCulture); + var startGreen = float.Parse(split[5], CultureInfo.InvariantCulture); + var startBlue = float.Parse(split[6], CultureInfo.InvariantCulture); + var endRed = split.Length > 7 ? float.Parse(split[7], CultureInfo.InvariantCulture) : startRed; + var endGreen = split.Length > 8 ? float.Parse(split[8], CultureInfo.InvariantCulture) : startGreen; + var endBlue = split.Length > 9 ? float.Parse(split[9], CultureInfo.InvariantCulture) : startBlue; + timelineGroup?.Colour.Add(easing, startTime, endTime, + new Color4(startRed / 255f, startGreen / 255f, startBlue / 255f, 1), + new Color4(endRed / 255f, endGreen / 255f, endBlue / 255f, 1)); + } + break; + case "P": + { + var type = split[4]; + switch (type) + { + case "A": timelineGroup?.BlendingMode.Add(easing, startTime, endTime, BlendingMode.Additive, startTime == endTime ? BlendingMode.Additive : BlendingMode.Inherit); break; + case "H": timelineGroup?.FlipH.Add(easing, startTime, endTime, true, startTime == endTime); break; + case "V": timelineGroup?.FlipV.Add(easing, startTime, endTime, true, startTime == endTime); break; + } + } + break; + default: + throw new InvalidDataException($@"Unknown command type: {commandType}"); + } + } + break; + } } } + private static string cleanFilename(string path) + => FileSafety.PathStandardise(path.Trim('\"')); + + private static Anchor parseOrigin(string value) + { + var origin = (LegacyOrigins)Enum.Parse(typeof(LegacyOrigins), value); + switch (origin) + { + case LegacyOrigins.TopLeft: return Anchor.TopLeft; + case LegacyOrigins.TopCentre: return Anchor.TopCentre; + case LegacyOrigins.TopRight: return Anchor.TopRight; + case LegacyOrigins.CentreLeft: return Anchor.CentreLeft; + case LegacyOrigins.Centre: return Anchor.Centre; + case LegacyOrigins.CentreRight: return Anchor.CentreRight; + case LegacyOrigins.BottomLeft: return Anchor.BottomLeft; + case LegacyOrigins.BottomCentre: return Anchor.BottomCentre; + case LegacyOrigins.BottomRight: return Anchor.BottomRight; + } + throw new InvalidDataException($@"Unknown origin: {value}"); + } + + private static string parseLayer(string value) + => Enum.Parse(typeof(StoryLayer), value).ToString(); + private void handleTimingPoints(Beatmap beatmap, string line) { string[] split = line.Split(','); @@ -414,6 +607,8 @@ namespace osu.Game.Beatmaps.Formats Section section = Section.None; bool hasCustomColours = false; + StoryboardSprite storyboardSprite = null; + CommandTimelineGroup timelineGroup = null; string line; while ((line = stream.ReadLine()) != null) @@ -421,7 +616,7 @@ namespace osu.Game.Beatmaps.Formats if (string.IsNullOrEmpty(line)) continue; - if (line.StartsWith(" ") || line.StartsWith("_") || line.StartsWith("//")) + if (line.StartsWith("//")) continue; if (line.StartsWith(@"osu file format v")) @@ -452,7 +647,7 @@ namespace osu.Game.Beatmaps.Formats handleDifficulty(beatmap, line); break; case Section.Events: - handleEvents(beatmap, line); + handleEvents(beatmap, line, ref storyboardSprite, ref timelineGroup); break; case Section.TimingPoints: handleTimingPoints(beatmap, line); @@ -509,5 +704,27 @@ namespace osu.Game.Beatmaps.Formats Sample = 5, Animation = 6 } + + internal enum LegacyOrigins + { + TopLeft, + Centre, + CentreLeft, + TopRight, + BottomCentre, + TopCentre, + Custom, + CentreRight, + BottomLeft, + BottomRight + }; + + internal enum StoryLayer + { + Background = 0, + Fail = 1, + Pass = 2, + Foreground = 3 + } } } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index df8e7f5e3b..bb3122489e 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -100,8 +100,11 @@ namespace osu.Game.Beatmaps public void TransferTo(WorkingBeatmap other) { - if (track != null && BeatmapInfo.AudioEquals(other.BeatmapInfo)) - other.track = track; + lock (trackLock) + { + if (track != null && BeatmapInfo.AudioEquals(other.BeatmapInfo)) + other.track = track; + } if (background != null && BeatmapInfo.BackgroundEquals(other.BeatmapInfo)) other.background = background; diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs index 32a37a4910..ccc23e3ff6 100644 --- a/osu.Game/Graphics/UserInterface/OsuButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuButton.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.Collections.Generic; using OpenTK.Graphics; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -112,7 +113,7 @@ namespace osu.Game.Graphics.UserInterface return base.OnMouseUp(state, args); } - public string[] FilterTerms => new[] { Text }; + public IEnumerable FilterTerms => new[] { Text }; public bool MatchingFilter { diff --git a/osu.Game/Overlays/Chat/ChannelListItem.cs b/osu.Game/Overlays/Chat/ChannelListItem.cs index 8360e793d8..f4cf806044 100644 --- a/osu.Game/Overlays/Chat/ChannelListItem.cs +++ b/osu.Game/Overlays/Chat/ChannelListItem.cs @@ -2,6 +2,7 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Collections.Generic; using OpenTK; using OpenTK.Graphics; using osu.Framework.Allocation; @@ -34,7 +35,7 @@ namespace osu.Game.Overlays.Chat private Color4 topicColour; private Color4 hoverColour; - public string[] FilterTerms => new[] { channel.Name }; + public IEnumerable FilterTerms => new[] { channel.Name }; public bool MatchingFilter { set diff --git a/osu.Game/Overlays/Chat/ChannelSection.cs b/osu.Game/Overlays/Chat/ChannelSection.cs index 1f046aff2a..5068b415bc 100644 --- a/osu.Game/Overlays/Chat/ChannelSection.cs +++ b/osu.Game/Overlays/Chat/ChannelSection.cs @@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Chat public readonly FillFlowContainer ChannelFlow; public IEnumerable FilterableChildren => ChannelFlow.Children; - public string[] FilterTerms => new[] { Header }; + public IEnumerable FilterTerms => new[] { Header }; public bool MatchingFilter { set diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index 046e56573f..fb84853a0d 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -47,7 +47,7 @@ namespace osu.Game.Overlays.KeyBinding private FillFlowContainer buttons; - public string[] FilterTerms => new[] { text.Text }.Concat(bindings.Select(b => b.KeyCombination.ReadableString())).ToArray(); + public IEnumerable FilterTerms => new[] { text.Text }.Concat(bindings.Select(b => b.KeyCombination.ReadableString())).ToArray(); public KeyBindingRow(object action, IEnumerable bindings) { @@ -371,4 +371,4 @@ namespace osu.Game.Overlays.KeyBinding } } } -} \ No newline at end of file +} diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 2aaa182685..723b3f4e96 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -129,7 +129,7 @@ namespace osu.Game.Overlays.Music return true; } - public string[] FilterTerms { get; private set; } + public IEnumerable FilterTerms { get; private set; } private bool matching = true; diff --git a/osu.Game/Overlays/Music/PlaylistList.cs b/osu.Game/Overlays/Music/PlaylistList.cs index 360e2ad843..6f1eaded7f 100644 --- a/osu.Game/Overlays/Music/PlaylistList.cs +++ b/osu.Game/Overlays/Music/PlaylistList.cs @@ -229,7 +229,7 @@ namespace osu.Game.Overlays.Music private class ItemSearchContainer : FillFlowContainer, IHasFilterableChildren { - public string[] FilterTerms => new string[] { }; + public IEnumerable FilterTerms => new string[] { }; public bool MatchingFilter { set diff --git a/osu.Game/Overlays/Settings/Sections/Debug/GCSettings.cs b/osu.Game/Overlays/Settings/Sections/Debug/GCSettings.cs index 0a32b50809..495a2543d1 100644 --- a/osu.Game/Overlays/Settings/Sections/Debug/GCSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Debug/GCSettings.cs @@ -28,7 +28,7 @@ namespace osu.Game.Overlays.Settings.Sections.Debug { RelativeSizeAxes = Axes.X, Text = "Force garbage collection", - Action = () => GC.Collect() + Action = GC.Collect }, }; } diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 01e32b5a1b..833a5ff966 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Settings.Sections.General { RelativeSizeAxes = Axes.X, Text = "Open osu! folder", - Action = () => storage.OpenInNativeExplorer(), + Action = storage.OpenInNativeExplorer, } }; } diff --git a/osu.Game/Overlays/Settings/Sections/Input/KeyboardSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/KeyboardSettings.cs index 01e73d0168..b68fd4bc04 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/KeyboardSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/KeyboardSettings.cs @@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input { RelativeSizeAxes = Axes.X, Text = "Key Configuration", - Action = () => keyConfig.ToggleVisibility() + Action = keyConfig.ToggleVisibility }, }; } diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index c74f4070e7..f2044f178b 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.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.Collections.Generic; using OpenTK.Graphics; using osu.Framework.Configuration; using osu.Framework.Graphics; @@ -53,7 +54,7 @@ namespace osu.Game.Overlays.Settings } } - public string[] FilterTerms => new[] { LabelText }; + public IEnumerable FilterTerms => new[] { LabelText }; public bool MatchingFilter { diff --git a/osu.Game/Overlays/Settings/SettingsSection.cs b/osu.Game/Overlays/Settings/SettingsSection.cs index f091192d27..eb6e398477 100644 --- a/osu.Game/Overlays/Settings/SettingsSection.cs +++ b/osu.Game/Overlays/Settings/SettingsSection.cs @@ -23,7 +23,7 @@ namespace osu.Game.Overlays.Settings public abstract string Header { get; } public IEnumerable FilterableChildren => Children.OfType(); - public string[] FilterTerms => new[] { Header }; + public IEnumerable FilterTerms => new[] { Header }; private const int header_size = 26; private const int header_margin = 25; diff --git a/osu.Game/Overlays/Settings/SettingsSubsection.cs b/osu.Game/Overlays/Settings/SettingsSubsection.cs index 0fbb5b92f7..4164ceee21 100644 --- a/osu.Game/Overlays/Settings/SettingsSubsection.cs +++ b/osu.Game/Overlays/Settings/SettingsSubsection.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Settings protected abstract string Header { get; } public IEnumerable FilterableChildren => Children.OfType(); - public string[] FilterTerms => new[] { Header }; + public IEnumerable FilterTerms => new[] { Header }; public bool MatchingFilter { set diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 2544cc2837..1c82d15f50 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Menu OnEdit = delegate { Push(new Editor()); }, OnSolo = delegate { Push(consumeSongSelect()); }, OnMulti = delegate { Push(new Lobby()); }, - OnExit = delegate { Exit(); }, + OnExit = Exit, } } }, diff --git a/osu.Game/Screens/Play/SongProgressGraph.cs b/osu.Game/Screens/Play/SongProgressGraph.cs index 541065e532..38c680902a 100644 --- a/osu.Game/Screens/Play/SongProgressGraph.cs +++ b/osu.Game/Screens/Play/SongProgressGraph.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Collections.Generic; +using System.Diagnostics; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects; @@ -34,10 +35,12 @@ namespace osu.Game.Screens.Play foreach (var h in objects) { - IHasEndTime end = h as IHasEndTime; + var endTime = (h as IHasEndTime)?.EndTime ?? h.StartTime; + + Debug.Assert(endTime >= h.StartTime); int startRange = (int)((h.StartTime - firstHit) / interval); - int endRange = (int)(((end?.EndTime > 0 ? end.EndTime : h.StartTime) - firstHit) / interval); + int endRange = (int)((endTime - firstHit) / interval); for (int i = startRange; i <= endRange; i++) Values[i]++; } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index f97c4fe420..84457b77a7 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -106,7 +106,7 @@ namespace osu.Game.Screens.Select Origin = Anchor.CentreRight, SelectionChanged = carouselSelectionChanged, BeatmapsChanged = carouselBeatmapsLoaded, - DeleteRequested = b => promptDelete(b), + DeleteRequested = promptDelete, RestoreRequested = s => { foreach (var b in s.Beatmaps) manager.Restore(b); }, HideDifficultyRequested = b => manager.Hide(b), StartRequested = () => carouselRaisedStart(), diff --git a/osu.Game/Screens/Tournament/Drawings.cs b/osu.Game/Screens/Tournament/Drawings.cs index 7cd81a924d..3d27552212 100644 --- a/osu.Game/Screens/Tournament/Drawings.cs +++ b/osu.Game/Screens/Tournament/Drawings.cs @@ -237,7 +237,7 @@ namespace osu.Game.Screens.Tournament RelativeSizeAxes = Axes.X, Text = "Reset", - Action = () => reset(false) + Action = () => reset() } } } diff --git a/osu.Game/Storyboards/CommandLoop.cs b/osu.Game/Storyboards/CommandLoop.cs new file mode 100644 index 0000000000..02b5eb0122 --- /dev/null +++ b/osu.Game/Storyboards/CommandLoop.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; + +namespace osu.Game.Storyboards +{ + public class CommandLoop : CommandTimelineGroup + { + public double LoopStartTime; + public int LoopCount; + + public override double StartTime => LoopStartTime; + public override double EndTime => LoopStartTime + CommandsDuration * LoopCount; + + public CommandLoop(double startTime, int loopCount) + { + LoopStartTime = startTime; + LoopCount = loopCount; + } + + public override IEnumerable.TypedCommand> GetCommands(CommandTimelineSelector timelineSelector, double offset = 0) + { + for (var loop = 0; loop < LoopCount; loop++) + { + var loopOffset = LoopStartTime + loop * CommandsDuration; + foreach (var command in base.GetCommands(timelineSelector, offset + loopOffset)) + yield return command; + } + } + + public override string ToString() + => $"{LoopStartTime} x{LoopCount}"; + } +} diff --git a/osu.Game/Storyboards/CommandTimeline.cs b/osu.Game/Storyboards/CommandTimeline.cs new file mode 100644 index 0000000000..b9bb6629d1 --- /dev/null +++ b/osu.Game/Storyboards/CommandTimeline.cs @@ -0,0 +1,77 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Framework.Caching; +using osu.Framework.Graphics; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace osu.Game.Storyboards +{ + public class CommandTimeline : ICommandTimeline + { + private readonly List commands = new List(); + public IEnumerable Commands => commands.OrderBy(c => c.StartTime); + public bool HasCommands => commands.Count > 0; + + private Cached startTimeBacking; + public double StartTime => startTimeBacking.IsValid ? startTimeBacking : (startTimeBacking.Value = HasCommands ? commands.Min(c => c.StartTime) : double.MinValue); + + private Cached endTimeBacking; + public double EndTime => endTimeBacking.IsValid ? endTimeBacking : (endTimeBacking.Value = HasCommands ? commands.Max(c => c.EndTime) : double.MaxValue); + + public T StartValue => HasCommands ? commands.OrderBy(c => c.StartTime).First().StartValue : default(T); + public T EndValue => HasCommands ? commands.OrderByDescending(c => c.EndTime).First().EndValue : default(T); + + public void Add(Easing easing, double startTime, double endTime, T startValue, T endValue) + { + if (endTime < startTime) + return; + + commands.Add(new TypedCommand { Easing = easing, StartTime = startTime, EndTime = endTime, StartValue = startValue, EndValue = endValue, }); + + startTimeBacking.Invalidate(); + endTimeBacking.Invalidate(); + } + + public override string ToString() + => $"{commands.Count} command(s)"; + + public class TypedCommand : ICommand + { + public Easing Easing { get; set; } + public double StartTime { get; set; } + public double EndTime { get; set; } + public double Duration => EndTime - StartTime; + + public T StartValue; + public T EndValue; + + public int CompareTo(ICommand other) + { + var result = StartTime.CompareTo(other.StartTime); + if (result != 0) return result; + return EndTime.CompareTo(other.EndTime); + } + + public override string ToString() + => $"{StartTime} -> {EndTime}, {StartValue} -> {EndValue} {Easing}"; + } + } + + public interface ICommandTimeline + { + double StartTime { get; } + double EndTime { get; } + bool HasCommands { get; } + } + + public interface ICommand : IComparable + { + Easing Easing { get; set; } + double StartTime { get; set; } + double EndTime { get; set; } + double Duration { get; } + } +} diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs new file mode 100644 index 0000000000..332a6f79cb --- /dev/null +++ b/osu.Game/Storyboards/CommandTimelineGroup.cs @@ -0,0 +1,68 @@ +// 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.Graphics; +using System.Collections.Generic; +using System.Linq; + +namespace osu.Game.Storyboards +{ + public delegate CommandTimeline CommandTimelineSelector(CommandTimelineGroup commandTimelineGroup); + + public class CommandTimelineGroup + { + public CommandTimeline X = new CommandTimeline(); + public CommandTimeline Y = new CommandTimeline(); + public CommandTimeline Scale = new CommandTimeline(); + public CommandTimeline Rotation = new CommandTimeline(); + public CommandTimeline Colour = new CommandTimeline(); + public CommandTimeline Alpha = new CommandTimeline(); + public CommandTimeline BlendingMode = new CommandTimeline(); + public CommandTimeline FlipH = new CommandTimeline(); + public CommandTimeline FlipV = new CommandTimeline(); + + public IEnumerable Timelines + { + get + { + yield return X; + yield return Y; + yield return Scale; + yield return Rotation; + yield return Colour; + yield return Alpha; + yield return BlendingMode; + yield return FlipH; + yield return FlipV; + } + } + + public double CommandsStartTime => Timelines.Where(t => t.HasCommands).Min(t => t.StartTime); + public double CommandsEndTime => Timelines.Where(t => t.HasCommands).Max(t => t.EndTime); + public double CommandsDuration => CommandsEndTime - CommandsStartTime; + + public virtual double StartTime => CommandsStartTime; + public virtual double EndTime => CommandsEndTime; + public double Duration => EndTime - StartTime; + + public bool HasCommands => Timelines.Any(t => t.HasCommands); + + public virtual IEnumerable.TypedCommand> GetCommands(CommandTimelineSelector timelineSelector, double offset = 0) + { + if (offset != 0) + return timelineSelector(this).Commands.Select(command => + new CommandTimeline.TypedCommand + { + Easing = command.Easing, + StartTime = offset + command.StartTime, + EndTime = offset + command.EndTime, + StartValue = command.StartValue, + EndValue = command.EndValue, + }); + + return timelineSelector(this).Commands; + } + } +} diff --git a/osu.Game/Storyboards/CommandTrigger.cs b/osu.Game/Storyboards/CommandTrigger.cs new file mode 100644 index 0000000000..e2731f9c45 --- /dev/null +++ b/osu.Game/Storyboards/CommandTrigger.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 + +namespace osu.Game.Storyboards +{ + public class CommandTrigger : CommandTimelineGroup + { + public string TriggerName; + public double TriggerStartTime; + public double TriggerEndTime; + public int GroupNumber; + + public CommandTrigger(string triggerName, double startTime, double endTime, int groupNumber) + { + TriggerName = triggerName; + TriggerStartTime = startTime; + TriggerEndTime = endTime; + GroupNumber = groupNumber; + } + + public override string ToString() + => $"{TriggerName} {TriggerStartTime} -> {TriggerEndTime} ({GroupNumber})"; + } +} diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs new file mode 100644 index 0000000000..f88e5d118f --- /dev/null +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -0,0 +1,59 @@ +// 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Textures; +using osu.Game.IO; + +namespace osu.Game.Storyboards.Drawables +{ + public class DrawableStoryboard : Container + { + public Storyboard Storyboard { get; private set; } + + protected override Vector2 DrawScale => new Vector2(Parent.DrawHeight / 480); + public override bool HandleInput => false; + + private bool passing = true; + public bool Passing + { + get { return passing; } + set + { + if (passing == value) return; + passing = value; + updateLayerVisibility(); + } + } + + private DependencyContainer dependencies; + protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent) => + dependencies = new DependencyContainer(base.CreateLocalDependencies(parent)); + + public DrawableStoryboard(Storyboard storyboard) + { + Storyboard = storyboard; + Size = new Vector2(640, 480); + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load(FileStore fileStore) + { + dependencies.Cache(new TextureStore(new RawTextureLoaderStore(fileStore.Store), false) { ScaleAdjust = 1, }); + + foreach (var layer in Storyboard.Layers) + Add(layer.CreateDrawable()); + } + + private void updateLayerVisibility() + { + foreach (var layer in Children) + layer.Enabled = passing ? layer.Layer.EnabledWhenPassing : layer.Layer.EnabledWhenFailing; + } + } +} diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs new file mode 100644 index 0000000000..d8b7d05ee9 --- /dev/null +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -0,0 +1,87 @@ +// 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.Graphics; +using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Textures; +using System.Linq; + +namespace osu.Game.Storyboards.Drawables +{ + public class DrawableStoryboardAnimation : TextureAnimation, IFlippable + { + public StoryboardAnimation Animation { get; private set; } + + protected override bool ShouldBeAlive => Animation.HasCommands && base.ShouldBeAlive; + public override bool RemoveWhenNotAlive => !Animation.HasCommands || base.RemoveWhenNotAlive; + + public bool FlipH { get; set; } + public bool FlipV { get; set; } + + protected override Vector2 DrawScale + => new Vector2(FlipH ? -base.DrawScale.X : base.DrawScale.X, FlipV ? -base.DrawScale.Y : base.DrawScale.Y); + + public override Anchor Origin + { + get + { + var origin = base.Origin; + + if (FlipH) + { + if (origin.HasFlag(Anchor.x0)) + origin = Anchor.x2 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); + else if (origin.HasFlag(Anchor.x2)) + origin = Anchor.x0 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); + } + + if (FlipV) + { + if (origin.HasFlag(Anchor.y0)) + origin = Anchor.y2 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); + else if (origin.HasFlag(Anchor.y2)) + origin = Anchor.y0 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); + } + + return origin; + } + } + + public override bool IsPresent + => !float.IsNaN(DrawPosition.X) && !float.IsNaN(DrawPosition.Y) && base.IsPresent; + + public DrawableStoryboardAnimation(StoryboardAnimation animation) + { + Animation = animation; + Origin = animation.Origin; + Position = animation.InitialPosition; + Repeat = animation.LoopType == AnimationLoopType.LoopForever; + + if (animation.HasCommands) + { + LifetimeStart = animation.StartTime; + LifetimeEnd = animation.EndTime; + } + } + + [BackgroundDependencyLoader] + private void load(OsuGameBase game, TextureStore textureStore) + { + var basePath = Animation.Path.ToLowerInvariant(); + for (var frame = 0; frame < Animation.FrameCount; frame++) + { + var framePath = basePath.Replace(".", frame + "."); + + var path = game.Beatmap.Value.BeatmapSetInfo.Files.FirstOrDefault(f => f.Filename.ToLowerInvariant() == framePath)?.FileInfo.StoragePath; + if (path == null) + continue; + + var texture = textureStore.Get(path); + AddFrame(texture, Animation.FrameDelay); + } + Animation.ApplyTransforms(this); + } + } +} diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs new file mode 100644 index 0000000000..2b5db5b6fa --- /dev/null +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardLayer.cs @@ -0,0 +1,37 @@ +// 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; + +namespace osu.Game.Storyboards.Drawables +{ + public class DrawableStoryboardLayer : Container + { + public StoryboardLayer Layer { get; private set; } + public bool Enabled; + + public override bool IsPresent => Enabled && base.IsPresent; + + public DrawableStoryboardLayer(StoryboardLayer layer) + { + Layer = layer; + RelativeSizeAxes = Axes.Both; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Enabled = layer.EnabledWhenPassing; + } + + [BackgroundDependencyLoader] + private void load() + { + foreach (var element in Layer.Elements) + { + var drawable = element.CreateDrawable(); + if (drawable != null) + Add(drawable); + } + } + } +} diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs new file mode 100644 index 0000000000..4b491fa008 --- /dev/null +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -0,0 +1,80 @@ +// 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.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using System.Linq; + +namespace osu.Game.Storyboards.Drawables +{ + public class DrawableStoryboardSprite : Sprite, IFlippable + { + public StoryboardSprite Sprite { get; private set; } + + protected override bool ShouldBeAlive => Sprite.HasCommands && base.ShouldBeAlive; + public override bool RemoveWhenNotAlive => !Sprite.HasCommands || base.RemoveWhenNotAlive; + + public bool FlipH { get; set; } + public bool FlipV { get; set; } + + protected override Vector2 DrawScale + => new Vector2(FlipH ? -base.DrawScale.X : base.DrawScale.X, FlipV ? -base.DrawScale.Y : base.DrawScale.Y); + + public override Anchor Origin + { + get + { + var origin = base.Origin; + + if (FlipH) + { + if (origin.HasFlag(Anchor.x0)) + origin = Anchor.x2 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); + else if (origin.HasFlag(Anchor.x2)) + origin = Anchor.x0 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); + } + + if (FlipV) + { + if (origin.HasFlag(Anchor.y0)) + origin = Anchor.y2 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); + else if (origin.HasFlag(Anchor.y2)) + origin = Anchor.y0 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); + } + + return origin; + } + } + + public override bool IsPresent + => !float.IsNaN(DrawPosition.X) && !float.IsNaN(DrawPosition.Y) && base.IsPresent; + + public DrawableStoryboardSprite(StoryboardSprite sprite) + { + Sprite = sprite; + Origin = sprite.Origin; + Position = sprite.InitialPosition; + + if (sprite.HasCommands) + { + LifetimeStart = sprite.StartTime; + LifetimeEnd = sprite.EndTime; + } + } + + [BackgroundDependencyLoader] + private void load(OsuGameBase game, TextureStore textureStore) + { + var spritePath = Sprite.Path.ToLowerInvariant(); + var path = game.Beatmap.Value.BeatmapSetInfo.Files.FirstOrDefault(f => f.Filename.ToLowerInvariant() == spritePath)?.FileInfo.StoragePath; + if (path == null) + return; + + Texture = textureStore.Get(path); + Sprite.ApplyTransforms(this); + } + } +} diff --git a/osu.Game/Storyboards/Drawables/DrawablesExtensions.cs b/osu.Game/Storyboards/Drawables/DrawablesExtensions.cs new file mode 100644 index 0000000000..3b21c47b96 --- /dev/null +++ b/osu.Game/Storyboards/Drawables/DrawablesExtensions.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.Graphics; +using osu.Framework.Graphics.Transforms; + +namespace osu.Game.Storyboards.Drawables +{ + public static class DrawablesExtensions + { + /// + /// Adjusts after a delay. + /// + /// A to which further transforms can be added. + public static TransformSequence TransformBlendingMode(this T drawable, BlendingMode newValue, double delay = 0) + where T : Drawable + => drawable.TransformTo(drawable.PopulateTransform(new TransformBlendingMode(), newValue, delay)); + } + + public class TransformBlendingMode : Transform + { + private BlendingMode valueAt(double time) + => time < EndTime ? StartValue : EndValue; + + public override string TargetMember => nameof(Drawable.Blending); + + protected override void Apply(Drawable d, double time) => d.Blending = valueAt(time); + protected override void ReadIntoStartValue(Drawable d) => StartValue = d.Blending.Mode; + } +} diff --git a/osu.Game/Storyboards/Drawables/IFlippable.cs b/osu.Game/Storyboards/Drawables/IFlippable.cs new file mode 100644 index 0000000000..4d21c9d140 --- /dev/null +++ b/osu.Game/Storyboards/Drawables/IFlippable.cs @@ -0,0 +1,55 @@ +// 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.Transforms; + +namespace osu.Game.Storyboards.Drawables +{ + public interface IFlippable : ITransformable + { + bool FlipH { get; set; } + bool FlipV { get; set; } + } + + public class TransformFlipH : Transform + { + private bool valueAt(double time) + => time < EndTime ? StartValue : EndValue; + + public override string TargetMember => nameof(IFlippable.FlipH); + + protected override void Apply(IFlippable d, double time) => d.FlipH = valueAt(time); + protected override void ReadIntoStartValue(IFlippable d) => StartValue = d.FlipH; + } + + public class TransformFlipV : Transform + { + private bool valueAt(double time) + => time < EndTime ? StartValue : EndValue; + + public override string TargetMember => nameof(IFlippable.FlipV); + + protected override void Apply(IFlippable d, double time) => d.FlipV = valueAt(time); + protected override void ReadIntoStartValue(IFlippable d) => StartValue = d.FlipV; + } + + public static class FlippableExtensions + { + /// + /// Adjusts after a delay. + /// + /// A to which further transforms can be added. + public static TransformSequence TransformFlipH(this T flippable, bool newValue, double delay = 0) + where T : IFlippable + => flippable.TransformTo(flippable.PopulateTransform(new TransformFlipH(), newValue, delay)); + + /// + /// Adjusts after a delay. + /// + /// A to which further transforms can be added. + public static TransformSequence TransformFlipV(this T flippable, bool newValue, double delay = 0) + where T : IFlippable + => flippable.TransformTo(flippable.PopulateTransform(new TransformFlipV(), newValue, delay)); + } +} diff --git a/osu.Game/Storyboards/IStoryboardElement.cs b/osu.Game/Storyboards/IStoryboardElement.cs new file mode 100644 index 0000000000..d5fc86b0f7 --- /dev/null +++ b/osu.Game/Storyboards/IStoryboardElement.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 + +using osu.Framework.Graphics; + +namespace osu.Game.Storyboards +{ + public interface IStoryboardElement + { + string Path { get; } + Drawable CreateDrawable(); + } +} diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs new file mode 100644 index 0000000000..111cdd5d41 --- /dev/null +++ b/osu.Game/Storyboards/Storyboard.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 osu.Game.Storyboards.Drawables; +using System.Collections.Generic; +using System.Linq; + +namespace osu.Game.Storyboards +{ + public class Storyboard + { + private readonly Dictionary layers = new Dictionary(); + public IEnumerable Layers => layers.Values; + + public Storyboard() + { + layers.Add("Background", new StoryboardLayer("Background", 3)); + layers.Add("Fail", new StoryboardLayer("Fail", 2) { EnabledWhenPassing = false, }); + layers.Add("Pass", new StoryboardLayer("Pass", 1) { EnabledWhenFailing = false, }); + layers.Add("Foreground", new StoryboardLayer("Foreground", 0)); + } + + public StoryboardLayer GetLayer(string name) + { + StoryboardLayer layer; + if (!layers.TryGetValue(name, out layer)) + layers[name] = layer = new StoryboardLayer(name, layers.Values.Min(l => l.Depth) - 1); + + return layer; + } + + public DrawableStoryboard CreateDrawable() + => new DrawableStoryboard(this); + } +} diff --git a/osu.Game/Storyboards/StoryboardAnimation.cs b/osu.Game/Storyboards/StoryboardAnimation.cs new file mode 100644 index 0000000000..98936df9e5 --- /dev/null +++ b/osu.Game/Storyboards/StoryboardAnimation.cs @@ -0,0 +1,33 @@ +// 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.Graphics; +using osu.Game.Storyboards.Drawables; + +namespace osu.Game.Storyboards +{ + public class StoryboardAnimation : StoryboardSprite + { + public int FrameCount; + public double FrameDelay; + public AnimationLoopType LoopType; + + public StoryboardAnimation(string path, Anchor origin, Vector2 initialPosition, int frameCount, double frameDelay, AnimationLoopType loopType) + : base(path, origin, initialPosition) + { + FrameCount = frameCount; + FrameDelay = frameDelay; + LoopType = loopType; + } + + public override Drawable CreateDrawable() + => new DrawableStoryboardAnimation(this); + } + + public enum AnimationLoopType + { + LoopForever, + LoopOnce, + } +} diff --git a/osu.Game/Storyboards/StoryboardLayer.cs b/osu.Game/Storyboards/StoryboardLayer.cs new file mode 100644 index 0000000000..f565b13eb5 --- /dev/null +++ b/osu.Game/Storyboards/StoryboardLayer.cs @@ -0,0 +1,33 @@ +// Copyright (c) 2007-2017 ppy Pty Ltd . +// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE + +using osu.Game.Storyboards.Drawables; +using System.Collections.Generic; + +namespace osu.Game.Storyboards +{ + public class StoryboardLayer + { + public string Name; + public int Depth; + public bool EnabledWhenPassing = true; + public bool EnabledWhenFailing = true; + + private readonly List elements = new List(); + public IEnumerable Elements => elements; + + public StoryboardLayer(string name, int depth) + { + Name = name; + Depth = depth; + } + + public void Add(IStoryboardElement element) + { + elements.Add(element); + } + + public DrawableStoryboardLayer CreateDrawable() + => new DrawableStoryboardLayer(this) { Depth = Depth, }; + } +} diff --git a/osu.Game/Storyboards/StoryboardSample.cs b/osu.Game/Storyboards/StoryboardSample.cs new file mode 100644 index 0000000000..bcf6a4329d --- /dev/null +++ b/osu.Game/Storyboards/StoryboardSample.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.Framework.Graphics; + +namespace osu.Game.Storyboards +{ + public class StoryboardSample : IStoryboardElement + { + public string Path { get; set; } + public double Time; + public float Volume; + + public StoryboardSample(string path, double time, float volume) + { + Path = path; + Time = time; + Volume = volume; + } + + public Drawable CreateDrawable() + => null; + } +} diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs new file mode 100644 index 0000000000..598167d720 --- /dev/null +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -0,0 +1,113 @@ +// 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.Graphics; +using osu.Game.Storyboards.Drawables; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace osu.Game.Storyboards +{ + public class StoryboardSprite : IStoryboardElement + { + private readonly List loops = new List(); + private readonly List triggers = new List(); + + public string Path { get; set; } + public Anchor Origin; + public Vector2 InitialPosition; + + public readonly CommandTimelineGroup TimelineGroup = new CommandTimelineGroup(); + + public double StartTime => Math.Min( + TimelineGroup.HasCommands ? TimelineGroup.CommandsStartTime : double.MaxValue, + loops.Any(l => l.HasCommands) ? loops.Where(l => l.HasCommands).Min(l => l.StartTime) : double.MaxValue); + + public double EndTime => Math.Max( + TimelineGroup.HasCommands ? TimelineGroup.CommandsEndTime : double.MinValue, + loops.Any(l => l.HasCommands) ? loops.Where(l => l.HasCommands).Max(l => l.EndTime) : double.MinValue); + + public bool HasCommands => TimelineGroup.HasCommands || loops.Any(l => l.HasCommands); + + private delegate void DrawablePropertyInitializer(Drawable drawable, T value); + private delegate void DrawableTransformer(Drawable drawable, T value, double duration, Easing easing); + + public StoryboardSprite(string path, Anchor origin, Vector2 initialPosition) + { + Path = path; + Origin = origin; + InitialPosition = initialPosition; + } + + public CommandLoop AddLoop(double startTime, int loopCount) + { + var loop = new CommandLoop(startTime, loopCount); + loops.Add(loop); + return loop; + } + + public CommandTrigger AddTrigger(string triggerName, double startTime, double endTime, int groupNumber) + { + var trigger = new CommandTrigger(triggerName, startTime, endTime, groupNumber); + triggers.Add(trigger); + return trigger; + } + + public virtual Drawable CreateDrawable() + => new DrawableStoryboardSprite(this); + + public void ApplyTransforms(Drawable drawable, IEnumerable> triggeredGroups = null) + { + applyCommands(drawable, getCommands(g => g.X, triggeredGroups), (d, value) => d.X = value, (d, value, duration, easing) => d.MoveToX(value, duration, easing)); + applyCommands(drawable, getCommands(g => g.Y, triggeredGroups), (d, value) => d.Y = value, (d, value, duration, easing) => d.MoveToY(value, duration, easing)); + applyCommands(drawable, getCommands(g => g.Scale, triggeredGroups), (d, value) => d.Scale = value, (d, value, duration, easing) => d.ScaleTo(value, duration, easing)); + applyCommands(drawable, getCommands(g => g.Rotation, triggeredGroups), (d, value) => d.Rotation = value, (d, value, duration, easing) => d.RotateTo(value, duration, easing)); + applyCommands(drawable, getCommands(g => g.Colour, triggeredGroups), (d, value) => d.Colour = value, (d, value, duration, easing) => d.FadeColour(value, duration, easing)); + applyCommands(drawable, getCommands(g => g.Alpha, triggeredGroups), (d, value) => d.Alpha = value, (d, value, duration, easing) => d.FadeTo(value, duration, easing)); + applyCommands(drawable, getCommands(g => g.BlendingMode, triggeredGroups), (d, value) => d.Blending = value, (d, value, duration, easing) => d.TransformBlendingMode(value, duration), false); + + var flippable = drawable as IFlippable; + if (flippable != null) + { + applyCommands(drawable, getCommands(g => g.FlipH, triggeredGroups), (d, value) => flippable.FlipH = value, (d, value, duration, easing) => flippable.TransformFlipH(value, duration), false); + applyCommands(drawable, getCommands(g => g.FlipV, triggeredGroups), (d, value) => flippable.FlipV = value, (d, value, duration, easing) => flippable.TransformFlipV(value, duration), false); + } + } + + private void applyCommands(Drawable drawable, IEnumerable.TypedCommand> commands, DrawablePropertyInitializer initializeProperty, DrawableTransformer transform, bool alwaysInitialize = true) + where T : struct + { + var initialized = false; + foreach (var command in commands.OrderBy(l => l)) + { + if (!initialized) + { + if (alwaysInitialize || command.StartTime == command.EndTime) + initializeProperty.Invoke(drawable, command.StartValue); + initialized = true; + } + using (drawable.BeginAbsoluteSequence(command.StartTime)) + { + transform(drawable, command.StartValue, 0, Easing.None); + transform(drawable, command.EndValue, command.Duration, command.Easing); + } + } + } + + private IEnumerable.TypedCommand> getCommands(CommandTimelineSelector timelineSelector, IEnumerable> triggeredGroups) + { + var commands = TimelineGroup.GetCommands(timelineSelector); + foreach (var loop in loops) + commands = commands.Concat(loop.GetCommands(timelineSelector)); + if (triggeredGroups != null) + foreach (var pair in triggeredGroups) + commands = commands.Concat(pair.Item1.GetCommands(timelineSelector, pair.Item2)); + return commands; + } + + public override string ToString() + => $"{Path}, {Origin}, {InitialPosition}"; + } +} diff --git a/osu.Game/Users/Avatar.cs b/osu.Game/Users/Avatar.cs index 5d518f1780..111c901ca0 100644 --- a/osu.Game/Users/Avatar.cs +++ b/osu.Game/Users/Avatar.cs @@ -26,7 +26,7 @@ namespace osu.Game.Users private void load(TextureStore textures) { Texture texture = null; - if (user?.Id > 1) texture = textures.Get($@"https://a.ppy.sh/{user.Id}"); + if (user != null && user.Id > 1) texture = textures.Get($@"https://a.ppy.sh/{user.Id}"); if (texture == null) texture = textures.Get(@"Online/avatar-guest"); Add(new Sprite diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 65ec7d31b3..92bcaf90f0 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -81,6 +81,22 @@ + + + + + + + + + + + + + + + + diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 0b8c196ec8..4011e3991f 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -42,6 +42,7 @@ WARNING WARNING ERROR + HINT HINT WARNING WARNING @@ -56,6 +57,8 @@ WARNING HINT HINT + HINT + HINT HINT WARNING WARNING @@ -617,6 +620,7 @@ Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-frame <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" />