1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 08:22:56 +08:00

Merge branch 'master' into skin-bindables

This commit is contained in:
Bartłomiej Dach 2021-05-05 20:16:27 +02:00 committed by GitHub
commit 3cac837acf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 569 additions and 97 deletions

View File

@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
Entry = null; Entry = null;
} }
private void onEntryInvalidated() => refreshPoints(); private void onEntryInvalidated() => Scheduler.AddOnce(refreshPoints);
private void refreshPoints() private void refreshPoints()
{ {

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
@ -278,6 +279,54 @@ namespace osu.Game.Tests.NonVisual
setTime(-100, -100); setTime(-100, -100);
} }
[Test]
public void TestReplayFramesSortStability()
{
const double repeating_time = 5000;
// add a collection of frames in shuffled order time-wise; each frame also stores its original index to check stability later.
// data is hand-picked and breaks if the unstable List<T>.Sort() is used.
// in theory this can still return a false-positive with another unstable algorithm if extremely unlucky,
// but there is no conceivable fool-proof way to prevent that anyways.
replay.Frames.AddRange(new[]
{
repeating_time,
0,
3000,
repeating_time,
repeating_time,
6000,
9000,
repeating_time,
repeating_time,
1000,
11000,
21000,
4000,
repeating_time,
repeating_time,
8000,
2000,
7000,
repeating_time,
repeating_time,
10000
}.Select((time, index) => new TestReplayFrame(time, true, index)));
replay.HasReceivedAllFrames = true;
// create a new handler with the replay for the sort to be performed.
handler = new TestInputHandler(replay);
// ensure sort stability by checking that the frames with time == repeating_time are sorted in ascending frame index order themselves.
var repeatingTimeFramesData = replay.Frames
.Cast<TestReplayFrame>()
.Where(f => f.Time == repeating_time)
.Select(f => f.FrameIndex);
Assert.That(repeatingTimeFramesData, Is.Ordered.Ascending);
}
private void setReplayFrames() private void setReplayFrames()
{ {
replay.Frames = new List<ReplayFrame> replay.Frames = new List<ReplayFrame>
@ -324,11 +373,13 @@ namespace osu.Game.Tests.NonVisual
private class TestReplayFrame : ReplayFrame private class TestReplayFrame : ReplayFrame
{ {
public readonly bool IsImportant; public readonly bool IsImportant;
public readonly int FrameIndex;
public TestReplayFrame(double time, bool isImportant = false) public TestReplayFrame(double time, bool isImportant = false, int frameIndex = 0)
: base(time) : base(time)
{ {
IsImportant = isImportant; IsImportant = isImportant;
FrameIndex = frameIndex;
} }
} }

View File

@ -0,0 +1,191 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Storyboards;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneStoryboardWithOutro : PlayerTestScene
{
protected override bool HasCustomSteps => true;
protected new OutroPlayer Player => (OutroPlayer)base.Player;
private double currentStoryboardDuration;
private bool showResults = true;
private event Func<HealthProcessor, JudgementResult, bool> currentFailConditions;
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("enable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, true));
AddStep("set dim level to 0", () => LocalConfig.SetValue<double>(OsuSetting.DimLevel, 0));
AddStep("reset fail conditions", () => currentFailConditions = (_, __) => false);
AddStep("set storyboard duration to 2s", () => currentStoryboardDuration = 2000);
AddStep("set ShowResults = true", () => showResults = true);
}
[Test]
public void TestStoryboardSkipOutro()
{
CreateTest(null);
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddStep("skip outro", () => InputManager.Key(osuTK.Input.Key.Space));
AddAssert("score shown", () => Player.IsScoreShown);
}
[Test]
public void TestStoryboardNoSkipOutro()
{
CreateTest(null);
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddUntilStep("wait for score shown", () => Player.IsScoreShown);
}
[Test]
public void TestStoryboardExitToSkipOutro()
{
CreateTest(null);
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddStep("exit via pause", () => Player.ExitViaPause());
AddAssert("score shown", () => Player.IsScoreShown);
}
[TestCase(false)]
[TestCase(true)]
public void TestStoryboardToggle(bool enabledAtBeginning)
{
CreateTest(null);
AddStep($"{(enabledAtBeginning ? "enable" : "disable")} storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, enabledAtBeginning));
AddStep("toggle storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, !enabledAtBeginning));
AddUntilStep("wait for score shown", () => Player.IsScoreShown);
}
[Test]
public void TestOutroEndsDuringFailAnimation()
{
CreateTest(() =>
{
AddStep("fail on first judgement", () => currentFailConditions = (_, __) => true);
AddStep("set storyboard duration to 1.3s", () => currentStoryboardDuration = 1300);
});
AddUntilStep("wait for fail", () => Player.HasFailed);
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddUntilStep("wait for fail overlay", () => Player.FailOverlay.State.Value == Visibility.Visible);
}
[Test]
public void TestShowResultsFalse()
{
CreateTest(() =>
{
AddStep("set ShowResults = false", () => showResults = false);
});
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddWaitStep("wait", 10);
AddAssert("no score shown", () => !Player.IsScoreShown);
}
[Test]
public void TestStoryboardEndsBeforeCompletion()
{
CreateTest(() => AddStep("set storyboard duration to .1s", () => currentStoryboardDuration = 100));
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddUntilStep("wait for score shown", () => Player.IsScoreShown);
}
[Test]
public void TestStoryboardRewind()
{
SkipOverlay.FadeContainer fadeContainer() => Player.ChildrenOfType<SkipOverlay.FadeContainer>().First();
CreateTest(null);
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddUntilStep("skip overlay content becomes visible", () => fadeContainer().State == Visibility.Visible);
AddStep("rewind", () => Player.GameplayClockContainer.Seek(-1000));
AddUntilStep("skip overlay content not visible", () => fadeContainer().State == Visibility.Hidden);
AddUntilStep("skip overlay content becomes visible", () => fadeContainer().State == Visibility.Visible);
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= currentStoryboardDuration);
}
protected override bool AllowFail => true;
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new OutroPlayer(currentFailConditions, showResults);
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new Beatmap();
beatmap.HitObjects.Add(new HitCircle());
return beatmap;
}
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{
return base.CreateWorkingBeatmap(beatmap, createStoryboard(currentStoryboardDuration));
}
private Storyboard createStoryboard(double duration)
{
var storyboard = new Storyboard();
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
sprite.TimelineGroup.Alpha.Add(Easing.None, 0, duration, 1, 0);
storyboard.GetLayer("Background").Add(sprite);
return storyboard;
}
protected class OutroPlayer : TestPlayer
{
public void ExitViaPause() => PerformExit(true);
public new FailOverlay FailOverlay => base.FailOverlay;
public bool IsScoreShown => !this.IsCurrentScreen() && this.GetChildScreen() is ResultsScreen;
private event Func<HealthProcessor, JudgementResult, bool> failConditions;
public OutroPlayer(Func<HealthProcessor, JudgementResult, bool> failConditions, bool showResults = true)
: base(false, showResults)
{
this.failConditions = failConditions;
}
protected override void LoadComplete()
{
base.LoadComplete();
HealthProcessor.FailConditions += failConditions;
}
protected override Task ImportScore(Score score)
{
return Task.CompletedTask;
}
}
}
}

View File

@ -7,6 +7,7 @@ using JetBrains.Annotations;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -112,8 +113,8 @@ namespace osu.Game.Tests.Visual.SongSelect
private void testInfoLabels(int expectedCount) private void testInfoLabels(int expectedCount)
{ {
AddAssert("check info labels exists", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.BufferedWedgeInfo.InfoLabel>().Any()); AddAssert("check info labels exists", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any());
AddAssert("check info labels count", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.BufferedWedgeInfo.InfoLabel>().Count() == expectedCount); AddAssert("check info labels count", () => infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Count() == expectedCount);
} }
[Test] [Test]
@ -124,7 +125,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("check default title", () => infoWedge.Info.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title); AddAssert("check default title", () => infoWedge.Info.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title);
AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist); AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist);
AddAssert("check empty author", () => !infoWedge.Info.MapperContainer.Children.Any()); AddAssert("check empty author", () => !infoWedge.Info.MapperContainer.Children.Any());
AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.BufferedWedgeInfo.InfoLabel>().Any()); AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType<BeatmapInfoWedge.WedgeInfoText.InfoLabel>().Any());
} }
[Test] [Test]
@ -135,15 +136,15 @@ namespace osu.Game.Tests.Visual.SongSelect
private void selectBeatmap([CanBeNull] IBeatmap b) private void selectBeatmap([CanBeNull] IBeatmap b)
{ {
BeatmapInfoWedge.BufferedWedgeInfo infoBefore = null; Container containerBefore = null;
AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () => AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () =>
{ {
infoBefore = infoWedge.Info; containerBefore = infoWedge.DisplayedContent;
infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b); infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : CreateWorkingBeatmap(b);
}); });
AddUntilStep("wait for async load", () => infoWedge.Info != infoBefore); AddUntilStep("wait for async load", () => infoWedge.DisplayedContent != containerBefore);
} }
private IBeatmap createTestBeatmap(RulesetInfo ruleset) private IBeatmap createTestBeatmap(RulesetInfo ruleset)
@ -193,7 +194,9 @@ namespace osu.Game.Tests.Visual.SongSelect
private class TestBeatmapInfoWedge : BeatmapInfoWedge private class TestBeatmapInfoWedge : BeatmapInfoWedge
{ {
public new BufferedWedgeInfo Info => base.Info; public new Container DisplayedContent => base.DisplayedContent;
public new WedgeInfoText Info => base.Info;
} }
private class TestHitObject : ConvertHitObject, IHasPosition private class TestHitObject : ConvertHitObject, IHasPosition

View File

@ -28,7 +28,7 @@ namespace osu.Game.Graphics.Containers
protected override bool BlockNonPositionalInput => true; protected override bool BlockNonPositionalInput => true;
/// <summary> /// <summary>
/// Temporary to allow for overlays in the main screen content to not dim theirselves. /// Temporary to allow for overlays in the main screen content to not dim themselves.
/// Should be eventually replaced by dimming which is aware of the target dim container (traverse parent for certain interface type?). /// Should be eventually replaced by dimming which is aware of the target dim container (traverse parent for certain interface type?).
/// </summary> /// </summary>
protected virtual bool DimMainContent => true; protected virtual bool DimMainContent => true;

View File

@ -141,7 +141,14 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
scalingSettings.ForEach(s => bindPreviewEvent(s.Current)); scalingSettings.ForEach(s => bindPreviewEvent(s.Current));
windowModeDropdown.Current.ValueChanged += _ => updateResolutionDropdown(); windowModeDropdown.Current.BindValueChanged(mode =>
{
updateResolutionDropdown();
const string not_fullscreen_note = "Running without fullscreen mode will increase your input latency!";
windowModeDropdown.WarningText = mode.NewValue != WindowMode.Fullscreen ? not_fullscreen_note : string.Empty;
}, true);
windowModes.BindCollectionChanged((sender, args) => windowModes.BindCollectionChanged((sender, args) =>
{ {

View File

@ -13,6 +13,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
{ {
protected override string Header => "Renderer"; protected override string Header => "Renderer";
private SettingsEnumDropdown<FrameSync> frameLimiterDropdown;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(FrameworkConfigManager config, OsuConfigManager osuConfig) private void load(FrameworkConfigManager config, OsuConfigManager osuConfig)
{ {
@ -20,7 +22,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
Children = new Drawable[] Children = new Drawable[]
{ {
// TODO: this needs to be a custom dropdown at some point // TODO: this needs to be a custom dropdown at some point
new SettingsEnumDropdown<FrameSync> frameLimiterDropdown = new SettingsEnumDropdown<FrameSync>
{ {
LabelText = "Frame limiter", LabelText = "Frame limiter",
Current = config.GetBindable<FrameSync>(FrameworkSetting.FrameSync) Current = config.GetBindable<FrameSync>(FrameworkSetting.FrameSync)
@ -37,5 +39,17 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
}, },
}; };
} }
protected override void LoadComplete()
{
base.LoadComplete();
frameLimiterDropdown.Current.BindValueChanged(limit =>
{
const string unlimited_frames_note = "Using unlimited frame limiter can lead to stutters, bad performance and overheating. It will not improve perceived latency. \"2x refresh rate\" is recommended.";
frameLimiterDropdown.WarningText = limit.NewValue == FrameSync.Unlimited ? unlimited_frames_note : string.Empty;
}, true);
}
} }
} }

View File

@ -2,8 +2,11 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Online.API;
using osu.Game.Users;
namespace osu.Game.Overlays.Settings.Sections.UserInterface namespace osu.Game.Overlays.Settings.Sections.UserInterface
{ {
@ -11,9 +14,15 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
{ {
protected override string Header => "Main Menu"; protected override string Header => "Main Menu";
private IBindable<User> user;
private SettingsEnumDropdown<BackgroundSource> backgroundSourceDropdown;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config, IAPIProvider api)
{ {
user = api.LocalUser.GetBoundCopy();
Children = new Drawable[] Children = new Drawable[]
{ {
new SettingsCheckbox new SettingsCheckbox
@ -31,7 +40,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
LabelText = "Intro sequence", LabelText = "Intro sequence",
Current = config.GetBindable<IntroSequence>(OsuSetting.IntroSequence), Current = config.GetBindable<IntroSequence>(OsuSetting.IntroSequence),
}, },
new SettingsEnumDropdown<BackgroundSource> backgroundSourceDropdown = new SettingsEnumDropdown<BackgroundSource>
{ {
LabelText = "Background source", LabelText = "Background source",
Current = config.GetBindable<BackgroundSource>(OsuSetting.MenuBackgroundSource), Current = config.GetBindable<BackgroundSource>(OsuSetting.MenuBackgroundSource),
@ -43,5 +52,17 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
} }
}; };
} }
protected override void LoadComplete()
{
base.LoadComplete();
backgroundSourceDropdown.Current.BindValueChanged(source =>
{
const string not_supporter_note = "Changes to this setting will only apply with an active osu!supporter tag.";
backgroundSourceDropdown.WarningText = user.Value?.IsSupporter != true ? not_supporter_note : string.Empty;
}, true);
}
} }
} }

View File

@ -18,7 +18,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osuTK; using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays.Settings namespace osu.Game.Overlays.Settings
{ {
@ -36,10 +36,15 @@ namespace osu.Game.Overlays.Settings
private SpriteText labelText; private SpriteText labelText;
private OsuTextFlowContainer warningText;
public bool ShowsDefaultIndicator = true; public bool ShowsDefaultIndicator = true;
public string TooltipText { get; set; } public string TooltipText { get; set; }
[Resolved]
private OsuColour colours { get; set; }
public virtual LocalisableString LabelText public virtual LocalisableString LabelText
{ {
get => labelText?.Text ?? string.Empty; get => labelText?.Text ?? string.Empty;
@ -57,6 +62,31 @@ namespace osu.Game.Overlays.Settings
} }
} }
/// <summary>
/// Text to be displayed at the bottom of this <see cref="SettingsItem{T}"/>.
/// Generally used to recommend the user change their setting as the current one is considered sub-optimal.
/// </summary>
public string WarningText
{
set
{
if (warningText == null)
{
// construct lazily for cases where the label is not needed (may be provided by the Control).
FlowContent.Add(warningText = new OsuTextFlowContainer
{
Colour = colours.Yellow,
Margin = new MarginPadding { Bottom = 5 },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
});
}
warningText.Alpha = string.IsNullOrWhiteSpace(value) ? 0 : 1;
warningText.Text = value;
}
}
public virtual Bindable<T> Current public virtual Bindable<T> Current
{ {
get => controlWithCurrent.Current; get => controlWithCurrent.Current;
@ -92,7 +122,10 @@ namespace osu.Game.Overlays.Settings
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS }, Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS },
Child = Control = CreateControl() Children = new[]
{
Control = CreateControl(),
},
}, },
}; };
@ -141,6 +174,7 @@ namespace osu.Game.Overlays.Settings
{ {
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;
Width = SettingsPanel.CONTENT_MARGINS; Width = SettingsPanel.CONTENT_MARGINS;
Padding = new MarginPadding { Vertical = 1.5f };
Alpha = 0f; Alpha = 0f;
} }
@ -163,7 +197,7 @@ namespace osu.Game.Overlays.Settings
Type = EdgeEffectType.Glow, Type = EdgeEffectType.Glow,
Radius = 2, Radius = 2,
}, },
Size = new Vector2(0.33f, 0.8f), Width = 0.33f,
Child = new Box { RelativeSizeAxes = Axes.Both }, Child = new Box { RelativeSizeAxes = Axes.Both },
}; };
} }
@ -196,12 +230,6 @@ namespace osu.Game.Overlays.Settings
UpdateState(); UpdateState();
} }
public void SetButtonColour(Color4 buttonColour)
{
this.buttonColour = buttonColour;
UpdateState();
}
public void UpdateState() => Scheduler.AddOnce(updateState); public void UpdateState() => Scheduler.AddOnce(updateState);
private void updateState() private void updateState()

View File

@ -5,6 +5,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
using osu.Game.Replays; using osu.Game.Replays;
@ -97,7 +98,7 @@ namespace osu.Game.Rulesets.Replays
{ {
// TODO: This replay frame ordering should be enforced on the Replay type. // TODO: This replay frame ordering should be enforced on the Replay type.
// Currently, the ordering can be broken if the frames are added after this construction. // Currently, the ordering can be broken if the frames are added after this construction.
replay.Frames.Sort((x, y) => x.Time.CompareTo(y.Time)); replay.Frames = replay.Frames.OrderBy(f => f.Time).ToList();
this.replay = replay; this.replay = replay;
currentFrameIndex = -1; currentFrameIndex = -1;

View File

@ -34,13 +34,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
// For non-pooled rulesets, hitobjects are already present in the playfield which allows the blueprints to be loaded in the async context.
if (Composer != null)
{
foreach (var obj in Composer.HitObjects)
AddBlueprintFor(obj.HitObject);
}
selectedHitObjects.BindTo(Beatmap.SelectedHitObjects); selectedHitObjects.BindTo(Beatmap.SelectedHitObjects);
selectedHitObjects.CollectionChanged += (selectedObjects, args) => selectedHitObjects.CollectionChanged += (selectedObjects, args) =>
{ {
@ -69,7 +62,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (Composer != null) if (Composer != null)
{ {
// For pooled rulesets, blueprints must be added for hitobjects already "current" as they would've not been "current" during the async load addition process above.
foreach (var obj in Composer.HitObjects) foreach (var obj in Composer.HitObjects)
AddBlueprintFor(obj.HitObject); AddBlueprintFor(obj.HitObject);

View File

@ -48,7 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
{ {
AllowPause = false, AllowPause = false,
AllowRestart = false, AllowRestart = false,
AllowSkippingIntro = false, AllowSkipping = false,
}) })
{ {
this.userIds = userIds; this.userIds = userIds;

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Storyboards; using osu.Game.Storyboards;
@ -19,6 +20,14 @@ namespace osu.Game.Screens.Play
private readonly Storyboard storyboard; private readonly Storyboard storyboard;
private DrawableStoryboard drawableStoryboard; private DrawableStoryboard drawableStoryboard;
/// <summary>
/// Whether the storyboard is considered finished.
/// </summary>
/// <remarks>
/// This is true by default in here, until an actual drawable storyboard is loaded, in which case it'll bind to it.
/// </remarks>
public IBindable<bool> HasStoryboardEnded = new BindableBool(true);
public DimmableStoryboard(Storyboard storyboard) public DimmableStoryboard(Storyboard storyboard)
{ {
this.storyboard = storyboard; this.storyboard = storyboard;
@ -49,6 +58,7 @@ namespace osu.Game.Screens.Play
return; return;
drawableStoryboard = storyboard.CreateDrawable(); drawableStoryboard = storyboard.CreateDrawable();
HasStoryboardEnded.BindTo(drawableStoryboard.HasStoryboardEnded);
if (async) if (async)
LoadComponentAsync(drawableStoryboard, onStoryboardCreated); LoadComponentAsync(drawableStoryboard, onStoryboardCreated);

View File

@ -104,7 +104,8 @@ namespace osu.Game.Screens.Play
private BreakTracker breakTracker; private BreakTracker breakTracker;
private SkipOverlay skipOverlay; private SkipOverlay skipIntroOverlay;
private SkipOverlay skipOutroOverlay;
protected ScoreProcessor ScoreProcessor { get; private set; } protected ScoreProcessor ScoreProcessor { get; private set; }
@ -244,7 +245,6 @@ namespace osu.Game.Screens.Play
HUDOverlay.ShowHud.Value = false; HUDOverlay.ShowHud.Value = false;
HUDOverlay.ShowHud.Disabled = true; HUDOverlay.ShowHud.Disabled = true;
BreakOverlay.Hide(); BreakOverlay.Hide();
skipOverlay.Hide();
} }
DrawableRuleset.FrameStableClock.WaitingOnFrames.BindValueChanged(waiting => DrawableRuleset.FrameStableClock.WaitingOnFrames.BindValueChanged(waiting =>
@ -281,8 +281,14 @@ namespace osu.Game.Screens.Play
ScoreProcessor.RevertResult(r); ScoreProcessor.RevertResult(r);
}; };
DimmableStoryboard.HasStoryboardEnded.ValueChanged += storyboardEnded =>
{
if (storyboardEnded.NewValue && completionProgressDelegate == null)
updateCompletionState();
};
// Bind the judgement processors to ourselves // Bind the judgement processors to ourselves
ScoreProcessor.HasCompleted.ValueChanged += updateCompletionState; ScoreProcessor.HasCompleted.BindValueChanged(_ => updateCompletionState());
HealthProcessor.Failed += onFail; HealthProcessor.Failed += onFail;
foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>()) foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>())
@ -355,10 +361,15 @@ namespace osu.Game.Screens.Play
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre Origin = Anchor.Centre
}, },
skipOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime) skipIntroOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime)
{ {
RequestSkip = performUserRequestedSkip RequestSkip = performUserRequestedSkip
}, },
skipOutroOverlay = new SkipOverlay(Beatmap.Value.Storyboard.LatestEventTime ?? 0)
{
RequestSkip = () => updateCompletionState(true),
Alpha = 0
},
FailOverlay = new FailOverlay FailOverlay = new FailOverlay
{ {
OnRetry = Restart, OnRetry = Restart,
@ -385,12 +396,15 @@ namespace osu.Game.Screens.Play
} }
}; };
if (!Configuration.AllowSkipping || !DrawableRuleset.AllowGameplayOverlays)
{
skipIntroOverlay.Expire();
skipOutroOverlay.Expire();
}
if (GameplayClockContainer is MasterGameplayClockContainer master) if (GameplayClockContainer is MasterGameplayClockContainer master)
HUDOverlay.PlayerSettingsOverlay.PlaybackSettings.UserPlaybackRate.BindTarget = master.UserPlaybackRate; HUDOverlay.PlayerSettingsOverlay.PlaybackSettings.UserPlaybackRate.BindTarget = master.UserPlaybackRate;
if (!Configuration.AllowSkippingIntro)
skipOverlay.Expire();
if (Configuration.AllowRestart) if (Configuration.AllowRestart)
{ {
container.Add(new HotkeyRetryOverlay container.Add(new HotkeyRetryOverlay
@ -525,6 +539,10 @@ namespace osu.Game.Screens.Play
Pause(); Pause();
return; return;
} }
// if the score is ready for display but results screen has not been pushed yet (e.g. storyboard is still playing beyond gameplay), then transition to results screen instead of exiting.
if (prepareScoreForDisplayTask != null)
updateCompletionState(true);
} }
this.Exit(); this.Exit();
@ -564,17 +582,23 @@ namespace osu.Game.Screens.Play
private ScheduledDelegate completionProgressDelegate; private ScheduledDelegate completionProgressDelegate;
private Task<ScoreInfo> prepareScoreForDisplayTask; private Task<ScoreInfo> prepareScoreForDisplayTask;
private void updateCompletionState(ValueChangedEvent<bool> completionState) /// <summary>
/// Handles changes in player state which may progress the completion of gameplay / this screen's lifetime.
/// </summary>
/// <param name="skipStoryboardOutro">If in a state where a storyboard outro is to be played, offers the choice of skipping beyond it.</param>
/// <exception cref="InvalidOperationException">Thrown if this method is called more than once without changing state.</exception>
private void updateCompletionState(bool skipStoryboardOutro = false)
{ {
// screen may be in the exiting transition phase. // screen may be in the exiting transition phase.
if (!this.IsCurrentScreen()) if (!this.IsCurrentScreen())
return; return;
if (!completionState.NewValue) if (!ScoreProcessor.HasCompleted.Value)
{ {
completionProgressDelegate?.Cancel(); completionProgressDelegate?.Cancel();
completionProgressDelegate = null; completionProgressDelegate = null;
ValidForResume = true; ValidForResume = true;
skipOutroOverlay.Hide();
return; return;
} }
@ -614,6 +638,20 @@ namespace osu.Game.Screens.Play
return score.ScoreInfo; return score.ScoreInfo;
}); });
if (skipStoryboardOutro)
{
scheduleCompletion();
return;
}
bool storyboardHasOutro = DimmableStoryboard.ContentDisplayed && !DimmableStoryboard.HasStoryboardEnded.Value;
if (storyboardHasOutro)
{
skipOutroOverlay.Show();
return;
}
using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY)) using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY))
scheduleCompletion(); scheduleCompletion();
} }

View File

@ -21,8 +21,8 @@ namespace osu.Game.Screens.Play
public bool AllowRestart { get; set; } = true; public bool AllowRestart { get; set; } = true;
/// <summary> /// <summary>
/// Whether the player should be allowed to skip the intro, advancing to the start of gameplay. /// Whether the player should be allowed to skip intros/outros, advancing to the start of gameplay or the end of a storyboard.
/// </summary> /// </summary>
public bool AllowSkippingIntro { get; set; } = true; public bool AllowSkipping { get; set; } = true;
} }
} }

View File

@ -8,19 +8,19 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Input.Bindings;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Input.Bindings;
namespace osu.Game.Screens.Play namespace osu.Game.Screens.Play
{ {
@ -92,6 +92,18 @@ namespace osu.Game.Screens.Play
private double fadeOutBeginTime => startTime - MasterGameplayClockContainer.MINIMUM_SKIP_TIME; private double fadeOutBeginTime => startTime - MasterGameplayClockContainer.MINIMUM_SKIP_TIME;
public override void Hide()
{
base.Hide();
fadeContainer.Hide();
}
public override void Show()
{
base.Show();
fadeContainer.Show();
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
@ -147,7 +159,7 @@ namespace osu.Game.Screens.Play
{ {
} }
private class FadeContainer : Container, IStateful<Visibility> public class FadeContainer : Container, IStateful<Visibility>
{ {
public event Action<Visibility> StateChanged; public event Action<Visibility> StateChanged;
@ -170,7 +182,7 @@ namespace osu.Game.Screens.Play
switch (state) switch (state)
{ {
case Visibility.Visible: case Visibility.Visible:
// we may be triggered to become visible mnultiple times but we only want to transform once. // we may be triggered to become visible multiple times but we only want to transform once.
if (stateChanged) if (stateChanged)
this.FadeIn(500, Easing.OutExpo); this.FadeIn(500, Easing.OutExpo);

View File

@ -11,7 +11,6 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -49,7 +48,9 @@ namespace osu.Game.Screens.Select
private IBindable<StarDifficulty?> beatmapDifficulty; private IBindable<StarDifficulty?> beatmapDifficulty;
protected BufferedWedgeInfo Info; protected Container DisplayedContent { get; private set; }
protected WedgeInfoText Info { get; private set; }
public BeatmapInfoWedge() public BeatmapInfoWedge()
{ {
@ -110,9 +111,9 @@ namespace osu.Game.Screens.Select
} }
} }
public override bool IsPresent => base.IsPresent || Info == null; // Visibility is updated in the LoadComponentAsync callback public override bool IsPresent => base.IsPresent || DisplayedContent == null; // Visibility is updated in the LoadComponentAsync callback
private BufferedWedgeInfo loadingInfo; private Container loadingInfo;
private void updateDisplay() private void updateDisplay()
{ {
@ -124,9 +125,9 @@ namespace osu.Game.Screens.Select
{ {
State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible; State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible;
Info?.FadeOut(250); DisplayedContent?.FadeOut(250);
Info?.Expire(); DisplayedContent?.Expire();
Info = null; DisplayedContent = null;
} }
if (beatmap == null) if (beatmap == null)
@ -135,17 +136,23 @@ namespace osu.Game.Screens.Select
return; return;
} }
LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value, mods.Value, beatmapDifficulty.Value ?? new StarDifficulty()) LoadComponentAsync(loadingInfo = new Container
{ {
RelativeSizeAxes = Axes.Both,
Shear = -Shear, Shear = -Shear,
Depth = Info?.Depth + 1 ?? 0 Depth = DisplayedContent?.Depth + 1 ?? 0,
Children = new Drawable[]
{
new BeatmapInfoWedgeBackground(beatmap),
Info = new WedgeInfoText(beatmap, ruleset.Value, mods.Value, beatmapDifficulty.Value ?? new StarDifficulty()),
}
}, loaded => }, loaded =>
{ {
// ensure we are the most recent loaded wedge. // ensure we are the most recent loaded wedge.
if (loaded != loadingInfo) return; if (loaded != loadingInfo) return;
removeOldInfo(); removeOldInfo();
Add(Info = loaded); Add(DisplayedContent = loaded);
}); });
} }
} }
@ -156,7 +163,7 @@ namespace osu.Game.Screens.Select
cancellationSource?.Cancel(); cancellationSource?.Cancel();
} }
public class BufferedWedgeInfo : BufferedContainer public class WedgeInfoText : Container
{ {
public OsuSpriteText VersionLabel { get; private set; } public OsuSpriteText VersionLabel { get; private set; }
public OsuSpriteText TitleLabel { get; private set; } public OsuSpriteText TitleLabel { get; private set; }
@ -176,8 +183,7 @@ namespace osu.Game.Screens.Select
private ModSettingChangeTracker settingChangeTracker; private ModSettingChangeTracker settingChangeTracker;
public BufferedWedgeInfo(WorkingBeatmap beatmap, RulesetInfo userRuleset, IReadOnlyList<Mod> mods, StarDifficulty difficulty) public WedgeInfoText(WorkingBeatmap beatmap, RulesetInfo userRuleset, IReadOnlyList<Mod> mods, StarDifficulty difficulty)
: base(pixelSnapping: true)
{ {
this.beatmap = beatmap; this.beatmap = beatmap;
ruleset = userRuleset ?? beatmap.BeatmapInfo.Ruleset; ruleset = userRuleset ?? beatmap.BeatmapInfo.Ruleset;
@ -191,7 +197,6 @@ namespace osu.Game.Screens.Select
var beatmapInfo = beatmap.BeatmapInfo; var beatmapInfo = beatmap.BeatmapInfo;
var metadata = beatmapInfo.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); var metadata = beatmapInfo.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata();
CacheDrawnFrameBuffer = true;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
titleBinding = localisation.GetLocalisedString(new RomanisableString(metadata.TitleUnicode, metadata.Title)); titleBinding = localisation.GetLocalisedString(new RomanisableString(metadata.TitleUnicode, metadata.Title));
@ -199,32 +204,6 @@ namespace osu.Game.Screens.Select
Children = new Drawable[] Children = new Drawable[]
{ {
// We will create the white-to-black gradient by modulating transparency and having
// a black backdrop. This results in an sRGB-space gradient and not linear space,
// transitioning from white to black more perceptually uniformly.
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
},
// We use a container, such that we can set the colour gradient to go across the
// vertices of the masked container instead of the vertices of the (larger) sprite.
new Container
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(Color4.White, Color4.White.Opacity(0.3f)),
Children = new[]
{
// Zoomed-in and cropped beatmap background
new BeatmapBackgroundSprite(beatmap)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FillMode = FillMode.Fill,
},
},
},
new DifficultyColourBar(starDifficulty) new DifficultyColourBar(starDifficulty)
{ {
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
@ -340,7 +319,6 @@ namespace osu.Game.Screens.Select
{ {
ArtistLabel.Text = artistBinding.Value; ArtistLabel.Text = artistBinding.Value;
TitleLabel.Text = string.IsNullOrEmpty(source) ? titleBinding.Value : source + " — " + titleBinding.Value; TitleLabel.Text = string.IsNullOrEmpty(source) ? titleBinding.Value : source + " — " + titleBinding.Value;
ForceRedraw();
} }
private void addInfoLabels() private void addInfoLabels()
@ -426,8 +404,6 @@ namespace osu.Game.Screens.Select
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm), CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm),
Content = labelText Content = labelText
}); });
ForceRedraw();
} }
private OsuSpriteText[] getMapper(BeatmapMetadata metadata) private OsuSpriteText[] getMapper(BeatmapMetadata metadata)

View File

@ -0,0 +1,66 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Framework.Graphics.Shapes;
namespace osu.Game.Screens.Select
{
internal class BeatmapInfoWedgeBackground : CompositeDrawable
{
private readonly WorkingBeatmap beatmap;
public BeatmapInfoWedgeBackground(WorkingBeatmap beatmap)
{
this.beatmap = beatmap;
}
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.Both;
InternalChild = new BufferedContainer
{
CacheDrawnFrameBuffer = true,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
// We will create the white-to-black gradient by modulating transparency and having
// a black backdrop. This results in an sRGB-space gradient and not linear space,
// transitioning from white to black more perceptually uniformly.
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
},
// We use a container, such that we can set the colour gradient to go across the
// vertices of the masked container instead of the vertices of the (larger) sprite.
new Container
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(Color4.White, Color4.White.Opacity(0.3f)),
Children = new[]
{
// Zoomed-in and cropped beatmap background
new BeatmapBackgroundSprite(beatmap)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
FillMode = FillMode.Fill,
},
},
},
}
};
}
}
}

View File

@ -5,6 +5,7 @@ using System.Linq;
using System.Threading; using System.Threading;
using osuTK; using osuTK;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
@ -19,6 +20,13 @@ namespace osu.Game.Storyboards.Drawables
[Cached] [Cached]
public Storyboard Storyboard { get; } public Storyboard Storyboard { get; }
/// <summary>
/// Whether the storyboard is considered finished.
/// </summary>
public IBindable<bool> HasStoryboardEnded => hasStoryboardEnded;
private readonly BindableBool hasStoryboardEnded = new BindableBool();
protected override Container<DrawableStoryboardLayer> Content { get; } protected override Container<DrawableStoryboardLayer> Content { get; }
protected override Vector2 DrawScale => new Vector2(Parent.DrawHeight / 480); protected override Vector2 DrawScale => new Vector2(Parent.DrawHeight / 480);
@ -39,6 +47,8 @@ namespace osu.Game.Storyboards.Drawables
public override bool RemoveCompletedTransforms => false; public override bool RemoveCompletedTransforms => false;
private double? lastEventEndTime;
private DependencyContainer dependencies; private DependencyContainer dependencies;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
@ -73,6 +83,14 @@ namespace osu.Game.Storyboards.Drawables
Add(layer.CreateDrawable()); Add(layer.CreateDrawable());
} }
lastEventEndTime = Storyboard.LatestEventTime;
}
protected override void Update()
{
base.Update();
hasStoryboardEnded.Value = lastEventEndTime == null || Time.Current >= lastEventEndTime;
} }
public DrawableStoryboardLayer OverlayLayer => Children.Single(layer => layer.Name == "Overlay"); public DrawableStoryboardLayer OverlayLayer => Children.Single(layer => layer.Name == "Overlay");

View File

@ -14,4 +14,17 @@ namespace osu.Game.Storyboards
Drawable CreateDrawable(); Drawable CreateDrawable();
} }
public static class StoryboardElementExtensions
{
/// <summary>
/// Returns the end time of this storyboard element.
/// </summary>
/// <remarks>
/// This returns the <see cref="IStoryboardElementWithDuration.EndTime"/> where available, falling back to <see cref="IStoryboardElement.StartTime"/> otherwise.
/// </remarks>
/// <param name="element">The storyboard element.</param>
/// <returns>The end time of this element.</returns>
public static double GetEndTime(this IStoryboardElement element) => (element as IStoryboardElementWithDuration)?.EndTime ?? element.StartTime;
}
} }

View File

@ -0,0 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Storyboards
{
/// <summary>
/// A <see cref="IStoryboardElement"/> that ends at a different time than its start time.
/// </summary>
public interface IStoryboardElementWithDuration : IStoryboardElement
{
/// <summary>
/// The time at which the <see cref="IStoryboardElement"/> ends.
/// </summary>
double EndTime { get; }
/// <summary>
/// The duration of the StoryboardElement.
/// </summary>
double Duration => EndTime - StartTime;
}
}

View File

@ -36,6 +36,16 @@ namespace osu.Game.Storyboards
/// </remarks> /// </remarks>
public double? EarliestEventTime => Layers.SelectMany(l => l.Elements).OrderBy(e => e.StartTime).FirstOrDefault()?.StartTime; public double? EarliestEventTime => Layers.SelectMany(l => l.Elements).OrderBy(e => e.StartTime).FirstOrDefault()?.StartTime;
/// <summary>
/// Across all layers, find the latest point in time that a storyboard element ends at.
/// Will return null if there are no elements.
/// </summary>
/// <remarks>
/// This iterates all elements and as such should be used sparingly or stored locally.
/// Videos and samples return StartTime as their EndTIme.
/// </remarks>
public double? LatestEventTime => Layers.SelectMany(l => l.Elements).OrderBy(e => e.GetEndTime()).LastOrDefault()?.GetEndTime();
/// <summary> /// <summary>
/// Depth of the currently front-most storyboard layer, excluding the overlay layer. /// Depth of the currently front-most storyboard layer, excluding the overlay layer.
/// </summary> /// </summary>

View File

@ -11,7 +11,7 @@ using JetBrains.Annotations;
namespace osu.Game.Storyboards namespace osu.Game.Storyboards
{ {
public class StoryboardSprite : IStoryboardElement public class StoryboardSprite : IStoryboardElementWithDuration
{ {
private readonly List<CommandLoop> loops = new List<CommandLoop>(); private readonly List<CommandLoop> loops = new List<CommandLoop>();
private readonly List<CommandTrigger> triggers = new List<CommandTrigger>(); private readonly List<CommandTrigger> triggers = new List<CommandTrigger>();