mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 09:27:29 +08:00
Merge remote-tracking branch 'upstream/master' into sample-control-points
This commit is contained in:
commit
608fa3cc73
@ -418,10 +418,13 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
private void clearPlate(DroppedObjectAnimation animation)
|
||||
{
|
||||
var droppedObjects = caughtObjectContainer.Children.Select(getDroppedObject).ToArray();
|
||||
var caughtObjects = caughtObjectContainer.Children.ToArray();
|
||||
|
||||
caughtObjectContainer.Clear(false);
|
||||
|
||||
// Use the already returned PoolableDrawables for new objects
|
||||
var droppedObjects = caughtObjects.Select(getDroppedObject).ToArray();
|
||||
|
||||
droppedObjectTarget.AddRange(droppedObjects);
|
||||
|
||||
foreach (var droppedObject in droppedObjects)
|
||||
@ -430,10 +433,10 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
private void removeFromPlate(CaughtObject caughtObject, DroppedObjectAnimation animation)
|
||||
{
|
||||
var droppedObject = getDroppedObject(caughtObject);
|
||||
|
||||
caughtObjectContainer.Remove(caughtObject, false);
|
||||
|
||||
var droppedObject = getDroppedObject(caughtObject);
|
||||
|
||||
droppedObjectTarget.Add(droppedObject);
|
||||
|
||||
applyDropAnimation(droppedObject, animation);
|
||||
@ -456,6 +459,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
break;
|
||||
}
|
||||
|
||||
// Define lifetime start for dropped objects to be disposed correctly when rewinding replay
|
||||
d.LifetimeStart = Clock.CurrentTime;
|
||||
d.Expire();
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
|
||||
{
|
||||
var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
|
||||
drawableTaikoRuleset.LockPlayfieldMaxAspect.Value = false;
|
||||
drawableTaikoRuleset.LockPlayfieldAspectRange.Value = false;
|
||||
|
||||
var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
|
||||
playfield.ClassicHitTargetPosition.Value = true;
|
||||
|
@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
{
|
||||
public new BindableDouble TimeRange => base.TimeRange;
|
||||
|
||||
public readonly BindableBool LockPlayfieldMaxAspect = new BindableBool(true);
|
||||
public readonly BindableBool LockPlayfieldAspectRange = new BindableBool(true);
|
||||
|
||||
public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager;
|
||||
|
||||
@ -69,7 +69,9 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
const float scroll_rate = 10;
|
||||
|
||||
// Since the time range will depend on a positional value, it is referenced to the x480 pixel space.
|
||||
float ratio = DrawHeight / 480;
|
||||
// Width is used because it defines how many notes fit on the playfield.
|
||||
// We clamp the ratio to the maximum aspect ratio to keep scroll speed consistent on widths lower than the default.
|
||||
float ratio = Math.Max(DrawSize.X / 768f, TaikoPlayfieldAdjustmentContainer.MAXIMUM_ASPECT);
|
||||
|
||||
TimeRange.Value = (Playfield.HitObjectContainer.DrawWidth / ratio) * scroll_rate;
|
||||
}
|
||||
@ -92,7 +94,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer
|
||||
{
|
||||
LockPlayfieldMaxAspect = { BindTarget = LockPlayfieldMaxAspect }
|
||||
LockPlayfieldAspectRange = { BindTarget = LockPlayfieldAspectRange }
|
||||
};
|
||||
|
||||
protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);
|
||||
|
@ -11,9 +11,11 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
public partial class TaikoPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
|
||||
{
|
||||
private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
|
||||
private const float default_aspect = 16f / 9f;
|
||||
|
||||
public readonly IBindable<bool> LockPlayfieldMaxAspect = new BindableBool(true);
|
||||
public const float MAXIMUM_ASPECT = 16f / 9f;
|
||||
public const float MINIMUM_ASPECT = 5f / 4f;
|
||||
|
||||
public readonly IBindable<bool> LockPlayfieldAspectRange = new BindableBool(true);
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
@ -26,12 +28,22 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
//
|
||||
// As a middle-ground, the aspect ratio can still be adjusted in the downwards direction but has a maximum limit.
|
||||
// This is still a bit weird, because readability changes with window size, but it is what it is.
|
||||
if (LockPlayfieldMaxAspect.Value && Parent.ChildSize.X / Parent.ChildSize.Y > default_aspect)
|
||||
height *= Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
|
||||
if (LockPlayfieldAspectRange.Value)
|
||||
{
|
||||
float currentAspect = Parent.ChildSize.X / Parent.ChildSize.Y;
|
||||
|
||||
if (currentAspect > MAXIMUM_ASPECT)
|
||||
height *= currentAspect / MAXIMUM_ASPECT;
|
||||
else if (currentAspect < MINIMUM_ASPECT)
|
||||
height *= currentAspect / MINIMUM_ASPECT;
|
||||
}
|
||||
|
||||
// Limit the maximum relative height of the playfield to one-third of available area to avoid it masking out on extreme resolutions.
|
||||
height = Math.Min(height, 1f / 3f);
|
||||
Height = height;
|
||||
|
||||
// Position the taiko playfield exactly one playfield from the top of the screen.
|
||||
// Position the taiko playfield exactly one playfield from the top of the screen, if there is enough space for it.
|
||||
// Note that the relative height cannot exceed one-third - if that limit is hit, the playfield will be exactly centered.
|
||||
RelativePositionAxes = Axes.Y;
|
||||
Y = height;
|
||||
}
|
||||
|
@ -160,6 +160,36 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDecodeVideoWithLowercaseExtension()
|
||||
{
|
||||
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
|
||||
|
||||
using (var resStream = TestResources.OpenResource("video-with-lowercase-extension.osb"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var beatmap = decoder.Decode(stream);
|
||||
var metadata = beatmap.Metadata;
|
||||
|
||||
Assert.AreEqual("BG.jpg", metadata.BackgroundFile);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDecodeVideoWithUppercaseExtension()
|
||||
{
|
||||
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
|
||||
|
||||
using (var resStream = TestResources.OpenResource("video-with-uppercase-extension.osb"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var beatmap = decoder.Decode(stream);
|
||||
var metadata = beatmap.Metadata;
|
||||
|
||||
Assert.AreEqual("BG.jpg", metadata.BackgroundFile);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDecodeImageSpecifiedAsVideo()
|
||||
{
|
||||
|
@ -95,6 +95,27 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLoopWithoutExplicitFadeOut()
|
||||
{
|
||||
var decoder = new LegacyStoryboardDecoder();
|
||||
|
||||
using (var resStream = TestResources.OpenResource("animation-loop-no-explicit-end-time.osb"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var storyboard = decoder.Decode(stream);
|
||||
|
||||
StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3);
|
||||
Assert.AreEqual(1, background.Elements.Count);
|
||||
|
||||
Assert.AreEqual(2000, background.Elements[0].StartTime);
|
||||
Assert.AreEqual(2000, (background.Elements[0] as StoryboardAnimation)?.EarliestTransformTime);
|
||||
|
||||
Assert.AreEqual(3000, (background.Elements[0] as StoryboardAnimation)?.GetEndTime());
|
||||
Assert.AreEqual(12000, (background.Elements[0] as StoryboardAnimation)?.EndTimeForDisplay);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCorrectAnimationStartTime()
|
||||
{
|
||||
@ -169,6 +190,40 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDecodeVideoWithLowercaseExtension()
|
||||
{
|
||||
var decoder = new LegacyStoryboardDecoder();
|
||||
|
||||
using (var resStream = TestResources.OpenResource("video-with-lowercase-extension.osb"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var storyboard = decoder.Decode(stream);
|
||||
|
||||
StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video");
|
||||
Assert.That(video.Elements.Count, Is.EqualTo(1));
|
||||
|
||||
Assert.AreEqual("Video.avi", ((StoryboardVideo)video.Elements[0]).Path);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDecodeVideoWithUppercaseExtension()
|
||||
{
|
||||
var decoder = new LegacyStoryboardDecoder();
|
||||
|
||||
using (var resStream = TestResources.OpenResource("video-with-uppercase-extension.osb"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var storyboard = decoder.Decode(stream);
|
||||
|
||||
StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video");
|
||||
Assert.That(video.Elements.Count, Is.EqualTo(1));
|
||||
|
||||
Assert.AreEqual("Video.AVI", ((StoryboardVideo)video.Elements[0]).Path);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDecodeImageSpecifiedAsVideo()
|
||||
{
|
||||
@ -179,8 +234,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
{
|
||||
var storyboard = decoder.Decode(stream);
|
||||
|
||||
StoryboardLayer foreground = storyboard.Layers.Single(l => l.Name == "Video");
|
||||
Assert.That(foreground.Elements.Count, Is.Zero);
|
||||
StoryboardLayer video = storyboard.Layers.Single(l => l.Name == "Video");
|
||||
Assert.That(video.Elements.Count, Is.Zero);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,6 @@
|
||||
[Events]
|
||||
//Storyboard Layer 0 (Background)
|
||||
Animation,Background,Centre,"img.jpg",320,240,2,150,LoopForever
|
||||
F,0,2000,,0,1
|
||||
L,2000,10
|
||||
F,18,0,1000,1,0
|
@ -0,0 +1,5 @@
|
||||
osu file format v14
|
||||
|
||||
[Events]
|
||||
0,0,"BG.jpg",0,0
|
||||
Video,0,"Video.avi",0,0
|
@ -0,0 +1,5 @@
|
||||
osu file format v14
|
||||
|
||||
[Events]
|
||||
0,0,"BG.jpg",0,0
|
||||
Video,0,"Video.AVI",0,0
|
@ -311,6 +311,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
public bool IsDrawable => true;
|
||||
public double StartTime => double.MinValue;
|
||||
public double EndTime => double.MaxValue;
|
||||
public double EndTimeForDisplay => double.MaxValue;
|
||||
|
||||
public Drawable CreateDrawable() => new DrawableTestStoryboardElement();
|
||||
}
|
||||
|
@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
// best way to check without exposing.
|
||||
private Drawable hideTarget => hudOverlay.KeyCounter;
|
||||
private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<DefaultKeyCounter>>().Single();
|
||||
private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().Single();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
|
@ -7,7 +7,9 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
@ -17,13 +19,21 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public TestSceneKeyCounter()
|
||||
{
|
||||
KeyCounterDisplay kc = new DefaultKeyCounterDisplay
|
||||
KeyCounterDisplay defaultDisplay = new DefaultKeyCounterDisplay
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Position = new Vector2(0, 72.7f)
|
||||
};
|
||||
|
||||
kc.AddRange(new InputTrigger[]
|
||||
KeyCounterDisplay argonDisplay = new ArgonKeyCounterDisplay
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Position = new Vector2(0, -72.7f)
|
||||
};
|
||||
|
||||
defaultDisplay.AddRange(new InputTrigger[]
|
||||
{
|
||||
new KeyCounterKeyboardTrigger(Key.X),
|
||||
new KeyCounterKeyboardTrigger(Key.X),
|
||||
@ -31,30 +41,41 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
new KeyCounterMouseTrigger(MouseButton.Right),
|
||||
});
|
||||
|
||||
var testCounter = (DefaultKeyCounter)kc.Counters.First();
|
||||
argonDisplay.AddRange(new InputTrigger[]
|
||||
{
|
||||
new KeyCounterKeyboardTrigger(Key.X),
|
||||
new KeyCounterKeyboardTrigger(Key.X),
|
||||
new KeyCounterMouseTrigger(MouseButton.Left),
|
||||
new KeyCounterMouseTrigger(MouseButton.Right),
|
||||
});
|
||||
|
||||
var testCounter = (DefaultKeyCounter)defaultDisplay.Counters.First();
|
||||
|
||||
AddStep("Add random", () =>
|
||||
{
|
||||
Key key = (Key)((int)Key.A + RNG.Next(26));
|
||||
kc.Add(new KeyCounterKeyboardTrigger(key));
|
||||
defaultDisplay.Add(new KeyCounterKeyboardTrigger(key));
|
||||
argonDisplay.Add(new KeyCounterKeyboardTrigger(key));
|
||||
});
|
||||
|
||||
Key testKey = ((KeyCounterKeyboardTrigger)kc.Counters.First().Trigger).Key;
|
||||
|
||||
void addPressKeyStep()
|
||||
{
|
||||
AddStep($"Press {testKey} key", () => InputManager.Key(testKey));
|
||||
}
|
||||
Key testKey = ((KeyCounterKeyboardTrigger)defaultDisplay.Counters.First().Trigger).Key;
|
||||
|
||||
addPressKeyStep();
|
||||
AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 1);
|
||||
addPressKeyStep();
|
||||
AddAssert($"Check {testKey} counter after keypress", () => testCounter.CountPresses.Value == 2);
|
||||
AddStep("Disable counting", () => testCounter.IsCounting.Value = false);
|
||||
AddStep("Disable counting", () =>
|
||||
{
|
||||
argonDisplay.IsCounting.Value = false;
|
||||
defaultDisplay.IsCounting.Value = false;
|
||||
});
|
||||
addPressKeyStep();
|
||||
AddAssert($"Check {testKey} count has not changed", () => testCounter.CountPresses.Value == 2);
|
||||
|
||||
Add(kc);
|
||||
Add(defaultDisplay);
|
||||
Add(argonDisplay);
|
||||
|
||||
void addPressKeyStep() => AddStep($"Press {testKey} key", () => InputManager.Key(testKey));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
// best way to check without exposing.
|
||||
private Drawable hideTarget => hudOverlay.KeyCounter;
|
||||
private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<DefaultKeyCounter>>().Single();
|
||||
private Drawable keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().Single();
|
||||
|
||||
[Test]
|
||||
public void TestComboCounterIncrementing()
|
||||
|
@ -0,0 +1,42 @@
|
||||
// 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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Menu;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Navigation
|
||||
{
|
||||
public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene
|
||||
{
|
||||
/// <summary>
|
||||
/// When entering the editor, a new beatmap is created as part of the asynchronous load process.
|
||||
/// This test ensures that in the case of an early exit from the editor (ie. while it's still loading)
|
||||
/// doesn't leave a dangling beatmap behind.
|
||||
///
|
||||
/// This may not fail 100% due to timing, but has a pretty high chance of hitting a failure so works well enough
|
||||
/// as a test.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void TestCancelNavigationToEditor()
|
||||
{
|
||||
BeatmapSetInfo[] beatmapSets = null!;
|
||||
|
||||
AddStep("Fetch initial beatmaps", () => beatmapSets = allBeatmapSets());
|
||||
|
||||
AddStep("Set current beatmap to default", () => Game.Beatmap.SetDefault());
|
||||
|
||||
AddStep("Push editor loader", () => Game.ScreenStack.Push(new EditorLoader()));
|
||||
AddUntilStep("Wait for loader current", () => Game.ScreenStack.CurrentScreen is EditorLoader);
|
||||
AddStep("Close editor while loading", () => Game.ScreenStack.CurrentScreen.Exit());
|
||||
|
||||
AddUntilStep("Wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu);
|
||||
AddAssert("Check no new beatmaps were made", () => allBeatmapSets().SequenceEqual(beatmapSets));
|
||||
|
||||
BeatmapSetInfo[] allBeatmapSets() => Game.Realm.Run(realm => realm.All<BeatmapSetInfo>().Where(x => !x.DeletePending).ToArray());
|
||||
}
|
||||
}
|
||||
}
|
@ -368,7 +368,7 @@ namespace osu.Game.Beatmaps
|
||||
// user requested abort
|
||||
return;
|
||||
|
||||
var video = b.Files.FirstOrDefault(f => OsuGameBase.VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.Ordinal)));
|
||||
var video = b.Files.FirstOrDefault(f => OsuGameBase.VIDEO_EXTENSIONS.Any(ex => f.Filename.EndsWith(ex, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
if (video != null)
|
||||
{
|
||||
|
@ -412,7 +412,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
// Some very old beatmaps had incorrect type specifications for their backgrounds (ie. using 1 for VIDEO
|
||||
// instead of 0 for BACKGROUND). To handle this gracefully, check the file extension against known supported
|
||||
// video extensions and handle similar to a background if it doesn't match.
|
||||
if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename)))
|
||||
if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(filename).ToLowerInvariant()))
|
||||
{
|
||||
beatmap.BeatmapInfo.Metadata.BackgroundFile = filename;
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ namespace osu.Game.Beatmaps.Formats
|
||||
//
|
||||
// This avoids potential weird crashes when ffmpeg attempts to parse an image file as a video
|
||||
// (see https://github.com/ppy/osu/issues/22829#issuecomment-1465552451).
|
||||
if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path)))
|
||||
if (!OsuGameBase.VIDEO_EXTENSIONS.Contains(Path.GetExtension(path).ToLowerInvariant()))
|
||||
break;
|
||||
|
||||
storyboard.GetLayer("Video").Add(new StoryboardVideo(path, offset));
|
||||
|
@ -70,7 +70,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
{
|
||||
get
|
||||
{
|
||||
if (OsuGameBase.VIDEO_EXTENSIONS.Contains(File.Extension))
|
||||
if (OsuGameBase.VIDEO_EXTENSIONS.Contains(File.Extension.ToLowerInvariant()))
|
||||
return FontAwesome.Regular.FileVideo;
|
||||
|
||||
switch (File.Extension)
|
||||
|
@ -100,14 +100,14 @@ namespace osu.Game.Localisation
|
||||
public static LocalisableString TimelineTicks => new TranslatableString(getKey(@"timeline_ticks"), @"Ticks");
|
||||
|
||||
/// <summary>
|
||||
/// "{0:0.0}°"
|
||||
/// "{0:0}°"
|
||||
/// </summary>
|
||||
public static LocalisableString RotationUnsnapped(float newRotation) => new TranslatableString(getKey(@"rotation_unsnapped"), @"{0:0.0}°", newRotation);
|
||||
public static LocalisableString RotationUnsnapped(float newRotation) => new TranslatableString(getKey(@"rotation_unsnapped"), @"{0:0}°", newRotation);
|
||||
|
||||
/// <summary>
|
||||
/// "{0:0.0}° (snapped)"
|
||||
/// "{0:0}° (snapped)"
|
||||
/// </summary>
|
||||
public static LocalisableString RotationSnapped(float newRotation) => new TranslatableString(getKey(@"rotation_snapped"), @"{0:0.0}° (snapped)", newRotation);
|
||||
public static LocalisableString RotationSnapped(float newRotation) => new TranslatableString(getKey(@"rotation_snapped"), @"{0:0}° (snapped)", newRotation);
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
@ -31,6 +32,9 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RankingsOverlay? rankingsOverlay { get; set; }
|
||||
|
||||
private UserCoverBackground cover = null!;
|
||||
private SupporterIcon supporterTag = null!;
|
||||
private UpdateableAvatar avatar = null!;
|
||||
@ -38,6 +42,7 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
private ExternalLinkButton openUserExternally = null!;
|
||||
private OsuSpriteText titleText = null!;
|
||||
private UpdateableFlag userFlag = null!;
|
||||
private OsuHoverContainer userCountryContainer = null!;
|
||||
private OsuSpriteText userCountryText = null!;
|
||||
private GroupBadgeFlow groupBadgeFlow = null!;
|
||||
private ToggleCoverButton coverToggle = null!;
|
||||
@ -156,13 +161,17 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
Size = new Vector2(28, 20),
|
||||
ShowPlaceholderOnUnknown = false,
|
||||
},
|
||||
userCountryText = new OsuSpriteText
|
||||
userCountryContainer = new OsuHoverContainer
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular),
|
||||
Margin = new MarginPadding { Left = 5 },
|
||||
Origin = Anchor.CentreLeft,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
}
|
||||
Origin = Anchor.CentreLeft,
|
||||
Margin = new MarginPadding { Left = 5 },
|
||||
Child = userCountryText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@ -202,6 +211,7 @@ namespace osu.Game.Overlays.Profile.Header
|
||||
openUserExternally.Link = $@"{api.WebsiteRootUrl}/users/{user?.Id ?? 0}";
|
||||
userFlag.CountryCode = user?.CountryCode ?? default;
|
||||
userCountryText.Text = (user?.CountryCode ?? default).GetDescription();
|
||||
userCountryContainer.Action = () => rankingsOverlay?.ShowCountry(user?.CountryCode ?? default);
|
||||
supporterTag.SupportLevel = user?.SupportLevel ?? 0;
|
||||
titleText.Text = user?.Title ?? string.Empty;
|
||||
titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff");
|
||||
|
@ -256,7 +256,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
return;
|
||||
}
|
||||
|
||||
if (host.Window is WindowsWindow)
|
||||
if (host.Renderer is IWindowsRenderer)
|
||||
{
|
||||
switch (fullscreenCapability.Value)
|
||||
{
|
||||
|
@ -20,11 +20,31 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public virtual bool RestartOnFail => Restart.Value;
|
||||
|
||||
private Action? triggerFailureDelegate;
|
||||
|
||||
public void ApplyToHealthProcessor(HealthProcessor healthProcessor)
|
||||
{
|
||||
triggerFailureDelegate = healthProcessor.TriggerFailure;
|
||||
healthProcessor.FailConditions += FailCondition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Immediately triggers a failure on the loaded <see cref="HealthProcessor"/>.
|
||||
/// </summary>
|
||||
protected void TriggerFailure() => triggerFailureDelegate?.Invoke();
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether <paramref name="result"/> should trigger a failure. Called every time a
|
||||
/// judgement is applied to <paramref name="healthProcessor"/>.
|
||||
/// </summary>
|
||||
/// <param name="healthProcessor">The loaded <see cref="HealthProcessor"/>.</param>
|
||||
/// <param name="result">The latest <see cref="JudgementResult"/>.</param>
|
||||
/// <returns>Whether the fail condition has been met.</returns>
|
||||
/// <remarks>
|
||||
/// This method should only be used to trigger failures based on <paramref name="result"/>.
|
||||
/// Using outside values to evaluate failure may introduce event ordering discrepancies, use
|
||||
/// an <see cref="IApplicableMod"/> with <see cref="TriggerFailure"/> instead.
|
||||
/// </remarks>
|
||||
protected abstract bool FailCondition(HealthProcessor healthProcessor, JudgementResult result);
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,15 @@ namespace osu.Game.Rulesets.Scoring
|
||||
/// </summary>
|
||||
public bool HasFailed { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Immediately triggers a failure for this HealthProcessor.
|
||||
/// </summary>
|
||||
public void TriggerFailure()
|
||||
{
|
||||
if (Failed?.Invoke() != false)
|
||||
HasFailed = true;
|
||||
}
|
||||
|
||||
protected override void ApplyResultInternal(JudgementResult result)
|
||||
{
|
||||
result.HealthAtJudgement = Health.Value;
|
||||
@ -42,10 +51,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
Health.Value += GetHealthIncreaseFor(result);
|
||||
|
||||
if (meetsAnyFailCondition(result))
|
||||
{
|
||||
if (Failed?.Invoke() != false)
|
||||
HasFailed = true;
|
||||
}
|
||||
TriggerFailure();
|
||||
}
|
||||
|
||||
protected override void RevertResultInternal(JudgementResult result)
|
||||
|
@ -118,7 +118,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
float oldRotation = cumulativeRotation.Value ?? 0;
|
||||
|
||||
float newRotation = shouldSnap ? snap(rawCumulativeRotation, snap_step) : rawCumulativeRotation;
|
||||
float newRotation = shouldSnap ? snap(rawCumulativeRotation, snap_step) : MathF.Round(rawCumulativeRotation);
|
||||
newRotation = (newRotation - 180) % 360 + 180;
|
||||
|
||||
cumulativeRotation.Value = newRotation;
|
||||
|
@ -210,7 +210,10 @@ namespace osu.Game.Screens.Edit
|
||||
// this is a bit haphazard, but guards against setting the lease Beatmap bindable if
|
||||
// the editor has already been exited.
|
||||
if (!ValidForPush)
|
||||
{
|
||||
beatmapManager.Delete(loadableBeatmap.BeatmapSetInfo);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
|
76
osu.Game/Screens/Play/ArgonKeyCounter.cs
Normal file
76
osu.Game/Screens/Play/ArgonKeyCounter.cs
Normal file
@ -0,0 +1,76 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
public partial class ArgonKeyCounter : KeyCounter
|
||||
{
|
||||
private Circle inputIndicator = null!;
|
||||
private OsuSpriteText countText = null!;
|
||||
|
||||
// These values were taken from Figma
|
||||
private const float line_height = 3;
|
||||
private const float name_font_size = 10;
|
||||
private const float count_font_size = 14;
|
||||
|
||||
// Make things look bigger without using Scale
|
||||
private const float scale_factor = 1.5f;
|
||||
|
||||
public ArgonKeyCounter(InputTrigger trigger)
|
||||
: base(trigger)
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
inputIndicator = new Circle
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = line_height * scale_factor,
|
||||
Alpha = 0.5f
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Position = new Vector2(0, -13) * scale_factor,
|
||||
Font = OsuFont.Torus.With(size: name_font_size * scale_factor, weight: FontWeight.Bold),
|
||||
Colour = colours.Blue0,
|
||||
Text = Trigger.Name
|
||||
},
|
||||
countText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Font = OsuFont.Torus.With(size: count_font_size * scale_factor, weight: FontWeight.Bold),
|
||||
},
|
||||
};
|
||||
|
||||
// Values from Figma didn't match visually
|
||||
// So these were just eyeballed
|
||||
Height = 30 * scale_factor;
|
||||
Width = 35 * scale_factor;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
IsActive.BindValueChanged(e => inputIndicator.Alpha = e.NewValue ? 1 : 0.5f, true);
|
||||
CountPresses.BindValueChanged(e => countText.Text = e.NewValue.ToString(@"#,0"), true);
|
||||
}
|
||||
}
|
||||
}
|
40
osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs
Normal file
40
osu.Game/Screens/Play/ArgonKeyCounterDisplay.cs
Normal file
@ -0,0 +1,40 @@
|
||||
// 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.Containers;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Play
|
||||
{
|
||||
public partial class ArgonKeyCounterDisplay : KeyCounterDisplay
|
||||
{
|
||||
private const int duration = 100;
|
||||
|
||||
protected override FillFlowContainer<KeyCounter> KeyFlow { get; }
|
||||
|
||||
public ArgonKeyCounterDisplay()
|
||||
{
|
||||
InternalChild = KeyFlow = new FillFlowContainer<KeyCounter>
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
Spacing = new Vector2(2),
|
||||
};
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
Size = KeyFlow.Size;
|
||||
}
|
||||
|
||||
protected override KeyCounter CreateCounter(InputTrigger trigger) => new ArgonKeyCounter(trigger);
|
||||
|
||||
protected override void UpdateVisibility()
|
||||
=> KeyFlow.FadeTo(AlwaysVisible.Value || ConfigVisibility.Value ? 1 : 0, duration);
|
||||
}
|
||||
}
|
@ -57,7 +57,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = Name,
|
||||
Text = Trigger.Name,
|
||||
Font = OsuFont.Numeric.With(size: 12),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
|
@ -1,7 +1,7 @@
|
||||
// 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 osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osuTK.Graphics;
|
||||
@ -13,13 +13,11 @@ namespace osu.Game.Screens.Play.HUD
|
||||
private const int duration = 100;
|
||||
private const double key_fade_time = 80;
|
||||
|
||||
private readonly FillFlowContainer<DefaultKeyCounter> keyFlow;
|
||||
|
||||
public override IEnumerable<KeyCounter> Counters => keyFlow;
|
||||
protected override FillFlowContainer<KeyCounter> KeyFlow { get; }
|
||||
|
||||
public DefaultKeyCounterDisplay()
|
||||
{
|
||||
InternalChild = keyFlow = new FillFlowContainer<DefaultKeyCounter>
|
||||
InternalChild = KeyFlow = new FillFlowContainer<KeyCounter>
|
||||
{
|
||||
Direction = FillDirection.Horizontal,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
@ -33,20 +31,19 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
// Don't use autosize as it will shrink to zero when KeyFlow is hidden.
|
||||
// In turn this can cause the display to be masked off screen and never become visible again.
|
||||
Size = keyFlow.Size;
|
||||
Size = KeyFlow.Size;
|
||||
}
|
||||
|
||||
public override void Add(InputTrigger trigger) =>
|
||||
keyFlow.Add(new DefaultKeyCounter(trigger)
|
||||
{
|
||||
FadeTime = key_fade_time,
|
||||
KeyDownTextColor = KeyDownTextColor,
|
||||
KeyUpTextColor = KeyUpTextColor,
|
||||
});
|
||||
protected override KeyCounter CreateCounter(InputTrigger trigger) => new DefaultKeyCounter(trigger)
|
||||
{
|
||||
FadeTime = key_fade_time,
|
||||
KeyDownTextColor = KeyDownTextColor,
|
||||
KeyUpTextColor = KeyUpTextColor,
|
||||
};
|
||||
|
||||
protected override void UpdateVisibility() =>
|
||||
// Isolate changing visibility of the key counters from fading this component.
|
||||
keyFlow.FadeTo(AlwaysVisible.Value || ConfigVisibility.Value ? 1 : 0, duration);
|
||||
KeyFlow.FadeTo(AlwaysVisible.Value || ConfigVisibility.Value ? 1 : 0, duration);
|
||||
|
||||
private Color4 keyDownTextColor = Color4.DarkGray;
|
||||
|
||||
@ -58,7 +55,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
if (value != keyDownTextColor)
|
||||
{
|
||||
keyDownTextColor = value;
|
||||
foreach (var child in keyFlow)
|
||||
foreach (var child in KeyFlow.Cast<DefaultKeyCounter>())
|
||||
child.KeyDownTextColor = value;
|
||||
}
|
||||
}
|
||||
@ -74,7 +71,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
if (value != keyUpTextColor)
|
||||
{
|
||||
keyUpTextColor = value;
|
||||
foreach (var child in keyFlow)
|
||||
foreach (var child in KeyFlow.Cast<DefaultKeyCounter>())
|
||||
child.KeyUpTextColor = value;
|
||||
}
|
||||
}
|
||||
|
@ -54,8 +54,6 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
Trigger.OnActivate += Activate;
|
||||
Trigger.OnDeactivate += Deactivate;
|
||||
|
||||
Name = trigger.Name;
|
||||
}
|
||||
|
||||
private void increment()
|
||||
|
@ -29,7 +29,9 @@ namespace osu.Game.Screens.Play.HUD
|
||||
/// <summary>
|
||||
/// The <see cref="KeyCounter"/>s contained in this <see cref="KeyCounterDisplay"/>.
|
||||
/// </summary>
|
||||
public abstract IEnumerable<KeyCounter> Counters { get; }
|
||||
public IEnumerable<KeyCounter> Counters => KeyFlow;
|
||||
|
||||
protected abstract FillFlowContainer<KeyCounter> KeyFlow { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the actions reported by all <see cref="InputTrigger"/>s within this <see cref="KeyCounterDisplay"/> should be counted.
|
||||
@ -53,13 +55,22 @@ namespace osu.Game.Screens.Play.HUD
|
||||
/// <summary>
|
||||
/// Add a <see cref="InputTrigger"/> to this display.
|
||||
/// </summary>
|
||||
public abstract void Add(InputTrigger trigger);
|
||||
public void Add(InputTrigger trigger)
|
||||
{
|
||||
var keyCounter = CreateCounter(trigger);
|
||||
|
||||
KeyFlow.Add(keyCounter);
|
||||
|
||||
IsCounting.BindTo(keyCounter.IsCounting);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a range of <see cref="InputTrigger"/> to this display.
|
||||
/// </summary>
|
||||
public void AddRange(IEnumerable<InputTrigger> triggers) => triggers.ForEach(Add);
|
||||
|
||||
protected abstract KeyCounter CreateCounter(InputTrigger trigger);
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
|
@ -85,7 +85,7 @@ namespace osu.Game.Storyboards.Drawables
|
||||
Loop = animation.LoopType == AnimationLoopType.LoopForever;
|
||||
|
||||
LifetimeStart = animation.StartTime;
|
||||
LifetimeEnd = animation.EndTime;
|
||||
LifetimeEnd = animation.EndTimeForDisplay;
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
|
@ -82,7 +82,7 @@ namespace osu.Game.Storyboards.Drawables
|
||||
Position = sprite.InitialPosition;
|
||||
|
||||
LifetimeStart = sprite.StartTime;
|
||||
LifetimeEnd = sprite.EndTime;
|
||||
LifetimeEnd = sprite.EndTimeForDisplay;
|
||||
}
|
||||
|
||||
[Resolved]
|
||||
|
@ -12,9 +12,17 @@ namespace osu.Game.Storyboards
|
||||
{
|
||||
/// <summary>
|
||||
/// The time at which the <see cref="IStoryboardElement"/> ends.
|
||||
/// This is consumed to extend the length of a storyboard to ensure all visuals are played to completion.
|
||||
/// </summary>
|
||||
double EndTime { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The time this element displays until.
|
||||
/// This is used for lifetime purposes, and includes long playing animations which don't necessarily extend
|
||||
/// a storyboard's play time.
|
||||
/// </summary>
|
||||
double EndTimeForDisplay { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The duration of the StoryboardElement.
|
||||
/// </summary>
|
||||
|
@ -1,8 +1,6 @@
|
||||
// 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 osuTK;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Storyboards.Drawables;
|
||||
|
@ -1,12 +1,9 @@
|
||||
// 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 System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Storyboards.Drawables;
|
||||
using osuTK;
|
||||
@ -84,6 +81,19 @@ namespace osu.Game.Storyboards
|
||||
}
|
||||
}
|
||||
|
||||
public double EndTimeForDisplay
|
||||
{
|
||||
get
|
||||
{
|
||||
double latestEndTime = TimelineGroup.EndTime;
|
||||
|
||||
foreach (var l in loops)
|
||||
latestEndTime = Math.Max(latestEndTime, l.StartTime + l.CommandsDuration * l.TotalIterations);
|
||||
|
||||
return latestEndTime;
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasCommands => TimelineGroup.HasCommands || loops.Any(l => l.HasCommands);
|
||||
|
||||
private delegate void DrawablePropertyInitializer<in T>(Drawable drawable, T value);
|
||||
@ -114,7 +124,7 @@ namespace osu.Game.Storyboards
|
||||
public virtual Drawable CreateDrawable()
|
||||
=> new DrawableStoryboardSprite(this);
|
||||
|
||||
public void ApplyTransforms(Drawable drawable, IEnumerable<Tuple<CommandTimelineGroup, double>> triggeredGroups = null)
|
||||
public void ApplyTransforms(Drawable drawable, IEnumerable<Tuple<CommandTimelineGroup, double>>? triggeredGroups = null)
|
||||
{
|
||||
// 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
|
||||
@ -156,7 +166,7 @@ namespace osu.Game.Storyboards
|
||||
|
||||
foreach (var command in commands)
|
||||
{
|
||||
DrawablePropertyInitializer<T> initFunc = null;
|
||||
DrawablePropertyInitializer<T>? initFunc = null;
|
||||
|
||||
if (!initialized)
|
||||
{
|
||||
@ -169,7 +179,7 @@ namespace osu.Game.Storyboards
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<CommandTimeline<T>.TypedCommand> getCommands<T>(CommandTimelineSelector<T> timelineSelector, IEnumerable<Tuple<CommandTimelineGroup, double>> triggeredGroups)
|
||||
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)
|
||||
@ -198,11 +208,11 @@ namespace osu.Game.Storyboards
|
||||
{
|
||||
public double StartTime => command.StartTime;
|
||||
|
||||
private readonly DrawablePropertyInitializer<T> initializeProperty;
|
||||
private readonly DrawablePropertyInitializer<T>? initializeProperty;
|
||||
private readonly DrawableTransformer<T> transform;
|
||||
private readonly CommandTimeline<T>.TypedCommand command;
|
||||
|
||||
public GeneratedCommand([NotNull] CommandTimeline<T>.TypedCommand command, [CanBeNull] DrawablePropertyInitializer<T> initializeProperty, [NotNull] DrawableTransformer<T> transform)
|
||||
public GeneratedCommand(CommandTimeline<T>.TypedCommand command, DrawablePropertyInitializer<T>? initializeProperty, DrawableTransformer<T> transform)
|
||||
{
|
||||
this.command = command;
|
||||
this.initializeProperty = initializeProperty;
|
||||
|
Loading…
Reference in New Issue
Block a user