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

Merge remote-tracking branch 'upstream/master' into sample-control-points

This commit is contained in:
OliBomby 2023-04-30 19:47:03 +02:00
commit 608fa3cc73
36 changed files with 441 additions and 80 deletions

View File

@ -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();
}

View File

@ -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;

View File

@ -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);

View File

@ -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;
}

View File

@ -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()
{

View File

@ -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);
}
}

View File

@ -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

View File

@ -0,0 +1,5 @@
osu file format v14
[Events]
0,0,"BG.jpg",0,0
Video,0,"Video.avi",0,0

View File

@ -0,0 +1,5 @@
osu file format v14
[Events]
0,0,"BG.jpg",0,0
Video,0,"Video.AVI",0,0

View File

@ -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();
}

View File

@ -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()

View File

@ -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));
}
}
}

View File

@ -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()

View File

@ -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());
}
}
}

View File

@ -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)
{

View File

@ -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;
}

View File

@ -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));

View File

@ -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)

View File

@ -100,14 +100,14 @@ namespace osu.Game.Localisation
public static LocalisableString TimelineTicks => new TranslatableString(getKey(@"timeline_ticks"), @"Ticks");
/// <summary>
/// "{0:0.0}&#176;"
/// "{0:0}&#176;"
/// </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}&#176; (snapped)"
/// "{0:0}&#176; (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}";
}

View File

@ -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");

View File

@ -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)
{

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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;

View File

@ -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

View 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);
}
}
}

View 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);
}
}

View File

@ -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,

View File

@ -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;
}
}

View File

@ -54,8 +54,6 @@ namespace osu.Game.Screens.Play.HUD
Trigger.OnActivate += Activate;
Trigger.OnDeactivate += Deactivate;
Name = trigger.Name;
}
private void increment()

View File

@ -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)
{

View File

@ -85,7 +85,7 @@ namespace osu.Game.Storyboards.Drawables
Loop = animation.LoopType == AnimationLoopType.LoopForever;
LifetimeStart = animation.StartTime;
LifetimeEnd = animation.EndTime;
LifetimeEnd = animation.EndTimeForDisplay;
}
[Resolved]

View File

@ -82,7 +82,7 @@ namespace osu.Game.Storyboards.Drawables
Position = sprite.InitialPosition;
LifetimeStart = sprite.StartTime;
LifetimeEnd = sprite.EndTime;
LifetimeEnd = sprite.EndTimeForDisplay;
}
[Resolved]

View File

@ -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>

View File

@ -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;

View File

@ -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;