1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 17:27:24 +08:00

Merge branch 'master' into preset-td-autoplay

This commit is contained in:
Bartłomiej Dach 2024-05-06 09:02:08 +02:00
commit 71776a583c
No known key found for this signature in database
41 changed files with 1187 additions and 609 deletions

View File

@ -30,23 +30,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
});
[Test]
public void TestAddOverlappingControlPoints()
{
createVisualiser(true);
addControlPointStep(new Vector2(200));
addControlPointStep(new Vector2(300));
addControlPointStep(new Vector2(300));
addControlPointStep(new Vector2(500, 300));
AddAssert("last connection displayed", () =>
{
var lastConnection = visualiser.Connections.Last(c => c.ControlPoint.Position == new Vector2(300));
return lastConnection.DrawWidth > 50;
});
}
[Test]
public void TestPerfectCurveTooManyPoints()
{
@ -194,24 +177,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
addAssertPointPositionChanged(points, i);
}
[Test]
public void TestStackingUpdatesConnectionPosition()
{
createVisualiser(true);
Vector2 connectionPosition;
addControlPointStep(connectionPosition = new Vector2(300));
addControlPointStep(new Vector2(600));
// Apply a big number in stacking so the person running the test can clearly see if it fails
AddStep("apply stacking", () => slider.StackHeightBindable.Value += 10);
AddAssert($"Connection at {connectionPosition} changed",
() => visualiser.Connections[0].Position,
() => !Is.EqualTo(connectionPosition)
);
}
private void addAssertPointPositionChanged(Vector2[] points, int index)
{
AddAssert($"Point at {points.ElementAt(index)} changed",

View File

@ -4,10 +4,7 @@
#nullable disable
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Lines;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
@ -15,36 +12,21 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
/// <summary>
/// A visualisation of the line between two <see cref="PathControlPointPiece{T}"/>s.
/// A visualisation of the lines between <see cref="PathControlPointPiece{T}"/>s.
/// </summary>
/// <typeparam name="T">The type of <see cref="OsuHitObject"/> which this <see cref="PathControlPointConnectionPiece{T}"/> visualises.</typeparam>
public partial class PathControlPointConnectionPiece<T> : CompositeDrawable where T : OsuHitObject, IHasPath
/// <typeparam name="T">The type of <see cref="OsuHitObject"/> which this <see cref="PathControlPointConnection{T}"/> visualises.</typeparam>
public partial class PathControlPointConnection<T> : SmoothPath where T : OsuHitObject, IHasPath
{
public readonly PathControlPoint ControlPoint;
private readonly Path path;
private readonly T hitObject;
public int ControlPointIndex { get; set; }
private IBindable<Vector2> hitObjectPosition;
private IBindable<int> pathVersion;
private IBindable<int> stackHeight;
public PathControlPointConnectionPiece(T hitObject, int controlPointIndex)
public PathControlPointConnection(T hitObject)
{
this.hitObject = hitObject;
ControlPointIndex = controlPointIndex;
Origin = Anchor.Centre;
AutoSizeAxes = Axes.Both;
ControlPoint = hitObject.Path.ControlPoints[controlPointIndex];
InternalChild = path = new SmoothPath
{
Anchor = Anchor.Centre,
PathRadius = 1
};
PathRadius = 1;
}
protected override void LoadComplete()
@ -68,18 +50,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
/// </summary>
private void updateConnectingPath()
{
Position = hitObject.StackedPosition + ControlPoint.Position;
Position = hitObject.StackedPosition;
path.ClearVertices();
ClearVertices();
int nextIndex = ControlPointIndex + 1;
if (nextIndex == 0 || nextIndex >= hitObject.Path.ControlPoints.Count)
return;
foreach (var controlPoint in hitObject.Path.ControlPoints)
AddVertex(controlPoint.Position);
path.AddVertex(Vector2.Zero);
path.AddVertex(hitObject.Path.ControlPoints[nextIndex].Position - ControlPoint.Position);
path.OriginPosition = path.PositionInBoundingBox(Vector2.Zero);
OriginPosition = PositionInBoundingBox(Vector2.Zero);
}
}
}

View File

@ -37,7 +37,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield.
internal readonly Container<PathControlPointPiece<T>> Pieces;
internal readonly Container<PathControlPointConnectionPiece<T>> Connections;
private readonly IBindableList<PathControlPoint> controlPoints = new BindableList<PathControlPoint>();
private readonly T hitObject;
@ -63,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
InternalChildren = new Drawable[]
{
Connections = new Container<PathControlPointConnectionPiece<T>> { RelativeSizeAxes = Axes.Both },
new PathControlPointConnection<T>(hitObject),
Pieces = new Container<PathControlPointPiece<T>> { RelativeSizeAxes = Axes.Both }
};
}
@ -78,6 +77,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
controlPoints.BindTo(hitObject.Path.ControlPoints);
}
// Generally all the control points are within the visible area all the time.
public override bool UpdateSubTreeMasking(Drawable source, RectangleF maskingBounds) => true;
/// <summary>
/// Handles correction of invalid path types.
/// </summary>
@ -185,17 +187,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
case NotifyCollectionChangedAction.Add:
Debug.Assert(e.NewItems != null);
// If inserting in the path (not appending),
// update indices of existing connections after insert location
if (e.NewStartingIndex < Pieces.Count)
{
foreach (var connection in Connections)
{
if (connection.ControlPointIndex >= e.NewStartingIndex)
connection.ControlPointIndex += e.NewItems.Count;
}
}
for (int i = 0; i < e.NewItems.Count; i++)
{
var point = (PathControlPoint)e.NewItems[i];
@ -209,8 +200,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
d.DragInProgress = DragInProgress;
d.DragEnded = DragEnded;
}));
Connections.Add(new PathControlPointConnectionPiece<T>(hitObject, e.NewStartingIndex + i));
}
break;
@ -222,19 +211,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
{
foreach (var piece in Pieces.Where(p => p.ControlPoint == point).ToArray())
piece.RemoveAndDisposeImmediately();
foreach (var connection in Connections.Where(c => c.ControlPoint == point).ToArray())
connection.RemoveAndDisposeImmediately();
}
// If removing before the end of the path,
// update indices of connections after remove location
if (e.OldStartingIndex < Pieces.Count)
{
foreach (var connection in Connections)
{
if (connection.ControlPointIndex >= e.OldStartingIndex)
connection.ControlPointIndex -= e.OldItems.Count;
}
}
break;

View File

@ -15,6 +15,7 @@ using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO.Archives;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
@ -23,6 +24,7 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Resources;
using osu.Game.Users;
namespace osu.Game.Tests.Scores.IO
{
@ -284,6 +286,196 @@ namespace osu.Game.Tests.Scores.IO
}
}
[Test]
public void TestUserLookedUpForOnlineScore()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = LoadOsuIntoHost(host, true);
var api = (DummyAPIAccess)osu.API;
api.HandleRequest = req =>
{
switch (req)
{
case GetUserRequest userRequest:
userRequest.TriggerSuccess(new APIUser
{
Username = "Test user",
CountryCode = CountryCode.JP,
Id = 1234
});
return true;
default:
return false;
}
};
var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
var toImport = new ScoreInfo
{
Rank = ScoreRank.B,
TotalScore = 987654,
Accuracy = 0.8,
MaxCombo = 500,
Combo = 250,
User = new APIUser { Username = "Test user" },
Date = DateTimeOffset.Now,
OnlineID = 12345,
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapInfo = beatmap.Beatmaps.First()
};
var imported = LoadScoreIntoOsu(osu, toImport);
Assert.AreEqual(toImport.Rank, imported.Rank);
Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
Assert.AreEqual(toImport.Accuracy, imported.Accuracy);
Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo);
Assert.AreEqual(toImport.User.Username, imported.User.Username);
Assert.AreEqual(toImport.Date, imported.Date);
Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
Assert.AreEqual(toImport.User.Username, imported.RealmUser.Username);
Assert.AreEqual(1234, imported.RealmUser.OnlineID);
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestUserLookedUpForLegacyOnlineScore()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = LoadOsuIntoHost(host, true);
var api = (DummyAPIAccess)osu.API;
api.HandleRequest = req =>
{
switch (req)
{
case GetUserRequest userRequest:
userRequest.TriggerSuccess(new APIUser
{
Username = "Test user",
CountryCode = CountryCode.JP,
Id = 1234
});
return true;
default:
return false;
}
};
var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
var toImport = new ScoreInfo
{
Rank = ScoreRank.B,
TotalScore = 987654,
Accuracy = 0.8,
MaxCombo = 500,
Combo = 250,
User = new APIUser { Username = "Test user" },
Date = DateTimeOffset.Now,
LegacyOnlineID = 12345,
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapInfo = beatmap.Beatmaps.First()
};
var imported = LoadScoreIntoOsu(osu, toImport);
Assert.AreEqual(toImport.Rank, imported.Rank);
Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
Assert.AreEqual(toImport.Accuracy, imported.Accuracy);
Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo);
Assert.AreEqual(toImport.User.Username, imported.User.Username);
Assert.AreEqual(toImport.Date, imported.Date);
Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
Assert.AreEqual(toImport.User.Username, imported.RealmUser.Username);
Assert.AreEqual(1234, imported.RealmUser.OnlineID);
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestUserNotLookedUpForOfflineScore()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = LoadOsuIntoHost(host, true);
var api = (DummyAPIAccess)osu.API;
api.HandleRequest = req =>
{
switch (req)
{
case GetUserRequest userRequest:
userRequest.TriggerSuccess(new APIUser
{
Username = "Test user",
CountryCode = CountryCode.JP,
Id = 1234
});
return true;
default:
return false;
}
};
var beatmap = BeatmapImportHelper.LoadOszIntoOsu(osu, TestResources.GetQuickTestBeatmapForImport()).GetResultSafely();
var toImport = new ScoreInfo
{
Rank = ScoreRank.B,
TotalScore = 987654,
Accuracy = 0.8,
MaxCombo = 500,
Combo = 250,
User = new APIUser { Username = "Test user" },
Date = DateTimeOffset.Now,
OnlineID = -1,
LegacyOnlineID = -1,
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapInfo = beatmap.Beatmaps.First()
};
var imported = LoadScoreIntoOsu(osu, toImport);
Assert.AreEqual(toImport.Rank, imported.Rank);
Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
Assert.AreEqual(toImport.Accuracy, imported.Accuracy);
Assert.AreEqual(toImport.MaxCombo, imported.MaxCombo);
Assert.AreEqual(toImport.User.Username, imported.User.Username);
Assert.AreEqual(toImport.Date, imported.Date);
Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
Assert.AreEqual(toImport.User.Username, imported.RealmUser.Username);
Assert.That(imported.RealmUser.OnlineID, Is.LessThanOrEqualTo(1));
}
finally
{
host.Exit();
}
}
}
public static ScoreInfo LoadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null)
{
// clone to avoid attaching the input score to realm.

View File

@ -85,8 +85,8 @@ namespace osu.Game.Tests.Visual.Gameplay
if (scaleTransformProvided)
{
sprite.TimelineGroup.Scale.Add(Easing.None, Time.Current, Time.Current + 1000, 1, 2);
sprite.TimelineGroup.Scale.Add(Easing.None, Time.Current + 1000, Time.Current + 2000, 2, 1);
sprite.Commands.AddScale(Easing.None, Time.Current, Time.Current + 1000, 1, 2);
sprite.Commands.AddScale(Easing.None, Time.Current + 1000, Time.Current + 2000, 2, 1);
}
layer.Elements.Clear();
@ -211,7 +211,8 @@ namespace osu.Game.Tests.Visual.Gameplay
var layer = storyboard.GetLayer("Background");
var sprite = new StoryboardSprite(lookupName, origin, initialPosition);
sprite.AddLoop(Time.Current, 100).Alpha.Add(Easing.None, 0, 10000, 1, 1);
var loop = sprite.AddLoopingGroup(Time.Current, 100);
loop.AddAlpha(Easing.None, 0, 10000, 1, 1);
layer.Elements.Clear();
layer.Add(sprite);

View File

@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Gameplay
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
sprite.TimelineGroup.Alpha.Add(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1);
sprite.Commands.AddAlpha(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1);
storyboard.GetLayer("Background").Add(sprite);
@ -73,17 +73,17 @@ namespace osu.Game.Tests.Visual.Gameplay
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
// these should be ignored as we have an alpha visibility blocker proceeding this command.
sprite.TimelineGroup.Scale.Add(Easing.None, loop_start_time, -18000, 0, 1);
var loopGroup = sprite.AddLoop(loop_start_time, 50);
loopGroup.Scale.Add(Easing.None, loop_start_time, -18000, 0, 1);
sprite.Commands.AddScale(Easing.None, loop_start_time, -18000, 0, 1);
var loopGroup = sprite.AddLoopingGroup(loop_start_time, 50);
loopGroup.AddScale(Easing.None, loop_start_time, -18000, 0, 1);
var target = addEventToLoop ? loopGroup : sprite.TimelineGroup;
var target = addEventToLoop ? loopGroup : sprite.Commands;
double loopRelativeOffset = addEventToLoop ? -loop_start_time : 0;
target.Alpha.Add(Easing.None, loopRelativeOffset + firstStoryboardEvent, loopRelativeOffset + firstStoryboardEvent + 500, 0, 1);
target.AddAlpha(Easing.None, loopRelativeOffset + firstStoryboardEvent, loopRelativeOffset + firstStoryboardEvent + 500, 0, 1);
// these should be ignored due to being in the future.
sprite.TimelineGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1);
loopGroup.Alpha.Add(Easing.None, 38000, 40000, 0, 1);
sprite.Commands.AddAlpha(Easing.None, 18000, 20000, 0, 1);
loopGroup.AddAlpha(Easing.None, 38000, 40000, 0, 1);
storyboard.GetLayer("Background").Add(sprite);

View File

@ -0,0 +1,264 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
using osu.Game.Storyboards;
using osu.Game.Storyboards.Drawables;
using osu.Game.Tests.Resources;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneStoryboardCommands : OsuTestScene
{
[Cached(typeof(Storyboard))]
private TestStoryboard storyboard { get; set; } = new TestStoryboard
{
UseSkinSprites = false,
AlwaysProvideTexture = true,
};
private readonly ManualClock manualClock = new ManualClock { Rate = 1, IsRunning = true };
private int clockDirection;
private const string lookup_name = "hitcircleoverlay";
private const double clock_limit = 2500;
protected override Container<Drawable> Content => content;
private Container content = null!;
private SpriteText timelineText = null!;
private Box timelineMarker = null!;
[BackgroundDependencyLoader]
private void load()
{
base.Content.Children = new Drawable[]
{
content = new Container
{
RelativeSizeAxes = Axes.Both,
},
timelineText = new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Margin = new MarginPadding { Bottom = 60 },
},
timelineMarker = new Box
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomCentre,
RelativePositionAxes = Axes.X,
Size = new Vector2(2, 50),
},
};
}
[SetUpSteps]
public void SetUpSteps()
{
AddStep("start clock", () => clockDirection = 1);
AddStep("pause clock", () => clockDirection = 0);
AddStep("set clock = 0", () => manualClock.CurrentTime = 0);
}
[Test]
public void TestNormalCommandPlayback()
{
AddStep("create storyboard", () => Child = createStoryboard(s =>
{
s.Commands.AddY(Easing.OutBounce, 500, 900, 100, 240);
s.Commands.AddY(Easing.OutQuint, 1100, 1500, 240, 100);
}));
assert(0, 100);
assert(500, 100);
assert(1000, 240);
assert(1500, 100);
assert(clock_limit, 100);
assert(1500, 100);
assert(1000, 240);
assert(500, 100);
assert(0, 100);
void assert(double time, double y)
{
AddStep($"set clock = {time}", () => manualClock.CurrentTime = time);
AddAssert($"sprite y = {y} at t = {time}", () => this.ChildrenOfType<DrawableStoryboardSprite>().Single().Y == y);
}
}
[Test]
public void TestLoopingCommandsPlayback()
{
AddStep("create storyboard", () => Child = createStoryboard(s =>
{
var loop = s.AddLoopingGroup(250, 1);
loop.AddY(Easing.OutBounce, 0, 400, 100, 240);
loop.AddY(Easing.OutQuint, 600, 1000, 240, 100);
}));
assert(0, 100);
assert(250, 100);
assert(850, 240);
assert(1250, 100);
assert(1850, 240);
assert(2250, 100);
assert(clock_limit, 100);
assert(2250, 100);
assert(1850, 240);
assert(1250, 100);
assert(850, 240);
assert(250, 100);
assert(0, 100);
void assert(double time, double y)
{
AddStep($"set clock = {time}", () => manualClock.CurrentTime = time);
AddAssert($"sprite y = {y} at t = {time}", () => this.ChildrenOfType<DrawableStoryboardSprite>().Single().Y == y);
}
}
[Test]
public void TestLoopManyTimes()
{
AddStep("create storyboard", () => Child = createStoryboard(s =>
{
var loop = s.AddLoopingGroup(500, 10000);
loop.AddY(Easing.OutBounce, 0, 60, 100, 240);
loop.AddY(Easing.OutQuint, 80, 120, 240, 100);
}));
}
[Test]
public void TestParameterTemporaryEffect()
{
AddStep("create storyboard", () => Child = createStoryboard(s =>
{
s.Commands.AddFlipV(Easing.None, 1000, 1500, true, false);
}));
AddAssert("sprite not flipped at t = 0", () => !this.ChildrenOfType<DrawableStoryboardSprite>().Single().FlipV);
AddStep("set clock = 1250", () => manualClock.CurrentTime = 1250);
AddAssert("sprite flipped at t = 1250", () => this.ChildrenOfType<DrawableStoryboardSprite>().Single().FlipV);
AddStep("set clock = 2000", () => manualClock.CurrentTime = 2000);
AddAssert("sprite not flipped at t = 2000", () => !this.ChildrenOfType<DrawableStoryboardSprite>().Single().FlipV);
AddStep("resume clock", () => clockDirection = 1);
}
[Test]
public void TestParameterPermanentEffect()
{
AddStep("create storyboard", () => Child = createStoryboard(s =>
{
s.Commands.AddFlipV(Easing.None, 1000, 1000, true, true);
}));
AddAssert("sprite flipped at t = 0", () => this.ChildrenOfType<DrawableStoryboardSprite>().Single().FlipV);
AddStep("set clock = 1250", () => manualClock.CurrentTime = 1250);
AddAssert("sprite flipped at t = 1250", () => this.ChildrenOfType<DrawableStoryboardSprite>().Single().FlipV);
AddStep("set clock = 2000", () => manualClock.CurrentTime = 2000);
AddAssert("sprite flipped at t = 2000", () => this.ChildrenOfType<DrawableStoryboardSprite>().Single().FlipV);
AddStep("resume clock", () => clockDirection = 1);
}
protected override void Update()
{
base.Update();
if (manualClock.CurrentTime > clock_limit || manualClock.CurrentTime < 0)
clockDirection = -clockDirection;
manualClock.CurrentTime += Time.Elapsed * clockDirection;
timelineText.Text = $"Time: {manualClock.CurrentTime:0}ms";
timelineMarker.X = (float)(manualClock.CurrentTime / clock_limit);
}
private DrawableStoryboard createStoryboard(Action<StoryboardSprite>? addCommands = null)
{
var layer = storyboard.GetLayer("Background");
var sprite = new StoryboardSprite(lookup_name, Anchor.Centre, new Vector2(320, 240));
sprite.Commands.AddScale(Easing.None, 0, clock_limit, 0.5f, 0.5f);
sprite.Commands.AddAlpha(Easing.None, 0, clock_limit, 1, 1);
addCommands?.Invoke(sprite);
layer.Elements.Clear();
layer.Add(sprite);
return storyboard.CreateDrawable().With(c => c.Clock = new FramedClock(manualClock));
}
private partial class TestStoryboard : Storyboard
{
public override DrawableStoryboard CreateDrawable(IReadOnlyList<Mod>? mods = null)
{
return new TestDrawableStoryboard(this, mods);
}
public bool AlwaysProvideTexture { get; set; }
public override string GetStoragePathFromStoryboardPath(string path) => AlwaysProvideTexture ? path : string.Empty;
private partial class TestDrawableStoryboard : DrawableStoryboard
{
private readonly bool alwaysProvideTexture;
public TestDrawableStoryboard(TestStoryboard storyboard, IReadOnlyList<Mod>? mods)
: base(storyboard, mods)
{
alwaysProvideTexture = storyboard.AlwaysProvideTexture;
}
protected override IResourceStore<byte[]> CreateResourceLookupStore() => alwaysProvideTexture
? new AlwaysReturnsTextureStore()
: new ResourceStore<byte[]>();
internal class AlwaysReturnsTextureStore : IResourceStore<byte[]>
{
private const string test_image = "Resources/Textures/test-image.png";
private readonly DllResourceStore store;
public AlwaysReturnsTextureStore()
{
store = TestResources.GetStore();
}
public void Dispose() => store.Dispose();
public byte[] Get(string name) => store.Get(test_image);
public Task<byte[]> GetAsync(string name, CancellationToken cancellationToken = new CancellationToken()) => store.GetAsync(test_image, cancellationToken);
public Stream GetStream(string name) => store.GetStream(test_image);
public IEnumerable<string> GetAvailableResources() => store.GetAvailableResources();
}
}
}
}
}

View File

@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
var storyboard = new Storyboard();
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
sprite.TimelineGroup.Alpha.Add(Easing.None, startTime, 0, 0, 1);
sprite.Commands.AddAlpha(Easing.None, startTime, 0, 0, 1);
storyboard.GetLayer("Background").Add(sprite);
return storyboard;
}

View File

@ -216,7 +216,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{
var storyboard = new Storyboard();
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
sprite.TimelineGroup.Alpha.Add(Easing.None, 0, duration, 1, 0);
sprite.Commands.AddAlpha(Easing.None, 0, duration, 1, 0);
storyboard.GetLayer("Background").Add(sprite);
return storyboard;
}

View File

@ -424,7 +424,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public void TestIntroStoryboardElement() => testLeadIn(b =>
{
var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero);
sprite.TimelineGroup.Alpha.Add(Easing.None, -2000, 0, 0, 1);
sprite.Commands.AddAlpha(Easing.None, -2000, 0, 0, 1);
b.Storyboard.GetLayer("Background").Add(sprite);
});

View File

@ -5,6 +5,7 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
@ -21,8 +22,10 @@ namespace osu.Game.Tests.Visual.UserInterface
private NowPlayingOverlay nowPlayingOverlay;
[BackgroundDependencyLoader]
private void load()
private void load(FrameworkConfigManager frameworkConfig)
{
AddToggleStep("toggle unicode", v => frameworkConfig.SetValue(FrameworkSetting.ShowUnicode, v));
nowPlayingOverlay = new NowPlayingOverlay
{
Origin = Anchor.Centre,
@ -50,9 +53,9 @@ namespace osu.Game.Tests.Visual.UserInterface
Metadata =
{
Artist = "very very very very very very very very very very verry long artist",
ArtistUnicode = "very very very very very very very very very very verry long artist",
ArtistUnicode = "very very very very very very very very very very verry long artist unicode",
Title = "very very very very very verry long title",
TitleUnicode = "very very very very very verry long title",
TitleUnicode = "very very very very very verry long title unicode",
}
}));
@ -61,9 +64,9 @@ namespace osu.Game.Tests.Visual.UserInterface
Metadata =
{
Artist = "very very very very very very very very very very verrry long artist",
ArtistUnicode = "very very very very very very very very very very verrry long artist",
ArtistUnicode = "not very long artist unicode",
Title = "very very very very very verrry long title",
TitleUnicode = "very very very very very verrry long title",
TitleUnicode = "not very long title unicode",
}
}));

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Game.Beatmaps.Legacy;
using osu.Game.IO;
using osu.Game.Storyboards;
using osu.Game.Storyboards.Commands;
using osuTK;
using osuTK.Graphics;
@ -17,7 +18,7 @@ namespace osu.Game.Beatmaps.Formats
public class LegacyStoryboardDecoder : LegacyDecoder<Storyboard>
{
private StoryboardSprite? storyboardSprite;
private CommandTimelineGroup? timelineGroup;
private StoryboardCommandGroup? currentCommandsGroup;
private Storyboard storyboard = null!;
@ -164,7 +165,7 @@ namespace osu.Game.Beatmaps.Formats
else
{
if (depth < 2)
timelineGroup = storyboardSprite?.TimelineGroup;
currentCommandsGroup = storyboardSprite?.Commands;
string commandType = split[0];
@ -176,7 +177,7 @@ namespace osu.Game.Beatmaps.Formats
double startTime = split.Length > 2 ? Parsing.ParseDouble(split[2]) : double.MinValue;
double endTime = split.Length > 3 ? Parsing.ParseDouble(split[3]) : double.MaxValue;
int groupNumber = split.Length > 4 ? Parsing.ParseInt(split[4]) : 0;
timelineGroup = storyboardSprite?.AddTrigger(triggerName, startTime, endTime, groupNumber);
currentCommandsGroup = storyboardSprite?.AddTriggerGroup(triggerName, startTime, endTime, groupNumber);
break;
}
@ -184,7 +185,7 @@ namespace osu.Game.Beatmaps.Formats
{
double startTime = Parsing.ParseDouble(split[1]);
int repeatCount = Parsing.ParseInt(split[2]);
timelineGroup = storyboardSprite?.AddLoop(startTime, Math.Max(0, repeatCount - 1));
currentCommandsGroup = storyboardSprite?.AddLoopingGroup(startTime, Math.Max(0, repeatCount - 1));
break;
}
@ -203,7 +204,7 @@ namespace osu.Game.Beatmaps.Formats
{
float startValue = Parsing.ParseFloat(split[4]);
float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue;
timelineGroup?.Alpha.Add(easing, startTime, endTime, startValue, endValue);
currentCommandsGroup?.AddAlpha(easing, startTime, endTime, startValue, endValue);
break;
}
@ -211,7 +212,7 @@ namespace osu.Game.Beatmaps.Formats
{
float startValue = Parsing.ParseFloat(split[4]);
float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue;
timelineGroup?.Scale.Add(easing, startTime, endTime, startValue, endValue);
currentCommandsGroup?.AddScale(easing, startTime, endTime, startValue, endValue);
break;
}
@ -221,7 +222,7 @@ namespace osu.Game.Beatmaps.Formats
float startY = Parsing.ParseFloat(split[5]);
float endX = split.Length > 6 ? Parsing.ParseFloat(split[6]) : startX;
float endY = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startY;
timelineGroup?.VectorScale.Add(easing, startTime, endTime, new Vector2(startX, startY), new Vector2(endX, endY));
currentCommandsGroup?.AddVectorScale(easing, startTime, endTime, new Vector2(startX, startY), new Vector2(endX, endY));
break;
}
@ -229,7 +230,7 @@ namespace osu.Game.Beatmaps.Formats
{
float startValue = Parsing.ParseFloat(split[4]);
float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue;
timelineGroup?.Rotation.Add(easing, startTime, endTime, float.RadiansToDegrees(startValue), float.RadiansToDegrees(endValue));
currentCommandsGroup?.AddRotation(easing, startTime, endTime, float.RadiansToDegrees(startValue), float.RadiansToDegrees(endValue));
break;
}
@ -239,8 +240,8 @@ namespace osu.Game.Beatmaps.Formats
float startY = Parsing.ParseFloat(split[5]);
float endX = split.Length > 6 ? Parsing.ParseFloat(split[6]) : startX;
float endY = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startY;
timelineGroup?.X.Add(easing, startTime, endTime, startX, endX);
timelineGroup?.Y.Add(easing, startTime, endTime, startY, endY);
currentCommandsGroup?.AddX(easing, startTime, endTime, startX, endX);
currentCommandsGroup?.AddY(easing, startTime, endTime, startY, endY);
break;
}
@ -248,7 +249,7 @@ namespace osu.Game.Beatmaps.Formats
{
float startValue = Parsing.ParseFloat(split[4]);
float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue;
timelineGroup?.X.Add(easing, startTime, endTime, startValue, endValue);
currentCommandsGroup?.AddX(easing, startTime, endTime, startValue, endValue);
break;
}
@ -256,7 +257,7 @@ namespace osu.Game.Beatmaps.Formats
{
float startValue = Parsing.ParseFloat(split[4]);
float endValue = split.Length > 5 ? Parsing.ParseFloat(split[5]) : startValue;
timelineGroup?.Y.Add(easing, startTime, endTime, startValue, endValue);
currentCommandsGroup?.AddY(easing, startTime, endTime, startValue, endValue);
break;
}
@ -268,7 +269,7 @@ namespace osu.Game.Beatmaps.Formats
float endRed = split.Length > 7 ? Parsing.ParseFloat(split[7]) : startRed;
float endGreen = split.Length > 8 ? Parsing.ParseFloat(split[8]) : startGreen;
float endBlue = split.Length > 9 ? Parsing.ParseFloat(split[9]) : startBlue;
timelineGroup?.Colour.Add(easing, startTime, endTime,
currentCommandsGroup?.AddColour(easing, startTime, endTime,
new Color4(startRed / 255f, startGreen / 255f, startBlue / 255f, 1),
new Color4(endRed / 255f, endGreen / 255f, endBlue / 255f, 1));
break;
@ -281,16 +282,16 @@ namespace osu.Game.Beatmaps.Formats
switch (type)
{
case "A":
timelineGroup?.BlendingParameters.Add(easing, startTime, endTime, BlendingParameters.Additive,
currentCommandsGroup?.AddBlendingParameters(easing, startTime, endTime, BlendingParameters.Additive,
startTime == endTime ? BlendingParameters.Additive : BlendingParameters.Inherit);
break;
case "H":
timelineGroup?.FlipH.Add(easing, startTime, endTime, true, startTime == endTime);
currentCommandsGroup?.AddFlipH(easing, startTime, endTime, true, startTime == endTime);
break;
case "V":
timelineGroup?.FlipV.Add(easing, startTime, endTime, true, startTime == endTime);
currentCommandsGroup?.AddFlipV(easing, startTime, endTime, true, startTime == endTime);
break;
}

View File

@ -5,6 +5,7 @@ using System;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
@ -479,6 +480,11 @@ namespace osu.Game.Overlays
private OsuSpriteText mainSpriteText = null!;
private OsuSpriteText fillerSpriteText = null!;
private Bindable<bool> showUnicode = null!;
[Resolved]
private FrameworkConfigManager frameworkConfig { get; set; } = null!;
private LocalisableString text;
public LocalisableString Text
@ -531,6 +537,9 @@ namespace osu.Game.Overlays
{
base.LoadComplete();
showUnicode = frameworkConfig.GetBindable<bool>(FrameworkSetting.ShowUnicode);
showUnicode.BindValueChanged(_ => updateText());
updateFontAndText();
}

View File

@ -127,6 +127,9 @@ namespace osu.Game.Scoring
if (model.RealmUser.OnlineID == APIUser.SYSTEM_USER_ID)
return;
if (model.OnlineID < 0 && model.LegacyOnlineID <= 0)
return;
string username = model.RealmUser.Username;
if (usernameLookupCache.TryGetValue(username, out var existing))

View File

@ -16,6 +16,7 @@ using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@ -138,7 +139,7 @@ namespace osu.Game.Screens.Play
},
automaticDownload = new SettingsCheckbox
{
LabelText = "Automatically download beatmaps",
LabelText = OnlineSettingsStrings.AutomaticallyDownloadMissingBeatmaps,
Current = config.GetBindable<bool>(OsuSetting.AutomaticallyDownloadMissingBeatmaps),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -1,52 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
namespace osu.Game.Storyboards
{
public class CommandLoop : CommandTimelineGroup
{
public double LoopStartTime;
/// <summary>
/// The total number of times this loop is played back. Always greater than zero.
/// </summary>
public readonly int TotalIterations;
public override double StartTime => LoopStartTime + CommandsStartTime;
public override double EndTime =>
// In an ideal world, we would multiply the command duration by TotalIterations here.
// Unfortunately this would clash with how stable handled end times, and results in some storyboards playing outro
// sequences for minutes or hours.
StartTime + CommandsDuration;
/// <summary>
/// Construct a new command loop.
/// </summary>
/// <param name="startTime">The start time of the loop.</param>
/// <param name="repeatCount">The number of times the loop should repeat. Should be greater than zero. Zero means a single playback.</param>
public CommandLoop(double startTime, int repeatCount)
{
if (repeatCount < 0) throw new ArgumentException("Repeat count must be zero or above.", nameof(repeatCount));
LoopStartTime = startTime;
TotalIterations = repeatCount + 1;
}
public override IEnumerable<CommandTimeline<T>.TypedCommand> GetCommands<T>(CommandTimelineSelector<T> timelineSelector, double offset = 0)
{
for (int loop = 0; loop < TotalIterations; loop++)
{
double loopOffset = LoopStartTime + loop * CommandsDuration;
foreach (var command in base.GetCommands(timelineSelector, offset + loopOffset))
yield return command;
}
}
public override string ToString()
=> $"{LoopStartTime} x{TotalIterations}";
}
}

View File

@ -1,89 +0,0 @@
// 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.
#nullable disable
using osu.Framework.Graphics;
using System;
using System.Collections.Generic;
using System.Linq;
namespace osu.Game.Storyboards
{
public class CommandTimeline<T> : ICommandTimeline
{
private readonly List<TypedCommand> commands = new List<TypedCommand>();
public IEnumerable<TypedCommand> Commands => commands.OrderBy(c => c.StartTime);
public bool HasCommands => commands.Count > 0;
public double StartTime { get; private set; } = double.MaxValue;
public double EndTime { get; private set; } = double.MinValue;
public T StartValue { get; private set; }
public T EndValue { get; private set; }
public void Add(Easing easing, double startTime, double endTime, T startValue, T endValue)
{
if (endTime < startTime)
{
endTime = startTime;
}
commands.Add(new TypedCommand { Easing = easing, StartTime = startTime, EndTime = endTime, StartValue = startValue, EndValue = endValue });
if (startTime < StartTime)
{
StartValue = startValue;
StartTime = startTime;
}
if (endTime > EndTime)
{
EndValue = endValue;
EndTime = endTime;
}
}
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)
{
int 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<ICommand>
{
Easing Easing { get; set; }
double StartTime { get; set; }
double EndTime { get; set; }
double Duration { get; }
}
}

View File

@ -1,118 +0,0 @@
// 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 osuTK;
using osuTK.Graphics;
using osu.Framework.Graphics;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
namespace osu.Game.Storyboards
{
public delegate CommandTimeline<T> CommandTimelineSelector<T>(CommandTimelineGroup commandTimelineGroup);
public class CommandTimelineGroup
{
public CommandTimeline<float> X = new CommandTimeline<float>();
public CommandTimeline<float> Y = new CommandTimeline<float>();
public CommandTimeline<float> Scale = new CommandTimeline<float>();
public CommandTimeline<Vector2> VectorScale = new CommandTimeline<Vector2>();
public CommandTimeline<float> Rotation = new CommandTimeline<float>();
public CommandTimeline<Color4> Colour = new CommandTimeline<Color4>();
public CommandTimeline<float> Alpha = new CommandTimeline<float>();
public CommandTimeline<BlendingParameters> BlendingParameters = new CommandTimeline<BlendingParameters>();
public CommandTimeline<bool> FlipH = new CommandTimeline<bool>();
public CommandTimeline<bool> FlipV = new CommandTimeline<bool>();
private readonly ICommandTimeline[] timelines;
public CommandTimelineGroup()
{
timelines = new ICommandTimeline[]
{
X,
Y,
Scale,
VectorScale,
Rotation,
Colour,
Alpha,
BlendingParameters,
FlipH,
FlipV
};
}
[JsonIgnore]
public double CommandsStartTime
{
get
{
double min = double.MaxValue;
for (int i = 0; i < timelines.Length; i++)
min = Math.Min(min, timelines[i].StartTime);
return min;
}
}
[JsonIgnore]
public double CommandsEndTime
{
get
{
double max = double.MinValue;
for (int i = 0; i < timelines.Length; i++)
max = Math.Max(max, timelines[i].EndTime);
return max;
}
}
[JsonIgnore]
public double CommandsDuration => CommandsEndTime - CommandsStartTime;
[JsonIgnore]
public virtual double StartTime => CommandsStartTime;
[JsonIgnore]
public virtual double EndTime => CommandsEndTime;
[JsonIgnore]
public bool HasCommands
{
get
{
for (int i = 0; i < timelines.Length; i++)
{
if (timelines[i].HasCommands)
return true;
}
return false;
}
}
public virtual IEnumerable<CommandTimeline<T>.TypedCommand> GetCommands<T>(CommandTimelineSelector<T> timelineSelector, double offset = 0)
{
if (offset != 0)
{
return timelineSelector(this).Commands.Select(command =>
new CommandTimeline<T>.TypedCommand
{
Easing = command.Easing,
StartTime = offset + command.StartTime,
EndTime = offset + command.EndTime,
StartValue = command.StartValue,
EndValue = command.EndValue,
});
}
return timelineSelector(this).Commands;
}
}
}

View File

@ -0,0 +1,46 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osu.Game.Storyboards.Drawables;
namespace osu.Game.Storyboards.Commands
{
public interface IStoryboardCommand
{
/// <summary>
/// The start time of the storyboard command.
/// </summary>
double StartTime { get; }
/// <summary>
/// The end time of the storyboard command.
/// </summary>
double EndTime { get; }
/// <summary>
/// The name of the <see cref="Drawable"/> property affected by this storyboard command.
/// Used to apply initial property values based on the list of commands given in <see cref="StoryboardSprite"/>.
/// </summary>
string PropertyName { get; }
/// <summary>
/// Sets the value of the corresponding property in <see cref="Drawable"/> to the start value of this command.
/// </summary>
/// <remarks>
/// Parameter commands (e.g. <see cref="StoryboardFlipHCommand"/> / <see cref="StoryboardFlipVCommand"/> / <see cref="StoryboardBlendingParametersCommand"/>) only apply the start value if they have zero duration, i.e. take "permanent" effect regardless of time.
/// </remarks>
/// <param name="d">The target drawable.</param>
void ApplyInitialValue<TDrawable>(TDrawable d)
where TDrawable : Drawable, IFlippable, IVectorScalable;
/// <summary>
/// Applies the transforms described by this storyboard command to the target drawable.
/// </summary>
/// <param name="d">The target drawable.</param>
/// <returns>The sequence of transforms applied to the target drawable.</returns>
TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)
where TDrawable : Drawable, IFlippable, IVectorScalable;
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardAlphaCommand : StoryboardCommand<float>
{
public StoryboardAlphaCommand(Easing easing, double startTime, double endTime, float startValue, float endValue)
: base(easing, startTime, endTime, startValue, endValue)
{
}
public override string PropertyName => nameof(Drawable.Alpha);
public override void ApplyInitialValue<TDrawable>(TDrawable d) => d.Alpha = StartValue;
public override TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)
=> d.FadeTo(StartValue).Then().FadeTo(EndValue, Duration, Easing);
}
}

View File

@ -0,0 +1,28 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardBlendingParametersCommand : StoryboardCommand<BlendingParameters>
{
public StoryboardBlendingParametersCommand(Easing easing, double startTime, double endTime, BlendingParameters startValue, BlendingParameters endValue)
: base(easing, startTime, endTime, startValue, endValue)
{
}
public override string PropertyName => nameof(Drawable.Blending);
public override void ApplyInitialValue<TDrawable>(TDrawable d)
{
if (StartTime == EndTime)
d.Blending = StartValue;
}
public override TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)
=> d.TransformTo(nameof(d.Blending), StartValue).Delay(Duration)
.TransformTo(nameof(d.Blending), EndValue);
}
}

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osuTK.Graphics;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardColourCommand : StoryboardCommand<Color4>
{
public StoryboardColourCommand(Easing easing, double startTime, double endTime, Color4 startValue, Color4 endValue)
: base(easing, startTime, endTime, startValue, endValue)
{
}
public override string PropertyName => nameof(Drawable.Colour);
public override void ApplyInitialValue<TDrawable>(TDrawable d) => d.Colour = StartValue;
public override TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)
=> d.FadeColour(StartValue).Then().FadeColour(EndValue, Duration, Easing);
}
}

View File

@ -0,0 +1,56 @@
// 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 osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osu.Game.Storyboards.Drawables;
namespace osu.Game.Storyboards.Commands
{
public abstract class StoryboardCommand<T> : IStoryboardCommand, IComparable<StoryboardCommand<T>>
{
public double StartTime { get; }
public double EndTime { get; }
public T StartValue { get; }
public T EndValue { get; }
public Easing Easing { get; }
public double Duration => EndTime - StartTime;
protected StoryboardCommand(Easing easing, double startTime, double endTime, T startValue, T endValue)
{
if (endTime < startTime)
endTime = startTime;
StartTime = startTime;
StartValue = startValue;
EndTime = endTime;
EndValue = endValue;
Easing = easing;
}
public abstract string PropertyName { get; }
public abstract void ApplyInitialValue<TDrawable>(TDrawable d)
where TDrawable : Drawable, IFlippable, IVectorScalable;
public abstract TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)
where TDrawable : Drawable, IFlippable, IVectorScalable;
public int CompareTo(StoryboardCommand<T>? other)
{
if (other == null)
return 1;
int result = StartTime.CompareTo(other.StartTime);
if (result != 0)
return result;
return EndTime.CompareTo(other.EndTime);
}
public override string ToString() => $"{StartTime} -> {EndTime}, {StartValue} -> {EndValue} {Easing}";
}
}

View File

@ -0,0 +1,130 @@
// 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.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using osu.Framework.Graphics;
using osu.Framework.Lists;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardCommandGroup
{
private readonly SortedList<StoryboardCommand<float>> x = new SortedList<StoryboardCommand<float>>();
public IReadOnlyList<StoryboardCommand<float>> X => x;
private readonly SortedList<StoryboardCommand<float>> y = new SortedList<StoryboardCommand<float>>();
public IReadOnlyList<StoryboardCommand<float>> Y => y;
private readonly SortedList<StoryboardCommand<float>> scale = new SortedList<StoryboardCommand<float>>();
public IReadOnlyList<StoryboardCommand<float>> Scale => scale;
private readonly SortedList<StoryboardCommand<Vector2>> vectorScale = new SortedList<StoryboardCommand<Vector2>>();
public IReadOnlyList<StoryboardCommand<Vector2>> VectorScale => vectorScale;
private readonly SortedList<StoryboardCommand<float>> rotation = new SortedList<StoryboardCommand<float>>();
public IReadOnlyList<StoryboardCommand<float>> Rotation => rotation;
private readonly SortedList<StoryboardCommand<Color4>> colour = new SortedList<StoryboardCommand<Color4>>();
public IReadOnlyList<StoryboardCommand<Color4>> Colour => colour;
private readonly SortedList<StoryboardCommand<float>> alpha = new SortedList<StoryboardCommand<float>>();
public IReadOnlyList<StoryboardCommand<float>> Alpha => alpha;
private readonly SortedList<StoryboardCommand<BlendingParameters>> blendingParameters = new SortedList<StoryboardCommand<BlendingParameters>>();
public IReadOnlyList<StoryboardCommand<BlendingParameters>> BlendingParameters => blendingParameters;
private readonly SortedList<StoryboardCommand<bool>> flipH = new SortedList<StoryboardCommand<bool>>();
public IReadOnlyList<StoryboardCommand<bool>> FlipH => flipH;
private readonly SortedList<StoryboardCommand<bool>> flipV = new SortedList<StoryboardCommand<bool>>();
public IReadOnlyList<StoryboardCommand<bool>> FlipV => flipV;
/// <summary>
/// Returns the earliest start time of the commands added to this group.
/// </summary>
[JsonIgnore]
public double StartTime { get; private set; } = double.MaxValue;
/// <summary>
/// Returns the latest end time of the commands added to this group.
/// </summary>
[JsonIgnore]
public double EndTime { get; private set; } = double.MinValue;
[JsonIgnore]
public double Duration => EndTime - StartTime;
[JsonIgnore]
public bool HasCommands { get; private set; }
private readonly IReadOnlyList<IStoryboardCommand>[] lists;
public IEnumerable<IStoryboardCommand> AllCommands => lists.SelectMany(g => g);
public StoryboardCommandGroup()
{
lists = new IReadOnlyList<IStoryboardCommand>[] { X, Y, Scale, VectorScale, Rotation, Colour, Alpha, BlendingParameters, FlipH, FlipV };
}
public void AddX(Easing easing, double startTime, double endTime, float startValue, float endValue)
=> AddCommand(x, new StoryboardXCommand(easing, startTime, endTime, startValue, endValue));
public void AddY(Easing easing, double startTime, double endTime, float startValue, float endValue)
=> AddCommand(y, new StoryboardYCommand(easing, startTime, endTime, startValue, endValue));
public void AddScale(Easing easing, double startTime, double endTime, float startValue, float endValue)
=> AddCommand(scale, new StoryboardScaleCommand(easing, startTime, endTime, startValue, endValue));
public void AddVectorScale(Easing easing, double startTime, double endTime, Vector2 startValue, Vector2 endValue)
=> AddCommand(vectorScale, new StoryboardVectorScaleCommand(easing, startTime, endTime, startValue, endValue));
public void AddRotation(Easing easing, double startTime, double endTime, float startValue, float endValue)
=> AddCommand(rotation, new StoryboardRotationCommand(easing, startTime, endTime, startValue, endValue));
public void AddColour(Easing easing, double startTime, double endTime, Color4 startValue, Color4 endValue)
=> AddCommand(colour, new StoryboardColourCommand(easing, startTime, endTime, startValue, endValue));
public void AddAlpha(Easing easing, double startTime, double endTime, float startValue, float endValue)
=> AddCommand(alpha, new StoryboardAlphaCommand(easing, startTime, endTime, startValue, endValue));
public void AddBlendingParameters(Easing easing, double startTime, double endTime, BlendingParameters startValue, BlendingParameters endValue)
=> AddCommand(blendingParameters, new StoryboardBlendingParametersCommand(easing, startTime, endTime, startValue, endValue));
public void AddFlipH(Easing easing, double startTime, double endTime, bool startValue, bool endValue)
=> AddCommand(flipH, new StoryboardFlipHCommand(easing, startTime, endTime, startValue, endValue));
public void AddFlipV(Easing easing, double startTime, double endTime, bool startValue, bool endValue)
=> AddCommand(flipV, new StoryboardFlipVCommand(easing, startTime, endTime, startValue, endValue));
/// <summary>
/// Adds the given storyboard <paramref name="command"/> to the target <paramref name="list"/>.
/// Can be overriden to apply custom effects to the given command before adding it to the list (e.g. looping or time offsets).
/// </summary>
/// <typeparam name="T">The value type of the target property affected by this storyboard command.</typeparam>
protected virtual void AddCommand<T>(ICollection<StoryboardCommand<T>> list, StoryboardCommand<T> command)
{
list.Add(command);
HasCommands = true;
if (command.StartTime < StartTime)
StartTime = command.StartTime;
if (command.EndTime > EndTime)
EndTime = command.EndTime;
}
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osu.Game.Storyboards.Drawables;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardFlipHCommand : StoryboardCommand<bool>
{
public StoryboardFlipHCommand(Easing easing, double startTime, double endTime, bool startValue, bool endValue)
: base(easing, startTime, endTime, startValue, endValue)
{
}
public override string PropertyName => nameof(IFlippable.FlipH);
public override void ApplyInitialValue<TDrawable>(TDrawable d)
{
if (StartTime == EndTime)
d.FlipH = StartValue;
}
public override TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)
=> d.TransformTo(nameof(IFlippable.FlipH), StartValue).Delay(Duration)
.TransformTo(nameof(IFlippable.FlipH), EndValue);
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osu.Game.Storyboards.Drawables;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardFlipVCommand : StoryboardCommand<bool>
{
public StoryboardFlipVCommand(Easing easing, double startTime, double endTime, bool startValue, bool endValue)
: base(easing, startTime, endTime, startValue, endValue)
{
}
public override string PropertyName => nameof(IFlippable.FlipV);
public override void ApplyInitialValue<TDrawable>(TDrawable d)
{
if (StartTime == EndTime)
d.FlipV = StartValue;
}
public override TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)
=> d.TransformTo(nameof(IFlippable.FlipV), StartValue).Delay(Duration)
.TransformTo(nameof(IFlippable.FlipV), EndValue);
}
}

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 System;
using System.Collections.Generic;
using osu.Framework.Graphics.Transforms;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardLoopingGroup : StoryboardCommandGroup
{
private readonly double loopStartTime;
/// <summary>
/// The total number of times this loop is played back. Always greater than zero.
/// </summary>
public readonly int TotalIterations;
/// <summary>
/// Construct a new command loop.
/// </summary>
/// <param name="startTime">The start time of the loop.</param>
/// <param name="repeatCount">The number of times the loop should repeat. Should be greater than zero. Zero means a single playback.</param>
public StoryboardLoopingGroup(double startTime, int repeatCount)
{
ArgumentOutOfRangeException.ThrowIfNegative(repeatCount);
loopStartTime = startTime;
TotalIterations = repeatCount + 1;
}
protected override void AddCommand<T>(ICollection<StoryboardCommand<T>> list, StoryboardCommand<T> command)
=> base.AddCommand(list, new StoryboardLoopingCommand<T>(command, this));
public override string ToString() => $"{loopStartTime} x{TotalIterations}";
private class StoryboardLoopingCommand<T> : StoryboardCommand<T>
{
private readonly StoryboardCommand<T> command;
private readonly StoryboardLoopingGroup loopingGroup;
public StoryboardLoopingCommand(StoryboardCommand<T> command, StoryboardLoopingGroup loopingGroup)
// In an ideal world, we would multiply the command duration by TotalIterations in command end time.
// Unfortunately this would clash with how stable handled end times, and results in some storyboards playing outro
// sequences for minutes or hours.
: base(command.Easing, loopingGroup.loopStartTime + command.StartTime, loopingGroup.loopStartTime + command.EndTime, command.StartValue, command.EndValue)
{
this.command = command;
this.loopingGroup = loopingGroup;
}
public override string PropertyName => command.PropertyName;
public override void ApplyInitialValue<TDrawable>(TDrawable d) => command.ApplyInitialValue(d);
public override TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)
{
if (loopingGroup.TotalIterations == 0)
return command.ApplyTransforms(d);
double loopingGroupDuration = loopingGroup.Duration;
return command.ApplyTransforms(d).Loop(loopingGroupDuration - Duration, loopingGroup.TotalIterations);
}
}
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardRotationCommand : StoryboardCommand<float>
{
public StoryboardRotationCommand(Easing easing, double startTime, double endTime, float startValue, float endValue)
: base(easing, startTime, endTime, startValue, endValue)
{
}
public override string PropertyName => nameof(Drawable.Rotation);
public override void ApplyInitialValue<TDrawable>(TDrawable d) => d.Rotation = StartValue;
public override TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)
=> d.RotateTo(StartValue).Then().RotateTo(EndValue, Duration, Easing);
}
}

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osuTK;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardScaleCommand : StoryboardCommand<float>
{
public StoryboardScaleCommand(Easing easing, double startTime, double endTime, float startValue, float endValue)
: base(easing, startTime, endTime, startValue, endValue)
{
}
public override string PropertyName => nameof(Drawable.Scale);
public override void ApplyInitialValue<TDrawable>(TDrawable d) => d.Scale = new Vector2(StartValue);
public override TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)
=> d.ScaleTo(StartValue).Then().ScaleTo(EndValue, Duration, Easing);
}
}

View File

@ -1,16 +1,16 @@
// 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
namespace osu.Game.Storyboards.Commands
{
public class CommandTrigger : CommandTimelineGroup
public class StoryboardTriggerGroup : StoryboardCommandGroup
{
public string TriggerName;
public double TriggerStartTime;
public double TriggerEndTime;
public int GroupNumber;
public CommandTrigger(string triggerName, double startTime, double endTime, int groupNumber)
public StoryboardTriggerGroup(string triggerName, double startTime, double endTime, int groupNumber)
{
TriggerName = triggerName;
TriggerStartTime = startTime;

View File

@ -0,0 +1,26 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osu.Game.Storyboards.Drawables;
using osuTK;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardVectorScaleCommand : StoryboardCommand<Vector2>
{
public StoryboardVectorScaleCommand(Easing easing, double startTime, double endTime, Vector2 startValue, Vector2 endValue)
: base(easing, startTime, endTime, startValue, endValue)
{
}
public override string PropertyName => nameof(IVectorScalable.VectorScale);
public override void ApplyInitialValue<TDrawable>(TDrawable d) => d.VectorScale = StartValue;
public override TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)
=> d.TransformTo(nameof(d.VectorScale), StartValue).Then()
.TransformTo(nameof(d.VectorScale), EndValue, Duration, Easing);
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardXCommand : StoryboardCommand<float>
{
public StoryboardXCommand(Easing easing, double startTime, double endTime, float startValue, float endValue)
: base(easing, startTime, endTime, startValue, endValue)
{
}
public override string PropertyName => nameof(Drawable.X);
public override void ApplyInitialValue<TDrawable>(TDrawable d) => d.X = StartValue;
public override TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)
=> d.MoveToX(StartValue).Then().MoveToX(EndValue, Duration, Easing);
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
namespace osu.Game.Storyboards.Commands
{
public class StoryboardYCommand : StoryboardCommand<float>
{
public StoryboardYCommand(Easing easing, double startTime, double endTime, float startValue, float endValue)
: base(easing, startTime, endTime, startValue, endValue)
{
}
public override string PropertyName => nameof(Drawable.Y);
public override void ApplyInitialValue<TDrawable>(TDrawable d) => d.Y = StartValue;
public override TransformSequence<TDrawable> ApplyTransforms<TDrawable>(TDrawable d)
=> d.MoveToY(StartValue).Then().MoveToY(EndValue, Duration, Easing);
}
}

View File

@ -1,11 +1,16 @@
// 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.IO;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Textures;
using osu.Framework.Graphics.Video;
using osu.Framework.Utils;
using osuTK;
namespace osu.Game.Storyboards.Drawables
{
@ -13,7 +18,7 @@ namespace osu.Game.Storyboards.Drawables
{
public readonly StoryboardVideo Video;
private Video? drawableVideo;
private DrawableVideo? drawableVideo;
public override bool RemoveWhenNotAlive => false;
@ -25,7 +30,7 @@ namespace osu.Game.Storyboards.Drawables
// This allows scaling based on the video's absolute size.
//
// If not specified we take up the full available space.
bool useRelative = !video.TimelineGroup.Scale.HasCommands;
bool useRelative = !video.Commands.Scale.Any();
RelativeSizeAxes = useRelative ? Axes.Both : Axes.None;
AutoSizeAxes = useRelative ? Axes.None : Axes.Both;
@ -42,7 +47,7 @@ namespace osu.Game.Storyboards.Drawables
if (stream == null)
return;
InternalChild = drawableVideo = new Video(stream, false)
InternalChild = drawableVideo = new DrawableVideo(stream, false)
{
RelativeSizeAxes = RelativeSizeAxes,
FillMode = FillMode.Fill,
@ -70,5 +75,65 @@ namespace osu.Game.Storyboards.Drawables
drawableVideo.FadeOut(500);
}
}
private partial class DrawableVideo : Video, IFlippable, IVectorScalable
{
private bool flipH;
public bool FlipH
{
get => flipH;
set
{
if (flipH == value)
return;
flipH = value;
Invalidate(Invalidation.MiscGeometry);
}
}
private bool flipV;
public bool FlipV
{
get => flipV;
set
{
if (flipV == value)
return;
flipV = value;
Invalidate(Invalidation.MiscGeometry);
}
}
private Vector2 vectorScale = Vector2.One;
public Vector2 VectorScale
{
get => vectorScale;
set
{
if (vectorScale == value)
return;
if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(VectorScale)} must be finite, but is {value}.");
vectorScale = value;
Invalidate(Invalidation.MiscGeometry);
}
}
protected override Vector2 DrawScale
=> new Vector2(FlipH ? -base.DrawScale.X : base.DrawScale.X, FlipV ? -base.DrawScale.Y : base.DrawScale.Y) * VectorScale;
public override Anchor Origin => StoryboardExtensions.AdjustOrigin(base.Origin, VectorScale, FlipH, FlipV);
public DrawableVideo(Stream stream, bool startAtCurrentTime = true)
: base(stream, startAtCurrentTime)
{
}
}
}
}

View File

@ -1,30 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
namespace osu.Game.Storyboards.Drawables
{
public static class DrawablesExtensions
{
/// <summary>
/// Adjusts <see cref="Drawable.Blending"/> after a delay.
/// </summary>
/// <returns>A <see cref="TransformSequence{T}"/> to which further transforms can be added.</returns>
public static TransformSequence<T> TransformBlendingMode<T>(this T drawable, BlendingParameters newValue, double delay = 0)
where T : Drawable
=> drawable.TransformTo(drawable.PopulateTransform(new TransformBlendingParameters(), newValue, delay));
}
public class TransformBlendingParameters : Transform<BlendingParameters, Drawable>
{
private BlendingParameters 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;
}
}

View File

@ -1,55 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
namespace osu.Game.Storyboards.Drawables
{
internal interface IFlippable : ITransformable
public interface IFlippable : IDrawable
{
bool FlipH { get; set; }
bool FlipV { get; set; }
}
internal class TransformFlipH : Transform<bool, IFlippable>
{
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;
}
internal class TransformFlipV : Transform<bool, IFlippable>
{
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;
}
internal static class FlippableExtensions
{
/// <summary>
/// Adjusts <see cref="IFlippable.FlipH"/> after a delay.
/// </summary>
/// <returns>A <see cref="TransformSequence{T}"/> to which further transforms can be added.</returns>
public static TransformSequence<T> TransformFlipH<T>(this T flippable, bool newValue, double delay = 0)
where T : class, IFlippable
=> flippable.TransformTo(flippable.PopulateTransform(new TransformFlipH(), newValue, delay));
/// <summary>
/// Adjusts <see cref="IFlippable.FlipV"/> after a delay.
/// </summary>
/// <returns>A <see cref="TransformSequence{T}"/> to which further transforms can be added.</returns>
public static TransformSequence<T> TransformFlipV<T>(this T flippable, bool newValue, double delay = 0)
where T : class, IFlippable
=> flippable.TransformTo(flippable.PopulateTransform(new TransformFlipV(), newValue, delay));
}
}

View File

@ -1,21 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Transforms;
using osuTK;
namespace osu.Game.Storyboards.Drawables
{
internal interface IVectorScalable : ITransformable
public interface IVectorScalable : IDrawable
{
Vector2 VectorScale { get; set; }
}
internal static class VectorScalableExtensions
{
public static TransformSequence<T> VectorScaleTo<T>(this T target, Vector2 newVectorScale, double duration = 0, Easing easing = Easing.None)
where T : class, IVectorScalable
=> target.TransformTo(nameof(IVectorScalable.VectorScale), newVectorScale, duration, easing);
}
}

View File

@ -21,8 +21,7 @@ namespace osu.Game.Storyboards
LoopType = loopType;
}
public override Drawable CreateDrawable()
=> new DrawableStoryboardAnimation(this);
public override Drawable CreateDrawable() => new DrawableStoryboardAnimation(this);
}
public enum AnimationLoopType

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Storyboards.Commands;
using osu.Game.Storyboards.Drawables;
using osuTK;
@ -12,8 +13,8 @@ namespace osu.Game.Storyboards
{
public class StoryboardSprite : IStoryboardElementWithDuration
{
private readonly List<CommandLoop> loops = new List<CommandLoop>();
private readonly List<CommandTrigger> triggers = new List<CommandTrigger>();
private readonly List<StoryboardLoopingGroup> loopingGroups = new List<StoryboardLoopingGroup>();
private readonly List<StoryboardTriggerGroup> triggerGroups = new List<StoryboardTriggerGroup>();
public string Path { get; }
public bool IsDrawable => HasCommands;
@ -21,7 +22,7 @@ namespace osu.Game.Storyboards
public Anchor Origin;
public Vector2 InitialPosition;
public readonly CommandTimelineGroup TimelineGroup = new CommandTimelineGroup();
public readonly StoryboardCommandGroup Commands = new StoryboardCommandGroup();
public double StartTime
{
@ -34,13 +35,13 @@ namespace osu.Game.Storyboards
// anything before that point can be ignored (the sprite is not visible after all).
var alphaCommands = new List<(double startTime, bool isZeroStartValue)>();
var command = TimelineGroup.Alpha.Commands.FirstOrDefault();
var command = Commands.Alpha.FirstOrDefault();
if (command != null) alphaCommands.Add((command.StartTime, command.StartValue == 0));
foreach (var loop in loops)
foreach (var loop in loopingGroups)
{
command = loop.Alpha.Commands.FirstOrDefault();
if (command != null) alphaCommands.Add((command.StartTime + loop.LoopStartTime, command.StartValue == 0));
command = loop.Alpha.FirstOrDefault();
if (command != null) alphaCommands.Add((command.StartTime, command.StartValue == 0));
}
if (alphaCommands.Count > 0)
@ -61,8 +62,8 @@ namespace osu.Game.Storyboards
{
// If we got to this point, either no alpha commands were present, or the earliest had a non-zero start value.
// The sprite's StartTime will be determined by the earliest command, regardless of type.
double earliestStartTime = TimelineGroup.StartTime;
foreach (var l in loops)
double earliestStartTime = Commands.StartTime;
foreach (var l in loopingGroups)
earliestStartTime = Math.Min(earliestStartTime, l.StartTime);
return earliestStartTime;
}
@ -72,9 +73,9 @@ namespace osu.Game.Storyboards
{
get
{
double latestEndTime = TimelineGroup.EndTime;
double latestEndTime = Commands.EndTime;
foreach (var l in loops)
foreach (var l in loopingGroups)
latestEndTime = Math.Max(latestEndTime, l.EndTime);
return latestEndTime;
@ -85,20 +86,16 @@ namespace osu.Game.Storyboards
{
get
{
double latestEndTime = TimelineGroup.EndTime;
double latestEndTime = Commands.EndTime;
foreach (var l in loops)
latestEndTime = Math.Max(latestEndTime, l.StartTime + l.CommandsDuration * l.TotalIterations);
foreach (var l in loopingGroups)
latestEndTime = Math.Max(latestEndTime, l.StartTime + l.Duration * l.TotalIterations);
return latestEndTime;
}
}
public bool HasCommands => TimelineGroup.HasCommands || loops.Any(l => l.HasCommands);
private delegate void DrawablePropertyInitializer<in T>(Drawable drawable, T value);
private delegate void DrawableTransformer<in T>(Drawable drawable, T value, double duration, Easing easing);
public bool HasCommands => Commands.HasCommands || loopingGroups.Any(l => l.HasCommands);
public StoryboardSprite(string path, Anchor origin, Vector2 initialPosition)
{
@ -107,127 +104,39 @@ namespace osu.Game.Storyboards
InitialPosition = initialPosition;
}
public CommandLoop AddLoop(double startTime, int repeatCount)
public virtual Drawable CreateDrawable() => new DrawableStoryboardSprite(this);
public StoryboardLoopingGroup AddLoopingGroup(double loopStartTime, int repeatCount)
{
var loop = new CommandLoop(startTime, repeatCount);
loops.Add(loop);
var loop = new StoryboardLoopingGroup(loopStartTime, repeatCount);
loopingGroups.Add(loop);
return loop;
}
public CommandTrigger AddTrigger(string triggerName, double startTime, double endTime, int groupNumber)
public StoryboardTriggerGroup AddTriggerGroup(string triggerName, double startTime, double endTime, int groupNumber)
{
var trigger = new CommandTrigger(triggerName, startTime, endTime, groupNumber);
triggers.Add(trigger);
var trigger = new StoryboardTriggerGroup(triggerName, startTime, endTime, groupNumber);
triggerGroups.Add(trigger);
return trigger;
}
public virtual Drawable CreateDrawable()
=> new DrawableStoryboardSprite(this);
public void ApplyTransforms(Drawable drawable, IEnumerable<Tuple<CommandTimelineGroup, double>>? triggeredGroups = null)
public void ApplyTransforms<TDrawable>(TDrawable drawable)
where TDrawable : Drawable, IFlippable, IVectorScalable
{
// For performance reasons, we need to apply the commands in order by start time. Not doing so will cause many functions to be interleaved, resulting in O(n^2) complexity.
// To achieve this, commands are "generated" as pairs of (command, initFunc, transformFunc) and batched into a contiguous list
// The list is then stably-sorted (to preserve command order), and applied to the drawable sequentially.
HashSet<string> appliedProperties = new HashSet<string>();
List<IGeneratedCommand> generated = new List<IGeneratedCommand>();
// For performance reasons, we need to apply the commands in chronological order.
// Not doing so will cause many functions to be interleaved, resulting in O(n^2) complexity.
IEnumerable<IStoryboardCommand> commands = Commands.AllCommands;
commands = commands.Concat(loopingGroups.SelectMany(l => l.AllCommands));
generateCommands(generated, getCommands(g => g.X, triggeredGroups), (d, value) => d.X = value, (d, value, duration, easing) => d.MoveToX(value, duration, easing));
generateCommands(generated, getCommands(g => g.Y, triggeredGroups), (d, value) => d.Y = value, (d, value, duration, easing) => d.MoveToY(value, duration, easing));
generateCommands(generated, getCommands(g => g.Scale, triggeredGroups), (d, value) => d.Scale = new Vector2(value), (d, value, duration, easing) => d.ScaleTo(value, duration, easing));
generateCommands(generated, getCommands(g => g.Rotation, triggeredGroups), (d, value) => d.Rotation = value, (d, value, duration, easing) => d.RotateTo(value, duration, easing));
generateCommands(generated, getCommands(g => g.Colour, triggeredGroups), (d, value) => d.Colour = value, (d, value, duration, easing) => d.FadeColour(value, duration, easing));
generateCommands(generated, getCommands(g => g.Alpha, triggeredGroups), (d, value) => d.Alpha = value, (d, value, duration, easing) => d.FadeTo(value, duration, easing));
generateCommands(generated, getCommands(g => g.BlendingParameters, triggeredGroups), (d, value) => d.Blending = value, (d, value, duration, _) => d.TransformBlendingMode(value, duration),
false);
if (drawable is IVectorScalable vectorScalable)
foreach (var command in commands.OrderBy(c => c.StartTime))
{
generateCommands(generated, getCommands(g => g.VectorScale, triggeredGroups), (_, value) => vectorScalable.VectorScale = value,
(_, value, duration, easing) => vectorScalable.VectorScaleTo(value, duration, easing));
}
if (drawable is IFlippable flippable)
{
generateCommands(generated, getCommands(g => g.FlipH, triggeredGroups), (_, value) => flippable.FlipH = value, (_, value, duration, _) => flippable.TransformFlipH(value, duration),
false);
generateCommands(generated, getCommands(g => g.FlipV, triggeredGroups), (_, value) => flippable.FlipV = value, (_, value, duration, _) => flippable.TransformFlipV(value, duration),
false);
}
foreach (var command in generated.OrderBy(g => g.StartTime))
command.ApplyTo(drawable);
}
private void generateCommands<T>(List<IGeneratedCommand> resultList, IEnumerable<CommandTimeline<T>.TypedCommand> commands,
DrawablePropertyInitializer<T> initializeProperty, DrawableTransformer<T> transform, bool alwaysInitialize = true)
{
bool initialized = false;
foreach (var command in commands)
{
DrawablePropertyInitializer<T>? initFunc = null;
if (!initialized)
{
if (alwaysInitialize || command.StartTime == command.EndTime)
initFunc = initializeProperty;
initialized = true;
}
resultList.Add(new GeneratedCommand<T>(command, initFunc, transform));
}
}
private IEnumerable<CommandTimeline<T>.TypedCommand> getCommands<T>(CommandTimelineSelector<T> timelineSelector, IEnumerable<Tuple<CommandTimelineGroup, double>>? 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}";
private interface IGeneratedCommand
{
double StartTime { get; }
void ApplyTo(Drawable drawable);
}
private readonly struct GeneratedCommand<T> : IGeneratedCommand
{
public double StartTime => command.StartTime;
private readonly DrawablePropertyInitializer<T>? initializeProperty;
private readonly DrawableTransformer<T> transform;
private readonly CommandTimeline<T>.TypedCommand command;
public GeneratedCommand(CommandTimeline<T>.TypedCommand command, DrawablePropertyInitializer<T>? initializeProperty, DrawableTransformer<T> transform)
{
this.command = command;
this.initializeProperty = initializeProperty;
this.transform = transform;
}
public void ApplyTo(Drawable drawable)
{
initializeProperty?.Invoke(drawable, command.StartValue);
if (appliedProperties.Add(command.PropertyName))
command.ApplyInitialValue(drawable);
using (drawable.BeginAbsoluteSequence(command.StartTime))
{
transform(drawable, command.StartValue, 0, Easing.None);
transform(drawable, command.EndValue, command.Duration, command.Easing);
}
command.ApplyTransforms(drawable);
}
}
}

View File

@ -14,7 +14,7 @@ namespace osu.Game.Storyboards
{
// This is just required to get a valid StartTime based on the incoming offset.
// Actual fades are handled inside DrawableStoryboardVideo for now.
TimelineGroup.Alpha.Add(Easing.None, offset, offset, 0, 0);
Commands.AddAlpha(Easing.None, offset, offset, 0, 0);
}
public override Drawable CreateDrawable() => new DrawableStoryboardVideo(this);

View File

@ -347,6 +347,7 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GL/@EntryIndexedValue">GL</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=GLSL/@EntryIndexedValue">GLSL</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HID/@EntryIndexedValue">HID</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HP/@EntryIndexedValue">HP</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HSL/@EntryIndexedValue">HSL</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HSPA/@EntryIndexedValue">HSPA</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=HSV/@EntryIndexedValue">HSV</s:String>