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

Merge branch 'master' into adjust-flashlight

This commit is contained in:
Dan Balasescu 2022-10-07 14:45:30 +09:00
commit bce20e0a59
60 changed files with 1032 additions and 151 deletions

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1003.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.1005.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.922.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.1005.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -137,12 +137,13 @@ namespace osu.Desktop
{ {
base.SetHost(host); base.SetHost(host);
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
var desktopWindow = (SDL2DesktopWindow)host.Window; var desktopWindow = (SDL2DesktopWindow)host.Window;
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
if (iconStream != null)
desktopWindow.SetIconFromStream(iconStream);
desktopWindow.CursorState |= CursorState.Hidden; desktopWindow.CursorState |= CursorState.Hidden;
desktopWindow.SetIconFromStream(iconStream);
desktopWindow.Title = Name; desktopWindow.Title = Name;
desktopWindow.DragDrop += f => fileDrop(new[] { f }); desktopWindow.DragDrop += f => fileDrop(new[] { f });
} }

View File

@ -1,10 +1,15 @@
// 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Mods;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests namespace osu.Game.Rulesets.Catch.Tests
@ -12,11 +17,11 @@ namespace osu.Game.Rulesets.Catch.Tests
[TestFixture] [TestFixture]
public class TestSceneCatchTouchInput : OsuTestScene public class TestSceneCatchTouchInput : OsuTestScene
{ {
private CatchTouchInputMapper catchTouchInputMapper = null!; [Test]
public void TestBasic()
[SetUpSteps]
public void SetUpSteps()
{ {
CatchTouchInputMapper catchTouchInputMapper = null!;
AddStep("create input overlay", () => AddStep("create input overlay", () =>
{ {
Child = new CatchInputManager(new CatchRuleset().RulesetInfo) Child = new CatchInputManager(new CatchRuleset().RulesetInfo)
@ -32,12 +37,30 @@ namespace osu.Game.Rulesets.Catch.Tests
} }
}; };
}); });
AddStep("show overlay", () => catchTouchInputMapper.Show());
} }
[Test] [Test]
public void TestBasic() public void TestWithoutRelax()
{ {
AddStep("show overlay", () => catchTouchInputMapper.Show()); AddStep("create drawable ruleset without relax mod", () =>
{
Child = new DrawableCatchRuleset(new CatchRuleset(), new CatchBeatmap(), new List<Mod>());
});
AddUntilStep("wait for load", () => Child.IsLoaded);
AddAssert("check touch input is shown", () => this.ChildrenOfType<CatchTouchInputMapper>().Any());
}
[Test]
public void TestWithRelax()
{
AddStep("create drawable ruleset with relax mod", () =>
{
Child = new DrawableCatchRuleset(new CatchRuleset(), new CatchBeatmap(), new List<Mod> { new CatchModRelax() });
});
AddUntilStep("wait for load", () => Child.IsLoaded);
AddAssert("check touch input is not shown", () => !this.ChildrenOfType<CatchTouchInputMapper>().Any());
} }
} }
} }

View File

@ -4,6 +4,7 @@
#nullable disable #nullable disable
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -36,7 +37,9 @@ namespace osu.Game.Rulesets.Catch.UI
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
KeyBindingInputManager.Add(new CatchTouchInputMapper()); // With relax mod, input maps directly to x position and left/right buttons are not used.
if (!Mods.Any(m => m is ModRelax))
KeyBindingInputManager.Add(new CatchTouchInputMapper());
} }
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay); protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new CatchFramedReplayInputHandler(replay);

View File

@ -54,10 +54,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
} }
} }
protected override void UpdateInitialTransforms()
{
}
protected override void UpdateStartTimeStateTransforms() => this.FadeOut(150); protected override void UpdateStartTimeStateTransforms() => this.FadeOut(150);
} }
} }

View File

@ -30,20 +30,15 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
public bool UpdateResult() => base.UpdateResult(true); public bool UpdateResult() => base.UpdateResult(true);
protected override void UpdateInitialTransforms()
{
base.UpdateInitialTransforms();
// This hitobject should never expire, so this is just a safe maximum.
LifetimeEnd = LifetimeStart + 30000;
}
protected override void UpdateHitStateTransforms(ArmedState state) protected override void UpdateHitStateTransforms(ArmedState state)
{ {
// suppress the base call explicitly. // suppress the base call explicitly.
// the hold note head should never change its visual state on its own due to the "freezing" mechanic // the hold note head should never change its visual state on its own due to the "freezing" mechanic
// (when hit, it remains visible in place at the judgement line; when dropped, it will scroll past the line). // (when hit, it remains visible in place at the judgement line; when dropped, it will scroll past the line).
// it will be hidden along with its parenting hold note when required. // it will be hidden along with its parenting hold note when required.
// Set `LifetimeEnd` explicitly to a non-`double.MaxValue` because otherwise this DHO is automatically expired.
LifetimeEnd = double.PositiveInfinity;
} }
public override bool OnPressed(KeyBindingPressEvent<ManiaAction> e) => false; // Handled by the hold note public override bool OnPressed(KeyBindingPressEvent<ManiaAction> e) => false; // Handled by the hold note

View File

@ -23,10 +23,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>(); protected readonly IBindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>();
// Leaving the default (10s) makes hitobjects not appear, as this offset is used for the initial state transforms.
// Calculated as DrawableManiaRuleset.MAX_TIME_RANGE + some additional allowance for velocity < 1.
protected override double InitialLifetimeOffset => 30000;
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private ManiaPlayfield playfield { get; set; } private ManiaPlayfield playfield { get; set; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,136 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Framework.Input.States;
using osu.Framework.Logging;
using osu.Framework.Testing.Input;
using osu.Game.Rulesets.Osu.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneSmoke : OsuSkinnableTestScene
{
[Test]
public void TestSmoking()
{
addStep("Create short smoke", 2_000);
addStep("Create medium smoke", 5_000);
addStep("Create long smoke", 10_000);
}
private void addStep(string stepName, double duration)
{
var smokeContainers = new List<SmokeContainer>();
AddStep(stepName, () =>
{
smokeContainers.Clear();
SetContents(_ =>
{
smokeContainers.Add(new TestSmokeContainer
{
Duration = duration,
RelativeSizeAxes = Axes.Both
});
return new SmokingInputManager
{
Duration = duration,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.95f),
Child = smokeContainers[^1],
};
});
});
AddUntilStep("Until skinnable expires", () =>
{
if (smokeContainers.Count == 0)
return false;
Logger.Log("How many: " + smokeContainers.Count);
foreach (var smokeContainer in smokeContainers)
{
if (smokeContainer.Children.Count != 0)
return false;
}
return true;
});
}
private class SmokingInputManager : ManualInputManager
{
public double Duration { get; init; }
private double? startTime;
public SmokingInputManager()
{
UseParentInput = false;
}
protected override void LoadComplete()
{
base.LoadComplete();
MoveMouseTo(ToScreenSpace(DrawSize / 2));
}
protected override void Update()
{
base.Update();
const float spin_angle = 4 * MathF.PI;
startTime ??= Time.Current;
float fraction = (float)((Time.Current - startTime) / Duration);
float angle = fraction * spin_angle;
float radius = fraction * Math.Min(DrawSize.X, DrawSize.Y) / 2;
Vector2 pos = radius * new Vector2(MathF.Cos(angle), MathF.Sin(angle)) + DrawSize / 2;
MoveMouseTo(ToScreenSpace(pos));
}
}
private class TestSmokeContainer : SmokeContainer
{
public double Duration { get; init; }
private bool isPressing;
private bool isFinished;
private double? startTime;
protected override void Update()
{
base.Update();
startTime ??= Time.Current + 0.1;
if (!isPressing && !isFinished && Time.Current > startTime)
{
OnPressed(new KeyBindingPressEvent<OsuAction>(new InputState(), OsuAction.Smoke));
isPressing = true;
isFinished = false;
}
if (isPressing && Time.Current > startTime + Duration)
{
OnReleased(new KeyBindingReleaseEvent<OsuAction>(new InputState(), OsuAction.Smoke));
isPressing = false;
isFinished = true;
}
}
}
}
}

View File

@ -204,12 +204,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
// todo: temporary / arbitrary, used for lifetime optimisation. // todo: temporary / arbitrary, used for lifetime optimisation.
this.Delay(800).FadeOut(); this.Delay(800).FadeOut();
// in the case of an early state change, the fade should be expedited to the current point in time.
if (HitStateUpdateTime < HitObject.StartTime)
ApproachCircle.FadeOut(50);
switch (state) switch (state)
{ {
default:
ApproachCircle.FadeOut();
break;
case ArmedState.Idle: case ArmedState.Idle:
HitArea.HitAction = null; HitArea.HitAction = null;
break; break;

View File

@ -11,7 +11,9 @@ using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Scoring;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
@ -64,6 +66,23 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ScaleBindable.UnbindFrom(HitObject.ScaleBindable); ScaleBindable.UnbindFrom(HitObject.ScaleBindable);
} }
protected override void UpdateInitialTransforms()
{
base.UpdateInitialTransforms();
// Dim should only be applied at a top level, as it will be implicitly applied to nested objects.
if (ParentHitObject == null)
{
// Of note, no one noticed this was missing for years, but it definitely feels like it should still exist.
// For now this is applied across all skins, and matches stable.
// For simplicity, dim colour is applied to the DrawableHitObject itself.
// We may need to make a nested container setup if this even causes a usage conflict (ie. with a mod).
this.FadeColour(new Color4(195, 195, 195, 255));
using (BeginDelayedSequence(InitialLifetimeOffset - OsuHitWindows.MISS_WINDOW))
this.FadeColour(Color4.White, 100);
}
}
protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt; protected sealed override double InitialLifetimeOffset => HitObject.TimePreempt;
private OsuInputManager osuActionInputManager; private OsuInputManager osuActionInputManager;

View File

@ -80,6 +80,9 @@ namespace osu.Game.Rulesets.Osu
LeftButton, LeftButton,
[Description("Right button")] [Description("Right button")]
RightButton RightButton,
[Description("Smoke")]
Smoke,
} }
} }

View File

@ -59,6 +59,7 @@ namespace osu.Game.Rulesets.Osu
{ {
new KeyBinding(InputKey.Z, OsuAction.LeftButton), new KeyBinding(InputKey.Z, OsuAction.LeftButton),
new KeyBinding(InputKey.X, OsuAction.RightButton), new KeyBinding(InputKey.X, OsuAction.RightButton),
new KeyBinding(InputKey.C, OsuAction.Smoke),
new KeyBinding(InputKey.MouseLeft, OsuAction.LeftButton), new KeyBinding(InputKey.MouseLeft, OsuAction.LeftButton),
new KeyBinding(InputKey.MouseRight, OsuAction.RightButton), new KeyBinding(InputKey.MouseRight, OsuAction.RightButton),
}; };

View File

@ -21,6 +21,7 @@ namespace osu.Game.Rulesets.Osu
SliderBall, SliderBall,
SliderBody, SliderBody,
SpinnerBody, SpinnerBody,
CursorSmoke,
ApproachCircle, ApproachCircle,
} }
} }

View File

@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Osu.Replays
Position = currentFrame.Position; Position = currentFrame.Position;
if (currentFrame.MouseLeft) Actions.Add(OsuAction.LeftButton); if (currentFrame.MouseLeft) Actions.Add(OsuAction.LeftButton);
if (currentFrame.MouseRight) Actions.Add(OsuAction.RightButton); if (currentFrame.MouseRight) Actions.Add(OsuAction.RightButton);
if (currentFrame.Smoke) Actions.Add(OsuAction.Smoke);
} }
public LegacyReplayFrame ToLegacy(IBeatmap beatmap) public LegacyReplayFrame ToLegacy(IBeatmap beatmap)
@ -41,6 +42,8 @@ namespace osu.Game.Rulesets.Osu.Replays
state |= ReplayButtonState.Left1; state |= ReplayButtonState.Left1;
if (Actions.Contains(OsuAction.RightButton)) if (Actions.Contains(OsuAction.RightButton))
state |= ReplayButtonState.Right1; state |= ReplayButtonState.Right1;
if (Actions.Contains(OsuAction.Smoke))
state |= ReplayButtonState.Smoke;
return new LegacyReplayFrame(Time, Position.X, Position.Y, state); return new LegacyReplayFrame(Time, Position.X, Position.Y, state);
} }

View File

@ -1,20 +1,23 @@
// 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring namespace osu.Game.Rulesets.Osu.Scoring
{ {
public class OsuHitWindows : HitWindows public class OsuHitWindows : HitWindows
{ {
/// <summary>
/// osu! ruleset has a fixed miss window regardless of difficulty settings.
/// </summary>
public const double MISS_WINDOW = 400;
private static readonly DifficultyRange[] osu_ranges = private static readonly DifficultyRange[] osu_ranges =
{ {
new DifficultyRange(HitResult.Great, 80, 50, 20), new DifficultyRange(HitResult.Great, 80, 50, 20),
new DifficultyRange(HitResult.Ok, 140, 100, 60), new DifficultyRange(HitResult.Ok, 140, 100, 60),
new DifficultyRange(HitResult.Meh, 200, 150, 100), new DifficultyRange(HitResult.Meh, 200, 150, 100),
new DifficultyRange(HitResult.Miss, 400, 400, 400), new DifficultyRange(HitResult.Miss, MISS_WINDOW, MISS_WINDOW, MISS_WINDOW),
}; };
public override bool IsHitResultAllowed(HitResult result) public override bool IsHitResultAllowed(HitResult result)

View File

@ -0,0 +1,19 @@
// 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.Textures;
namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public class DefaultSmokeSegment : SmokeSegment
{
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
// ISkinSource doesn't currently fallback to global textures.
// We might want to change this in the future if the intention is to allow the user to skin this as per legacy skins.
Texture = textures.Get("Gameplay/osu/cursor-smoke");
}
}
}

View File

@ -0,0 +1,19 @@
// 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.Game.Skinning;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
public class LegacySmokeSegment : SmokeSegment
{
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
base.LoadComplete();
Texture = skin.GetTexture("cursor-smoke");
}
}
}

View File

@ -106,6 +106,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
return null; return null;
case OsuSkinComponents.CursorSmoke:
if (GetTexture("cursor-smoke") != null)
return new LegacySmokeSegment();
return null;
case OsuSkinComponents.HitCircleText: case OsuSkinComponents.HitCircleText:
if (!this.HasFont(LegacyFont.HitCircle)) if (!this.HasFont(LegacyFont.HitCircle))
return null; return null;

View File

@ -0,0 +1,366 @@
// 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.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Textures;
using osu.Framework.Utils;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning
{
public abstract class SmokeSegment : Drawable, ITexturedShaderDrawable
{
private const int max_point_count = 18_000;
// fade anim values
private const double initial_fade_out_duration = 4000;
private const double re_fade_in_speed = 3;
private const double re_fade_in_duration = 50;
private const double final_fade_out_speed = 2;
private const double final_fade_out_duration = 8000;
private const float initial_alpha = 0.6f;
private const float re_fade_in_alpha = 1f;
private readonly int rotationSeed = RNG.Next();
// scale anim values
private const double scale_duration = 1200;
private const float initial_scale = 0.65f;
private const float final_scale = 1f;
// rotation anim values
private const double rotation_duration = 500;
private const float max_rotation = 0.25f;
public IShader? TextureShader { get; private set; }
public IShader? RoundedTextureShader { get; private set; }
protected Texture? Texture { get; set; }
private float radius => Texture?.DisplayWidth * 0.165f ?? 3;
protected readonly List<SmokePoint> SmokePoints = new List<SmokePoint>();
private float pointInterval => radius * 7f / 8;
private double smokeStartTime { get; set; } = double.MinValue;
private double smokeEndTime { get; set; } = double.MaxValue;
private float totalDistance;
private Vector2? lastPosition;
[BackgroundDependencyLoader]
private void load(ShaderManager shaders)
{
RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED);
TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE);
}
protected override void LoadComplete()
{
base.LoadComplete();
RelativeSizeAxes = Axes.Both;
LifetimeStart = smokeStartTime = Time.Current;
totalDistance = pointInterval;
}
private Vector2 nextPointDirection()
{
float angle = RNG.NextSingle(0, 2 * MathF.PI);
return new Vector2(MathF.Sin(angle), -MathF.Cos(angle));
}
public void AddPosition(Vector2 position, double time)
{
lastPosition ??= position;
float delta = (position - (Vector2)lastPosition).LengthFast;
totalDistance += delta;
int count = (int)(totalDistance / pointInterval);
if (count > 0)
{
Vector2 increment = position - (Vector2)lastPosition;
increment.NormalizeFast();
Vector2 pointPos = (pointInterval - (totalDistance - delta)) * increment + (Vector2)lastPosition;
increment *= pointInterval;
if (SmokePoints.Count > 0 && SmokePoints[^1].Time > time)
{
int index = ~SmokePoints.BinarySearch(new SmokePoint { Time = time }, new SmokePoint.UpperBoundComparer());
SmokePoints.RemoveRange(index, SmokePoints.Count - index);
}
totalDistance %= pointInterval;
for (int i = 0; i < count; i++)
{
SmokePoints.Add(new SmokePoint
{
Position = pointPos,
Time = time,
Direction = nextPointDirection(),
});
pointPos += increment;
}
Invalidate(Invalidation.DrawNode);
}
lastPosition = position;
if (SmokePoints.Count >= max_point_count)
FinishDrawing(time);
}
public void FinishDrawing(double time)
{
smokeEndTime = time;
double initialFadeOutDurationTrunc = Math.Min(initial_fade_out_duration, smokeEndTime - smokeStartTime);
LifetimeEnd = smokeEndTime + final_fade_out_duration + initialFadeOutDurationTrunc / re_fade_in_speed + initialFadeOutDurationTrunc / final_fade_out_speed;
}
protected override DrawNode CreateDrawNode() => new SmokeDrawNode(this);
protected override void Update()
{
base.Update();
Invalidate(Invalidation.DrawNode);
}
protected struct SmokePoint
{
public Vector2 Position;
public double Time;
public Vector2 Direction;
public struct UpperBoundComparer : IComparer<SmokePoint>
{
public int Compare(SmokePoint x, SmokePoint target)
{
// By returning -1 when the target value is equal to x, guarantees that the
// element at BinarySearch's returned index will always be the first element
// larger. Since 0 is never returned, the target is never "found", so the return
// value will be the index's complement.
return x.Time > target.Time ? 1 : -1;
}
}
}
protected class SmokeDrawNode : TexturedShaderDrawNode
{
protected new SmokeSegment Source => (SmokeSegment)base.Source;
protected double SmokeStartTime { get; private set; }
protected double SmokeEndTime { get; private set; }
protected double CurrentTime { get; private set; }
private readonly List<SmokePoint> points = new List<SmokePoint>();
private IVertexBatch<TexturedVertex2D>? quadBatch;
private float radius;
private Vector2 drawSize;
private Texture? texture;
// anim calculation vars (color, scale, direction)
private double initialFadeOutDurationTrunc;
private double firstVisiblePointTime;
private double initialFadeOutTime;
private double reFadeInTime;
private double finalFadeOutTime;
private Random rotationRNG = new Random();
public SmokeDrawNode(ITexturedShaderDrawable source)
: base(source)
{
}
public override void ApplyState()
{
base.ApplyState();
points.Clear();
points.AddRange(Source.SmokePoints);
radius = Source.radius;
drawSize = Source.DrawSize;
texture = Source.Texture;
SmokeStartTime = Source.smokeStartTime;
SmokeEndTime = Source.smokeEndTime;
CurrentTime = Source.Clock.CurrentTime;
rotationRNG = new Random(Source.rotationSeed);
initialFadeOutDurationTrunc = Math.Min(initial_fade_out_duration, SmokeEndTime - SmokeStartTime);
firstVisiblePointTime = SmokeEndTime - initialFadeOutDurationTrunc;
initialFadeOutTime = CurrentTime;
reFadeInTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / re_fade_in_speed);
finalFadeOutTime = CurrentTime - initialFadeOutDurationTrunc - firstVisiblePointTime * (1 - 1 / final_fade_out_speed);
}
public sealed override void Draw(IRenderer renderer)
{
base.Draw(renderer);
if (points.Count == 0)
return;
quadBatch ??= renderer.CreateQuadBatch<TexturedVertex2D>(max_point_count / 10, 10);
texture ??= renderer.WhitePixel;
RectangleF textureRect = texture.GetTextureRect();
var shader = GetAppropriateShader(renderer);
renderer.SetBlend(BlendingParameters.Additive);
renderer.PushLocalMatrix(DrawInfo.Matrix);
shader.Bind();
texture.Bind();
foreach (var point in points)
drawPointQuad(point, textureRect);
shader.Unbind();
renderer.PopLocalMatrix();
}
protected Color4 ColourAtPosition(Vector2 localPos) => DrawColourInfo.Colour.HasSingleColour
? ((SRGBColour)DrawColourInfo.Colour).Linear
: DrawColourInfo.Colour.Interpolate(Vector2.Divide(localPos, drawSize)).Linear;
protected virtual Color4 PointColour(SmokePoint point)
{
var color = Color4.White;
double timeDoingInitialFadeOut = Math.Min(initialFadeOutTime, SmokeEndTime) - point.Time;
if (timeDoingInitialFadeOut > 0)
{
float fraction = Math.Clamp((float)(timeDoingInitialFadeOut / initial_fade_out_duration), 0, 1);
color.A = (1 - fraction) * initial_alpha;
}
if (color.A > 0)
{
double timeDoingReFadeIn = reFadeInTime - point.Time / re_fade_in_speed;
double timeDoingFinalFadeOut = finalFadeOutTime - point.Time / final_fade_out_speed;
if (timeDoingFinalFadeOut > 0)
{
float fraction = Math.Clamp((float)(timeDoingFinalFadeOut / final_fade_out_duration), 0, 1);
fraction = MathF.Pow(fraction, 5);
color.A = (1 - fraction) * re_fade_in_alpha;
}
else if (timeDoingReFadeIn > 0)
{
float fraction = Math.Clamp((float)(timeDoingReFadeIn / re_fade_in_duration), 0, 1);
fraction = 1 - MathF.Pow(1 - fraction, 5);
color.A = fraction * (re_fade_in_alpha - color.A) + color.A;
}
}
return color;
}
protected virtual float PointScale(SmokePoint point)
{
double timeDoingScale = CurrentTime - point.Time;
float fraction = Math.Clamp((float)(timeDoingScale / scale_duration), 0, 1);
fraction = 1 - MathF.Pow(1 - fraction, 5);
return fraction * (final_scale - initial_scale) + initial_scale;
}
protected virtual Vector2 PointDirection(SmokePoint point)
{
float initialAngle = MathF.Atan2(point.Direction.Y, point.Direction.X);
float finalAngle = initialAngle + nextRotation();
double timeDoingRotation = CurrentTime - point.Time;
float fraction = Math.Clamp((float)(timeDoingRotation / rotation_duration), 0, 1);
fraction = 1 - MathF.Pow(1 - fraction, 5);
float angle = fraction * (finalAngle - initialAngle) + initialAngle;
return new Vector2(MathF.Sin(angle), -MathF.Cos(angle));
}
private float nextRotation() => max_rotation * ((float)rotationRNG.NextDouble() * 2 - 1);
private void drawPointQuad(SmokePoint point, RectangleF textureRect)
{
Debug.Assert(quadBatch != null);
var colour = PointColour(point);
float scale = PointScale(point);
var dir = PointDirection(point);
var ortho = dir.PerpendicularLeft;
if (colour.A == 0 || scale == 0)
return;
var localTopLeft = point.Position + (radius * scale * (-ortho - dir));
var localTopRight = point.Position + (radius * scale * (-ortho + dir));
var localBotLeft = point.Position + (radius * scale * (ortho - dir));
var localBotRight = point.Position + (radius * scale * (ortho + dir));
quadBatch.Add(new TexturedVertex2D
{
Position = localTopLeft,
TexturePosition = textureRect.TopLeft,
Colour = Color4Extensions.Multiply(ColourAtPosition(localTopLeft), colour),
});
quadBatch.Add(new TexturedVertex2D
{
Position = localTopRight,
TexturePosition = textureRect.TopRight,
Colour = Color4Extensions.Multiply(ColourAtPosition(localTopRight), colour),
});
quadBatch.Add(new TexturedVertex2D
{
Position = localBotRight,
TexturePosition = textureRect.BottomRight,
Colour = Color4Extensions.Multiply(ColourAtPosition(localBotRight), colour),
});
quadBatch.Add(new TexturedVertex2D
{
Position = localBotLeft,
TexturePosition = textureRect.BottomLeft,
Colour = Color4Extensions.Multiply(ColourAtPosition(localBotLeft), colour),
});
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
quadBatch?.Dispose();
}
}
}
}

View File

@ -54,6 +54,7 @@ namespace osu.Game.Rulesets.Osu.UI
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both }, playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
new SmokeContainer { RelativeSizeAxes = Axes.Both },
spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both }, spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both },
FollowPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both }, FollowPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
judgementLayer = new JudgementContainer<DrawableOsuJudgement> { RelativeSizeAxes = Axes.Both }, judgementLayer = new JudgementContainer<DrawableOsuJudgement> { RelativeSizeAxes = Axes.Both },

View File

@ -0,0 +1,77 @@
// 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.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.UI
{
/// <summary>
/// Manages smoke trails generated from user input.
/// </summary>
public class SmokeContainer : Container, IRequireHighFrequencyMousePosition, IKeyBindingHandler<OsuAction>
{
private SmokeSkinnableDrawable? currentSegmentSkinnable;
private Vector2 lastMousePosition;
public override bool ReceivePositionalInputAt(Vector2 _) => true;
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
{
if (e.Action == OsuAction.Smoke)
{
AddInternal(currentSegmentSkinnable = new SmokeSkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.CursorSmoke), _ => new DefaultSmokeSegment()));
// Add initial position immediately.
addPosition();
return true;
}
return false;
}
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{
if (e.Action == OsuAction.Smoke)
{
if (currentSegmentSkinnable?.Drawable is SmokeSegment segment)
{
segment.FinishDrawing(Time.Current);
currentSegmentSkinnable = null;
}
}
}
protected override bool OnMouseMove(MouseMoveEvent e)
{
lastMousePosition = e.MousePosition;
addPosition();
return base.OnMouseMove(e);
}
private void addPosition() => (currentSegmentSkinnable?.Drawable as SmokeSegment)?.AddPosition(lastMousePosition, Time.Current);
private class SmokeSkinnableDrawable : SkinnableDrawable
{
public override bool RemoveWhenNotAlive => true;
public override double LifetimeStart => Drawable.LifetimeStart;
public override double LifetimeEnd => Drawable.LifetimeEnd;
public SmokeSkinnableDrawable(ISkinComponent component, Func<ISkinComponent, Drawable>? defaultImplementation = null, ConfineMode confineMode = ConfineMode.NoScaling)
: base(component, defaultImplementation, confineMode)
{
}
}
}
}

View File

@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private int countOk; private int countOk;
private int countMeh; private int countMeh;
private int countMiss; private int countMiss;
private double accuracy;
private double effectiveMissCount; private double effectiveMissCount;
@ -36,6 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok); countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
accuracy = customAccuracy;
// The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000. // The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000.
if (totalSuccessfulHits > 0) if (totalSuccessfulHits > 0)
@ -87,7 +89,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>)) if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>))
difficultyValue *= 1.050 * lengthBonus; difficultyValue *= 1.050 * lengthBonus;
return difficultyValue * Math.Pow(score.Accuracy, 2.0); return difficultyValue * Math.Pow(accuracy, 2.0);
} }
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes) private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
@ -95,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (attributes.GreatHitWindow <= 0) if (attributes.GreatHitWindow <= 0)
return 0; return 0;
double accuracyValue = Math.Pow(60.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 8.0) * Math.Pow(attributes.StarRating, 0.4) * 27.0; double accuracyValue = Math.Pow(60.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(accuracy, 8.0) * Math.Pow(attributes.StarRating, 0.4) * 27.0;
double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
accuracyValue *= lengthBonus; accuracyValue *= lengthBonus;
@ -110,5 +112,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh; private int totalSuccessfulHits => countGreat + countOk + countMeh;
private double customAccuracy => totalHits > 0 ? (countGreat * 300 + countOk * 150) / (totalHits * 300.0) : 0;
} }
} }

View File

@ -8,7 +8,6 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -78,7 +77,7 @@ namespace osu.Game.Tests.Online
} }
}; };
beatmaps.AllowImport = new TaskCompletionSource<bool>(); beatmaps.AllowImport.Reset();
testBeatmapFile = TestResources.GetQuickTestBeatmapForImport(); testBeatmapFile = TestResources.GetQuickTestBeatmapForImport();
@ -132,7 +131,7 @@ namespace osu.Game.Tests.Online
AddStep("finish download", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet))!.TriggerSuccess(testBeatmapFile)); AddStep("finish download", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet))!.TriggerSuccess(testBeatmapFile));
addAvailabilityCheckStep("state importing", BeatmapAvailability.Importing); addAvailabilityCheckStep("state importing", BeatmapAvailability.Importing);
AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true)); AddStep("allow importing", () => beatmaps.AllowImport.Set());
AddUntilStep("wait for import", () => beatmaps.CurrentImport != null); AddUntilStep("wait for import", () => beatmaps.CurrentImport != null);
AddUntilStep("ensure beatmap available", () => beatmaps.IsAvailableLocally(testBeatmapSet)); AddUntilStep("ensure beatmap available", () => beatmaps.IsAvailableLocally(testBeatmapSet));
addAvailabilityCheckStep("state is locally available", BeatmapAvailability.LocallyAvailable); addAvailabilityCheckStep("state is locally available", BeatmapAvailability.LocallyAvailable);
@ -141,7 +140,7 @@ namespace osu.Game.Tests.Online
[Test] [Test]
public void TestTrackerRespectsSoftDeleting() public void TestTrackerRespectsSoftDeleting()
{ {
AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true)); AddStep("allow importing", () => beatmaps.AllowImport.Set());
AddStep("import beatmap", () => beatmaps.Import(testBeatmapFile).WaitSafely()); AddStep("import beatmap", () => beatmaps.Import(testBeatmapFile).WaitSafely());
addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable); addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable);
@ -155,7 +154,7 @@ namespace osu.Game.Tests.Online
[Test] [Test]
public void TestTrackerRespectsChecksum() public void TestTrackerRespectsChecksum()
{ {
AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true)); AddStep("allow importing", () => beatmaps.AllowImport.Set());
AddStep("import beatmap", () => beatmaps.Import(testBeatmapFile).WaitSafely()); AddStep("import beatmap", () => beatmaps.Import(testBeatmapFile).WaitSafely());
addAvailabilityCheckStep("initially locally available", BeatmapAvailability.LocallyAvailable); addAvailabilityCheckStep("initially locally available", BeatmapAvailability.LocallyAvailable);
@ -202,7 +201,7 @@ namespace osu.Game.Tests.Online
private class TestBeatmapManager : BeatmapManager private class TestBeatmapManager : BeatmapManager
{ {
public TaskCompletionSource<bool> AllowImport = new TaskCompletionSource<bool>(); public readonly ManualResetEventSlim AllowImport = new ManualResetEventSlim();
public Live<BeatmapSetInfo> CurrentImport { get; private set; } public Live<BeatmapSetInfo> CurrentImport { get; private set; }
@ -229,7 +228,9 @@ namespace osu.Game.Tests.Online
public override Live<BeatmapSetInfo> ImportModel(BeatmapSetInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default) public override Live<BeatmapSetInfo> ImportModel(BeatmapSetInfo item, ArchiveReader archive = null, bool batchImport = false, CancellationToken cancellationToken = default)
{ {
testBeatmapManager.AllowImport.Task.WaitSafely(); if (!testBeatmapManager.AllowImport.Wait(TimeSpan.FromSeconds(10), cancellationToken))
throw new TimeoutException("Timeout waiting for import to be allowed.");
return (testBeatmapManager.CurrentImport = base.ImportModel(item, archive, batchImport, cancellationToken)); return (testBeatmapManager.CurrentImport = base.ImportModel(item, archive, batchImport, cancellationToken));
} }
} }

View File

@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
addSeekStep(3000); addSeekStep(3000);
AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged)); AddAssert("all judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged));
AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.All(kc => kc.CountPresses >= 7)); AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Select(kc => kc.CountPresses).Sum() == 15);
AddStep("clear results", () => Player.Results.Clear()); AddStep("clear results", () => Player.Results.Clear());
addSeekStep(0); addSeekStep(0);
AddAssert("none judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged)); AddAssert("none judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged));

View File

@ -11,8 +11,10 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Framework.Timing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -167,14 +169,39 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("add control points", () => addControlPoints(testControlPoints, Time.Current)); AddStep("add control points", () => addControlPoints(testControlPoints, Time.Current));
} }
private void addHitObject(double time) [Test]
public void TestVeryFlowScroll()
{
const double long_time_range = 100000;
var manualClock = new ManualClock();
AddStep("set manual clock", () =>
{
manualClock.CurrentTime = 0;
scrollContainers.ForEach(c => c.Clock = new FramedClock(manualClock));
setScrollAlgorithm(ScrollVisualisationMethod.Constant);
scrollContainers.ForEach(c => c.TimeRange = long_time_range);
});
AddStep("add hit objects", () =>
{
addHitObject(long_time_range);
addHitObject(long_time_range + 100, 250);
});
AddAssert("hit objects are alive", () => playfields.All(p => p.HitObjectContainer.AliveObjects.Count() == 2));
}
private void addHitObject(double time, float size = 75)
{ {
playfields.ForEach(p => playfields.ForEach(p =>
{ {
var hitObject = new TestDrawableHitObject(time); var hitObject = new TestHitObject(size) { StartTime = time };
setAnchor(hitObject, p); var drawable = new TestDrawableHitObject(hitObject);
p.Add(hitObject); setAnchor(drawable, p);
p.Add(drawable);
}); });
} }
@ -248,6 +275,8 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
}; };
} }
protected override ScrollingHitObjectContainer CreateScrollingHitObjectContainer() => new TestScrollingHitObjectContainer();
} }
private class TestDrawableControlPoint : DrawableHitObject<HitObject> private class TestDrawableControlPoint : DrawableHitObject<HitObject>
@ -281,22 +310,41 @@ namespace osu.Game.Tests.Visual.Gameplay
} }
} }
private class TestDrawableHitObject : DrawableHitObject<HitObject> private class TestHitObject : HitObject
{ {
public TestDrawableHitObject(double time) public readonly float Size;
: base(new HitObject { StartTime = time, HitWindows = HitWindows.Empty })
{
Origin = Anchor.Custom;
OriginPosition = new Vector2(75 / 4.0f);
AutoSizeAxes = Axes.Both; public TestHitObject(float size)
{
Size = size;
}
}
private class TestDrawableHitObject : DrawableHitObject<TestHitObject>
{
public TestDrawableHitObject(TestHitObject hitObject)
: base(hitObject)
{
Origin = Anchor.Centre;
Size = new Vector2(hitObject.Size);
AddInternal(new Box AddInternal(new Box
{ {
Size = new Vector2(75), RelativeSizeAxes = Axes.Both,
Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1) Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1)
}); });
} }
} }
private class TestScrollingHitObjectContainer : ScrollingHitObjectContainer
{
protected override RectangleF GetConservativeBoundingBox(HitObjectLifetimeEntry entry)
{
if (entry.HitObject is TestHitObject testObject)
return new RectangleF().Inflate(testObject.Size / 2);
return base.GetConservativeBoundingBox(entry);
}
}
} }
} }

View File

@ -66,7 +66,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
AddSliderStep("score", 0, 1000000, 500000, v => scoreProcessor.TotalScore.Value = v); AddSliderStep("score", 0, 1000000, 500000, v => scoreProcessor.TotalScore.Value = v);
AddSliderStep("accuracy", 0f, 1f, 0.5f, v => scoreProcessor.Accuracy.Value = v); AddSliderStep("accuracy", 0f, 1f, 0.5f, v => scoreProcessor.Accuracy.Value = v);
AddSliderStep("combo", 0, 1000, 0, v => scoreProcessor.Combo.Value = v); AddSliderStep("combo", 0, 10000, 0, v => scoreProcessor.HighestCombo.Value = v);
AddStep("toggle expanded", () => leaderboard.Expanded.Value = !leaderboard.Expanded.Value);
} }
[Test] [Test]

View File

@ -32,6 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected new OutroPlayer Player => (OutroPlayer)base.Player; protected new OutroPlayer Player => (OutroPlayer)base.Player;
private double currentBeatmapDuration;
private double currentStoryboardDuration; private double currentStoryboardDuration;
private bool showResults = true; private bool showResults = true;
@ -45,7 +46,8 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("enable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, true)); AddStep("enable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, true));
AddStep("set dim level to 0", () => LocalConfig.SetValue<double>(OsuSetting.DimLevel, 0)); AddStep("set dim level to 0", () => LocalConfig.SetValue<double>(OsuSetting.DimLevel, 0));
AddStep("reset fail conditions", () => currentFailConditions = (_, _) => false); AddStep("reset fail conditions", () => currentFailConditions = (_, _) => false);
AddStep("set storyboard duration to 2s", () => currentStoryboardDuration = 2000); AddStep("set beatmap duration to 0s", () => currentBeatmapDuration = 0);
AddStep("set storyboard duration to 8s", () => currentStoryboardDuration = 8000);
AddStep("set ShowResults = true", () => showResults = true); AddStep("set ShowResults = true", () => showResults = true);
} }
@ -151,6 +153,24 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("player exited", () => Stack.CurrentScreen == null); AddAssert("player exited", () => Stack.CurrentScreen == null);
} }
[Test]
public void TestPerformExitAfterOutro()
{
CreateTest(() =>
{
AddStep("set beatmap duration to 4s", () => currentBeatmapDuration = 4000);
AddStep("set storyboard duration to 1s", () => currentStoryboardDuration = 1000);
});
AddUntilStep("storyboard ends", () => Player.GameplayClockContainer.CurrentTime >= currentStoryboardDuration);
AddStep("exit via pause", () => Player.ExitViaPause());
AddAssert("player paused", () => !Player.IsResuming);
AddStep("resume player", () => Player.Resume());
AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value);
AddUntilStep("wait for score shown", () => Player.IsScoreShown);
}
protected override bool AllowFail => true; protected override bool AllowFail => true;
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
@ -160,7 +180,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{ {
var beatmap = new Beatmap(); var beatmap = new Beatmap();
beatmap.HitObjects.Add(new HitCircle()); beatmap.HitObjects.Add(new HitCircle { StartTime = currentBeatmapDuration });
return beatmap; return beatmap;
} }
@ -189,7 +209,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private event Func<HealthProcessor, JudgementResult, bool> failConditions; private event Func<HealthProcessor, JudgementResult, bool> failConditions;
public OutroPlayer(Func<HealthProcessor, JudgementResult, bool> failConditions, bool showResults = true) public OutroPlayer(Func<HealthProcessor, JudgementResult, bool> failConditions, bool showResults = true)
: base(false, showResults) : base(showResults: showResults)
{ {
this.failConditions = failConditions; this.failConditions = failConditions;
} }

View File

@ -9,8 +9,10 @@ using NUnit.Framework;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -92,6 +94,31 @@ namespace osu.Game.Tests.Visual.Navigation
returnToMenu(); returnToMenu();
} }
[Test]
public void TestFromSongSelectWithFilter([Values] ScorePresentType type)
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddStep("filter to nothing", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).FilterControl.CurrentTextSearch.Value = "fdsajkl;fgewq");
AddUntilStep("wait for no results", () => Beatmap.IsDefault);
var firstImport = importScore(1, new CatchRuleset().RulesetInfo);
presentAndConfirm(firstImport, type);
}
[Test]
public void TestFromSongSelectWithConvertRulesetChange([Values] ScorePresentType type)
{
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo.Invoke());
AddUntilStep("song select is current", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect && songSelect.BeatmapSetsLoaded);
AddStep("set convert to false", () => Game.LocalConfig.SetValue(OsuSetting.ShowConvertedBeatmaps, false));
var firstImport = importScore(1, new CatchRuleset().RulesetInfo);
presentAndConfirm(firstImport, type);
}
[Test] [Test]
public void TestFromSongSelect([Values] ScorePresentType type) public void TestFromSongSelect([Values] ScorePresentType type)
{ {

View File

@ -89,6 +89,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString Collections => new TranslatableString(getKey(@"collections"), @"Collections"); public static LocalisableString Collections => new TranslatableString(getKey(@"collections"), @"Collections");
/// <summary>
/// "Mod presets"
/// </summary>
public static LocalisableString ModPresets => new TranslatableString(getKey(@"mod_presets"), @"Mod presets");
/// <summary> /// <summary>
/// "Name" /// "Name"
/// </summary> /// </summary>

View File

@ -44,11 +44,6 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString ClearAllCaches => new TranslatableString(getKey(@"clear_all_caches"), @"Clear all caches"); public static LocalisableString ClearAllCaches => new TranslatableString(getKey(@"clear_all_caches"), @"Clear all caches");
/// <summary>
/// "Compact realm"
/// </summary>
public static LocalisableString CompactRealm => new TranslatableString(getKey(@"compact_realm"), @"Compact realm");
private static string getKey(string key) => $"{prefix}:{key}"; private static string getKey(string key) => $"{prefix}:{key}";
} }
} }

View File

@ -64,6 +64,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString RunSetupWizard => new TranslatableString(getKey(@"run_setup_wizard"), @"Run setup wizard"); public static LocalisableString RunSetupWizard => new TranslatableString(getKey(@"run_setup_wizard"), @"Run setup wizard");
/// <summary>
/// "You are running the latest release ({0})"
/// </summary>
public static LocalisableString RunningLatestRelease(string version) => new TranslatableString(getKey(@"running_latest_release"), @"You are running the latest release ({0})", version);
private static string getKey(string key) => $"{prefix}:{key}"; private static string getKey(string key) => $"{prefix}:{key}";
} }
} }

View File

@ -19,6 +19,41 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString SelectDirectory => new TranslatableString(getKey(@"select_directory"), @"Select directory"); public static LocalisableString SelectDirectory => new TranslatableString(getKey(@"select_directory"), @"Select directory");
/// <summary>
/// "Migration in progress"
/// </summary>
public static LocalisableString MigrationInProgress => new TranslatableString(getKey(@"migration_in_progress"), @"Migration in progress");
/// <summary>
/// "This could take a few minutes depending on the speed of your disk(s)."
/// </summary>
public static LocalisableString MigrationDescription => new TranslatableString(getKey(@"migration_description"), @"This could take a few minutes depending on the speed of your disk(s).");
/// <summary>
/// "Please avoid interacting with the game!"
/// </summary>
public static LocalisableString ProhibitedInteractDuringMigration => new TranslatableString(getKey(@"prohibited_interact_during_migration"), @"Please avoid interacting with the game!");
/// <summary>
/// "Some files couldn't be cleaned up during migration. Clicking this notification will open the folder so you can manually clean things up."
/// </summary>
public static LocalisableString FailedCleanupNotification => new TranslatableString(getKey(@"failed_cleanup_notification"), @"Some files couldn't be cleaned up during migration. Clicking this notification will open the folder so you can manually clean things up.");
/// <summary>
/// "Please select a new location"
/// </summary>
public static LocalisableString SelectNewLocation => new TranslatableString(getKey(@"select_new_location"), @"Please select a new location");
/// <summary>
/// "The target directory already seems to have an osu! install. Use that data instead?"
/// </summary>
public static LocalisableString TargetDirectoryAlreadyInstalledOsu => new TranslatableString(getKey(@"target_directory_already_installed_osu"), @"The target directory already seems to have an osu! install. Use that data instead?");
/// <summary>
/// "To complete this operation, osu! will close. Please open it again to use the new data location."
/// </summary>
public static LocalisableString RestartAndReOpenRequiredForCompletion => new TranslatableString(getKey(@"restart_and_re_open_required_for_completion"), @"To complete this operation, osu! will close. Please open it again to use the new data location.");
/// <summary> /// <summary>
/// "Import beatmaps from stable" /// "Import beatmaps from stable"
/// </summary> /// </summary>
@ -84,6 +119,26 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString RestoreAllRecentlyDeletedModPresets => new TranslatableString(getKey(@"restore_all_recently_deleted_mod_presets"), @"Restore all recently deleted mod presets"); public static LocalisableString RestoreAllRecentlyDeletedModPresets => new TranslatableString(getKey(@"restore_all_recently_deleted_mod_presets"), @"Restore all recently deleted mod presets");
/// <summary>
/// "Deleted all collections!"
/// </summary>
public static LocalisableString DeletedAllCollections => new TranslatableString(getKey(@"deleted_all_collections"), @"Deleted all collections!");
/// <summary>
/// "Deleted all mod presets!"
/// </summary>
public static LocalisableString DeletedAllModPresets => new TranslatableString(getKey(@"deleted_all_mod_presets"), @"Deleted all mod presets!");
/// <summary>
/// "Restored all deleted mod presets!"
/// </summary>
public static LocalisableString RestoredAllDeletedModPresets => new TranslatableString(getKey(@"restored_all_deleted_mod_presets"), @"Restored all deleted mod presets!");
/// <summary>
/// "Please select your osu!stable install location"
/// </summary>
public static LocalisableString StableDirectorySelectHeader => new TranslatableString(getKey(@"stable_directory_select_header"), @"Please select your osu!stable install location");
private static string getKey(string key) => $"{prefix}:{key}"; private static string getKey(string key) => $"{prefix}:{key}";
} }
} }

View File

@ -19,6 +19,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString NoTabletDetected => new TranslatableString(getKey(@"no_tablet_detected"), @"No tablet detected!"); public static LocalisableString NoTabletDetected => new TranslatableString(getKey(@"no_tablet_detected"), @"No tablet detected!");
/// <summary>
/// "If your tablet is not detected, please read [this FAQ]({0}) for troubleshooting steps."
/// </summary>
public static LocalisableString NoTabletDetectedDescription(string url) => new TranslatableString(getKey(@"no_tablet_detected_description"), @"If your tablet is not detected, please read [this FAQ]({0}) for troubleshooting steps.", url);
/// <summary> /// <summary>
/// "Reset to full area" /// "Reset to full area"
/// </summary> /// </summary>

View File

@ -1,9 +1,9 @@
// 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. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics; using System.Diagnostics;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.SignalR.Client; using Microsoft.AspNetCore.SignalR.Client;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -58,7 +58,7 @@ namespace osu.Game.Online.Spectator
{ {
await connection.InvokeAsync(nameof(ISpectatorServer.BeginPlaySession), state); await connection.InvokeAsync(nameof(ISpectatorServer.BeginPlaySession), state);
} }
catch (HubException exception) catch (Exception exception)
{ {
if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE) if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE)
{ {

View File

@ -561,9 +561,11 @@ namespace osu.Game
return; return;
} }
// This should be able to be performed from song select, but that is disabled for now
// due to the weird decoupled ruleset logic (which can cause a crash in certain filter scenarios).
PerformFromScreen(screen => PerformFromScreen(screen =>
{ {
Logger.Log($"{nameof(PresentScore)} updating beatmap ({databasedBeatmap}) and ruleset ({databasedScore.ScoreInfo.Ruleset} to match score"); Logger.Log($"{nameof(PresentScore)} updating beatmap ({databasedBeatmap}) and ruleset ({databasedScore.ScoreInfo.Ruleset}) to match score");
Ruleset.Value = databasedScore.ScoreInfo.Ruleset; Ruleset.Value = databasedScore.ScoreInfo.Ruleset;
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap); Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap);
@ -578,7 +580,7 @@ namespace osu.Game
screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo, false)); screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo, false));
break; break;
} }
}, validScreens: new[] { typeof(PlaySongSelect) }); });
} }
public override Task Import(params ImportTask[] imports) public override Task Import(params ImportTask[] imports)

View File

@ -5,6 +5,7 @@
using System; using System;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Overlays.Dialog namespace osu.Game.Overlays.Dialog
@ -20,7 +21,7 @@ namespace osu.Game.Overlays.Dialog
/// <param name="message">The description of the action to be displayed to the user.</param> /// <param name="message">The description of the action to be displayed to the user.</param>
/// <param name="onConfirm">An action to perform on confirmation.</param> /// <param name="onConfirm">An action to perform on confirmation.</param>
/// <param name="onCancel">An optional action to perform on cancel.</param> /// <param name="onCancel">An optional action to perform on cancel.</param>
public ConfirmDialog(string message, Action onConfirm, Action onCancel = null) public ConfirmDialog(LocalisableString message, Action onConfirm, Action onCancel = null)
{ {
HeaderText = message; HeaderText = message;
BodyText = "Last chance to turn back"; BodyText = "Last chance to turn back";

View File

@ -35,7 +35,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings
}, },
new SettingsButton new SettingsButton
{ {
Text = DebugSettingsStrings.CompactRealm, Text = "Compact realm",
Action = () => Action = () =>
{ {
// Blocking operations implicitly causes a Compact(). // Blocking operations implicitly causes a Compact().

View File

@ -44,9 +44,12 @@ namespace osu.Game.Overlays.Settings.Sections.General
}, },
}; };
if (!LanguageExtensions.TryParseCultureCode(frameworkLocale.Value, out var locale)) frameworkLocale.BindValueChanged(locale =>
locale = Language.en; {
languageSelection.Current.Value = locale; if (!LanguageExtensions.TryParseCultureCode(locale.NewValue, out var language))
language = Language.en;
languageSelection.Current.Value = language;
}, true);
languageSelection.Current.BindValueChanged(val => frameworkLocale.Value = val.NewValue.ToCultureCode()); languageSelection.Current.BindValueChanged(val => frameworkLocale.Value = val.NewValue.ToCultureCode());
} }

View File

@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
{ {
notifications?.Post(new SimpleNotification notifications?.Post(new SimpleNotification
{ {
Text = $"You are running the latest release ({game.Version})", Text = GeneralSettingsStrings.RunningLatestRelease(game.Version),
Icon = FontAwesome.Solid.CheckCircle, Icon = FontAwesome.Solid.CheckCircle,
}); });
} }

View File

@ -73,8 +73,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
windowModes.BindTo(host.Window.SupportedWindowModes); windowModes.BindTo(host.Window.SupportedWindowModes);
} }
if (host.Window is WindowsWindow windowsWindow) if (host.Renderer is IWindowsRenderer windowsRenderer)
fullscreenCapability.BindTo(windowsWindow.FullscreenCapability); fullscreenCapability.BindTo(windowsRenderer.FullscreenCapability);
Children = new Drawable[] Children = new Drawable[]
{ {

View File

@ -72,7 +72,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours, LocalisationManager localisation)
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
@ -110,11 +110,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows || RuntimeInfo.OS == RuntimeInfo.Platform.Linux) if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows || RuntimeInfo.OS == RuntimeInfo.Platform.Linux)
{ {
t.NewLine(); t.NewLine();
t.AddText("If your tablet is not detected, please read "); var formattedSource = MessageFormatter.FormatText(localisation.GetLocalisedBindableString(TabletSettingsStrings.NoTabletDetectedDescription(RuntimeInfo.OS == RuntimeInfo.Platform.Windows
t.AddLink("this FAQ", LinkAction.External, RuntimeInfo.OS == RuntimeInfo.Platform.Windows
? @"https://opentabletdriver.net/Wiki/FAQ/Windows" ? @"https://opentabletdriver.net/Wiki/FAQ/Windows"
: @"https://opentabletdriver.net/Wiki/FAQ/Linux"); : @"https://opentabletdriver.net/Wiki/FAQ/Linux")).Value);
t.AddText(" for troubleshooting steps."); t.AddLinks(formattedSource.Text, formattedSource.Links);
} }
}), }),
} }

View File

@ -13,7 +13,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{ {
public class BeatmapSettings : SettingsSubsection public class BeatmapSettings : SettingsSubsection
{ {
protected override LocalisableString Header => "Beatmaps"; protected override LocalisableString Header => CommonStrings.Beatmaps;
private SettingsButton importBeatmapsButton = null!; private SettingsButton importBeatmapsButton = null!;
private SettingsButton deleteBeatmapsButton = null!; private SettingsButton deleteBeatmapsButton = null!;

View File

@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{ {
public class CollectionsSettings : SettingsSubsection public class CollectionsSettings : SettingsSubsection
{ {
protected override LocalisableString Header => "Collections"; protected override LocalisableString Header => CommonStrings.Collections;
private SettingsButton importCollectionsButton = null!; private SettingsButton importCollectionsButton = null!;
@ -51,7 +51,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
private void deleteAllCollections() private void deleteAllCollections()
{ {
realm.Write(r => r.RemoveAll<BeatmapCollection>()); realm.Write(r => r.RemoveAll<BeatmapCollection>());
notificationOverlay?.Post(new ProgressCompletionNotification { Text = "Deleted all collections!" }); notificationOverlay?.Post(new ProgressCompletionNotification { Text = MaintenanceSettingsStrings.DeletedAllCollections });
} }
} }
} }

View File

@ -15,6 +15,7 @@ using osu.Framework.Screens;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osu.Game.Screens; using osu.Game.Screens;
using osuTK; using osuTK;
@ -71,14 +72,14 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Text = "Migration in progress", Text = MaintenanceSettingsStrings.MigrationInProgress,
Font = OsuFont.Default.With(size: 40) Font = OsuFont.Default.With(size: 40)
}, },
new OsuSpriteText new OsuSpriteText
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Text = "This could take a few minutes depending on the speed of your disk(s).", Text = MaintenanceSettingsStrings.MigrationDescription,
Font = OsuFont.Default.With(size: 30) Font = OsuFont.Default.With(size: 30)
}, },
new LoadingSpinner(true) new LoadingSpinner(true)
@ -89,7 +90,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Text = "Please avoid interacting with the game!", Text = MaintenanceSettingsStrings.ProhibitedInteractDuringMigration,
Font = OsuFont.Default.With(size: 30) Font = OsuFont.Default.With(size: 30)
}, },
} }
@ -111,7 +112,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{ {
notifications.Post(new SimpleNotification notifications.Post(new SimpleNotification
{ {
Text = "Some files couldn't be cleaned up during migration. Clicking this notification will open the folder so you can manually clean things up.", Text = MaintenanceSettingsStrings.FailedCleanupNotification,
Activated = () => Activated = () =>
{ {
originalStorage.PresentExternally(); originalStorage.PresentExternally();

View File

@ -12,6 +12,7 @@ using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.Localisation;
using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Dialog;
namespace osu.Game.Overlays.Settings.Sections.Maintenance namespace osu.Game.Overlays.Settings.Sections.Maintenance
@ -35,7 +36,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
public override bool HideOverlaysOnEnter => true; public override bool HideOverlaysOnEnter => true;
public override LocalisableString HeaderText => "Please select a new location"; public override LocalisableString HeaderText => MaintenanceSettingsStrings.SelectNewLocation;
protected override void OnSelection(DirectoryInfo directory) protected override void OnSelection(DirectoryInfo directory)
{ {
@ -51,9 +52,9 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
// Quick test for whether there's already an osu! install at the target path. // Quick test for whether there's already an osu! install at the target path.
if (fileInfos.Any(f => f.Name == OsuGameBase.CLIENT_DATABASE_FILENAME)) if (fileInfos.Any(f => f.Name == OsuGameBase.CLIENT_DATABASE_FILENAME))
{ {
dialogOverlay.Push(new ConfirmDialog("The target directory already seems to have an osu! install. Use that data instead?", () => dialogOverlay.Push(new ConfirmDialog(MaintenanceSettingsStrings.TargetDirectoryAlreadyInstalledOsu, () =>
{ {
dialogOverlay.Push(new ConfirmDialog("To complete this operation, osu! will close. Please open it again to use the new data location.", () => dialogOverlay.Push(new ConfirmDialog(MaintenanceSettingsStrings.RestartAndReOpenRequiredForCompletion, () =>
{ {
(storage as OsuStorage)?.ChangeDataPath(target.FullName); (storage as OsuStorage)?.ChangeDataPath(target.FullName);
game.Exit(); game.Exit();

View File

@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{ {
public class ModPresetSettings : SettingsSubsection public class ModPresetSettings : SettingsSubsection
{ {
protected override LocalisableString Header => "Mod presets"; protected override LocalisableString Header => CommonStrings.ModPresets;
[Resolved] [Resolved]
private RealmAccess realm { get; set; } = null!; private RealmAccess realm { get; set; } = null!;
@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
deleteAllButton.Enabled.Value = true; deleteAllButton.Enabled.Value = true;
if (deletionTask.IsCompletedSuccessfully) if (deletionTask.IsCompletedSuccessfully)
notificationOverlay?.Post(new ProgressCompletionNotification { Text = "Deleted all mod presets!" }); notificationOverlay?.Post(new ProgressCompletionNotification { Text = MaintenanceSettingsStrings.DeletedAllModPresets });
else if (deletionTask.IsFaulted) else if (deletionTask.IsFaulted)
Logger.Error(deletionTask.Exception, "Failed to delete all mod presets"); Logger.Error(deletionTask.Exception, "Failed to delete all mod presets");
} }
@ -81,7 +81,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
undeleteButton.Enabled.Value = true; undeleteButton.Enabled.Value = true;
if (undeletionTask.IsCompletedSuccessfully) if (undeletionTask.IsCompletedSuccessfully)
notificationOverlay?.Post(new ProgressCompletionNotification { Text = "Restored all deleted mod presets!" }); notificationOverlay?.Post(new ProgressCompletionNotification { Text = MaintenanceSettingsStrings.RestoredAllDeletedModPresets });
else if (undeletionTask.IsFaulted) else if (undeletionTask.IsFaulted)
Logger.Error(undeletionTask.Exception, "Failed to restore mod presets"); Logger.Error(undeletionTask.Exception, "Failed to restore mod presets");
} }

View File

@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{ {
public class ScoreSettings : SettingsSubsection public class ScoreSettings : SettingsSubsection
{ {
protected override LocalisableString Header => "Scores"; protected override LocalisableString Header => CommonStrings.Scores;
private SettingsButton importScoresButton = null!; private SettingsButton importScoresButton = null!;
private SettingsButton deleteScoresButton = null!; private SettingsButton deleteScoresButton = null!;

View File

@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
{ {
public class SkinSettings : SettingsSubsection public class SkinSettings : SettingsSubsection
{ {
protected override LocalisableString Header => "Skins"; protected override LocalisableString Header => CommonStrings.Skins;
private SettingsButton importSkinsButton = null!; private SettingsButton importSkinsButton = null!;
private SettingsButton deleteSkinsButton = null!; private SettingsButton deleteSkinsButton = null!;

View File

@ -46,6 +46,10 @@ namespace osu.Game.Replays.Legacy
[IgnoreMember] [IgnoreMember]
public bool MouseRight2 => ButtonState.HasFlagFast(ReplayButtonState.Right2); public bool MouseRight2 => ButtonState.HasFlagFast(ReplayButtonState.Right2);
[JsonIgnore]
[IgnoreMember]
public bool Smoke => ButtonState.HasFlagFast(ReplayButtonState.Smoke);
[Key(3)] [Key(3)]
public ReplayButtonState ButtonState; public ReplayButtonState ButtonState;

View File

@ -5,10 +5,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Layout; using osu.Framework.Layout;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -127,6 +127,16 @@ namespace osu.Game.Rulesets.UI.Scrolling
private float scrollLength => scrollingAxis == Direction.Horizontal ? DrawWidth : DrawHeight; private float scrollLength => scrollingAxis == Direction.Horizontal ? DrawWidth : DrawHeight;
public override void Add(HitObjectLifetimeEntry entry)
{
// Scroll info is not available until loaded.
// The lifetime of all entries will be updated in the first Update.
if (IsLoaded)
setComputedLifetimeStart(entry);
base.Add(entry);
}
protected override void AddDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable) protected override void AddDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
{ {
base.AddDrawable(entry, drawable); base.AddDrawable(entry, drawable);
@ -145,7 +155,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
private void invalidateHitObject(DrawableHitObject hitObject) private void invalidateHitObject(DrawableHitObject hitObject)
{ {
hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject);
layoutComputed.Remove(hitObject); layoutComputed.Remove(hitObject);
} }
@ -157,10 +166,8 @@ namespace osu.Game.Rulesets.UI.Scrolling
layoutComputed.Clear(); layoutComputed.Clear();
// Reset lifetime to the conservative estimation.
// If a drawable becomes alive by this lifetime, its lifetime will be updated to a more precise lifetime in the next update.
foreach (var entry in Entries) foreach (var entry in Entries)
entry.SetInitialLifetime(); setComputedLifetimeStart(entry);
scrollingInfo.Algorithm.Reset(); scrollingInfo.Algorithm.Reset();
@ -187,38 +194,46 @@ namespace osu.Game.Rulesets.UI.Scrolling
} }
} }
private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject) /// <summary>
/// Get a conservative maximum bounding box of a <see cref="DrawableHitObject"/> corresponding to <paramref name="entry"/>.
/// It is used to calculate when the hit object appears.
/// </summary>
protected virtual RectangleF GetConservativeBoundingBox(HitObjectLifetimeEntry entry) => new RectangleF().Inflate(100);
private double computeDisplayStartTime(HitObjectLifetimeEntry entry)
{ {
// Origin position may be relative to the parent size RectangleF boundingBox = GetConservativeBoundingBox(entry);
Debug.Assert(hitObject.Parent != null); float startOffset = 0;
float originAdjustment = 0.0f;
// calculate the dimension of the part of the hitobject that should already be visible
// when the hitobject origin first appears inside the scrolling container
switch (direction.Value) switch (direction.Value)
{ {
case ScrollingDirection.Up: case ScrollingDirection.Right:
originAdjustment = hitObject.OriginPosition.Y; startOffset = boundingBox.Right;
break; break;
case ScrollingDirection.Down: case ScrollingDirection.Down:
originAdjustment = hitObject.DrawHeight - hitObject.OriginPosition.Y; startOffset = boundingBox.Bottom;
break; break;
case ScrollingDirection.Left: case ScrollingDirection.Left:
originAdjustment = hitObject.OriginPosition.X; startOffset = -boundingBox.Left;
break; break;
case ScrollingDirection.Right: case ScrollingDirection.Up:
originAdjustment = hitObject.DrawWidth - hitObject.OriginPosition.X; startOffset = -boundingBox.Top;
break; break;
} }
double computedStartTime = scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength); return scrollingInfo.Algorithm.GetDisplayStartTime(entry.HitObject.StartTime, startOffset, timeRange.Value, scrollLength);
}
private void setComputedLifetimeStart(HitObjectLifetimeEntry entry)
{
double computedStartTime = computeDisplayStartTime(entry);
// always load the hitobject before its first judgement offset // always load the hitobject before its first judgement offset
return Math.Min(hitObject.HitObject.StartTime - hitObject.MaximumJudgementOffset, computedStartTime); double judgementOffset = entry.HitObject.HitWindows?.WindowFor(Scoring.HitResult.Miss) ?? 0;
entry.LifetimeStart = Math.Min(entry.HitObject.StartTime - judgementOffset, computedStartTime);
} }
private void updateLayoutRecursive(DrawableHitObject hitObject) private void updateLayoutRecursive(DrawableHitObject hitObject)
@ -236,8 +251,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
{ {
updateLayoutRecursive(obj); updateLayoutRecursive(obj);
// Nested hitobjects don't need to scroll, but they do need accurate positions // Nested hitobjects don't need to scroll, but they do need accurate positions and start lifetime
updatePosition(obj, hitObject.HitObject.StartTime); updatePosition(obj, hitObject.HitObject.StartTime);
setComputedLifetimeStart(obj.Entry);
} }
} }

View File

@ -38,6 +38,8 @@ namespace osu.Game.Rulesets.UI.Scrolling
/// </summary> /// </summary>
public virtual Vector2 ScreenSpacePositionAtTime(double time) => HitObjectContainer.ScreenSpacePositionAtTime(time); public virtual Vector2 ScreenSpacePositionAtTime(double time) => HitObjectContainer.ScreenSpacePositionAtTime(time);
protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer(); protected sealed override HitObjectContainer CreateHitObjectContainer() => CreateScrollingHitObjectContainer();
protected virtual ScrollingHitObjectContainer CreateScrollingHitObjectContainer() => new ScrollingHitObjectContainer();
} }
} }

View File

@ -3,6 +3,7 @@
#nullable disable #nullable disable
using System;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -39,8 +40,6 @@ namespace osu.Game.Screens.Play.HUD
private const float rank_text_width = 35f; private const float rank_text_width = 35f;
private const float score_components_width = 85f;
private const float avatar_size = 25f; private const float avatar_size = 25f;
private const double panel_transition_duration = 500; private const double panel_transition_duration = 500;
@ -161,7 +160,7 @@ namespace osu.Game.Screens.Play.HUD
{ {
new Dimension(GridSizeMode.Absolute, rank_text_width), new Dimension(GridSizeMode.Absolute, rank_text_width),
new Dimension(), new Dimension(),
new Dimension(GridSizeMode.AutoSize, maxSize: score_components_width), new Dimension(GridSizeMode.AutoSize),
}, },
Content = new[] Content = new[]
{ {
@ -286,8 +285,19 @@ namespace osu.Game.Screens.Play.HUD
LoadComponentAsync(new DrawableAvatar(User), avatarContainer.Add); LoadComponentAsync(new DrawableAvatar(User), avatarContainer.Add);
TotalScore.BindValueChanged(v => scoreText.Text = v.NewValue.ToString("N0"), true); TotalScore.BindValueChanged(v => scoreText.Text = v.NewValue.ToString("N0"), true);
Accuracy.BindValueChanged(v => accuracyText.Text = v.NewValue.FormatAccuracy(), true);
Combo.BindValueChanged(v => comboText.Text = $"{v.NewValue}x", true); Accuracy.BindValueChanged(v =>
{
accuracyText.Text = v.NewValue.FormatAccuracy();
updateDetailsWidth();
}, true);
Combo.BindValueChanged(v =>
{
comboText.Text = $"{v.NewValue}x";
updateDetailsWidth();
}, true);
HasQuit.BindValueChanged(_ => updateState()); HasQuit.BindValueChanged(_ => updateState());
} }
@ -303,13 +313,10 @@ namespace osu.Game.Screens.Play.HUD
private void changeExpandedState(ValueChangedEvent<bool> expanded) private void changeExpandedState(ValueChangedEvent<bool> expanded)
{ {
scoreComponents.ClearTransforms();
if (expanded.NewValue) if (expanded.NewValue)
{ {
gridContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutQuint); gridContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutQuint);
scoreComponents.ResizeWidthTo(score_components_width, panel_transition_duration, Easing.OutQuint);
scoreComponents.FadeIn(panel_transition_duration, Easing.OutQuint); scoreComponents.FadeIn(panel_transition_duration, Easing.OutQuint);
usernameText.FadeIn(panel_transition_duration, Easing.OutQuint); usernameText.FadeIn(panel_transition_duration, Easing.OutQuint);
@ -318,11 +325,29 @@ namespace osu.Game.Screens.Play.HUD
{ {
gridContainer.ResizeWidthTo(compact_width, panel_transition_duration, Easing.OutQuint); gridContainer.ResizeWidthTo(compact_width, panel_transition_duration, Easing.OutQuint);
scoreComponents.ResizeWidthTo(0, panel_transition_duration, Easing.OutQuint);
scoreComponents.FadeOut(text_transition_duration, Easing.OutQuint); scoreComponents.FadeOut(text_transition_duration, Easing.OutQuint);
usernameText.FadeOut(text_transition_duration, Easing.OutQuint); usernameText.FadeOut(text_transition_duration, Easing.OutQuint);
} }
updateDetailsWidth();
}
private float? scoreComponentsTargetWidth;
private void updateDetailsWidth()
{
const float score_components_min_width = 88f;
float newWidth = Expanded.Value
? Math.Max(score_components_min_width, comboText.DrawWidth + accuracyText.DrawWidth + 25)
: 0;
if (scoreComponentsTargetWidth == newWidth)
return;
scoreComponentsTargetWidth = newWidth;
scoreComponents.ResizeWidthTo(newWidth, panel_transition_duration, Easing.OutQuint);
} }
private void updateState() private void updateState()

View File

@ -87,7 +87,7 @@ namespace osu.Game.Screens.Play.HUD
local.TotalScore.BindTarget = scoreProcessor.TotalScore; local.TotalScore.BindTarget = scoreProcessor.TotalScore;
local.Accuracy.BindTarget = scoreProcessor.Accuracy; local.Accuracy.BindTarget = scoreProcessor.Accuracy;
local.Combo.BindTarget = scoreProcessor.Combo; local.Combo.BindTarget = scoreProcessor.HighestCombo;
// Local score should always show lower than any existing scores in cases of ties. // Local score should always show lower than any existing scores in cases of ties.
local.DisplayOrder.Value = long.MaxValue; local.DisplayOrder.Value = long.MaxValue;

View File

@ -566,9 +566,6 @@ namespace osu.Game.Screens.Play
/// </param> /// </param>
protected void PerformExit(bool showDialogFirst) protected void PerformExit(bool showDialogFirst)
{ {
// if an exit has been requested, cancel any pending completion (the user has shown intention to exit).
resultsDisplayDelegate?.Cancel();
// there is a chance that an exit request occurs after the transition to results has already started. // there is a chance that an exit request occurs after the transition to results has already started.
// even in such a case, the user has shown intent, so forcefully return to this screen (to proceed with the upwards exit process). // even in such a case, the user has shown intent, so forcefully return to this screen (to proceed with the upwards exit process).
if (!this.IsCurrentScreen()) if (!this.IsCurrentScreen())
@ -603,6 +600,9 @@ namespace osu.Game.Screens.Play
} }
} }
// if an exit has been requested, cancel any pending completion (the user has shown intention to exit).
resultsDisplayDelegate?.Cancel();
// The actual exit is performed if // The actual exit is performed if
// - the pause / fail dialog was not requested // - the pause / fail dialog was not requested
// - the pause / fail dialog was requested but is already displayed (user showing intention to exit). // - the pause / fail dialog was requested but is already displayed (user showing intention to exit).
@ -780,19 +780,11 @@ namespace osu.Game.Screens.Play
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// A final display will only occur once all work is completed in <see cref="PrepareScoreForResultsAsync"/>. This means that even after calling this method, the results screen will never be shown until <see cref="JudgementProcessor.HasCompleted">ScoreProcessor.HasCompleted</see> becomes <see langword="true"/>. /// A final display will only occur once all work is completed in <see cref="PrepareScoreForResultsAsync"/>. This means that even after calling this method, the results screen will never be shown until <see cref="JudgementProcessor.HasCompleted">ScoreProcessor.HasCompleted</see> becomes <see langword="true"/>.
///
/// Calling this method multiple times will have no effect.
/// </remarks> /// </remarks>
/// <param name="withDelay">Whether a minimum delay (<see cref="RESULTS_DISPLAY_DELAY"/>) should be added before the screen is displayed.</param> /// <param name="withDelay">Whether a minimum delay (<see cref="RESULTS_DISPLAY_DELAY"/>) should be added before the screen is displayed.</param>
private void progressToResults(bool withDelay) private void progressToResults(bool withDelay)
{ {
if (resultsDisplayDelegate != null) resultsDisplayDelegate?.Cancel();
// Note that if progressToResults is called one withDelay=true and then withDelay=false, this no-delay timing will not be
// accounted for. shouldn't be a huge concern (a user pressing the skip button after a results progression has already been queued
// may take x00 more milliseconds than expected in the very rare edge case).
//
// If required we can handle this more correctly by rescheduling here.
return;
double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0; double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0;

View File

@ -138,7 +138,8 @@ namespace osu.Game.Screens.Select
return false; return false;
} }
TriggerClick(); if (!e.Repeat)
TriggerClick();
return true; return true;
} }

View File

@ -261,8 +261,8 @@ namespace osu.Game.Screens.Utility
string exclusive = "unknown"; string exclusive = "unknown";
if (host.Window is WindowsWindow windowsWindow) if (host.Renderer is IWindowsRenderer windowsRenderer)
exclusive = windowsWindow.FullscreenCapability.ToString(); exclusive = windowsRenderer.FullscreenCapability.ToString();
statusText.Clear(); statusText.Clear();

View File

@ -35,8 +35,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="10.15.1" /> <PackageReference Include="Realm" Version="10.15.1" />
<PackageReference Include="ppy.osu.Framework" Version="2022.922.0" /> <PackageReference Include="ppy.osu.Framework" Version="2022.1005.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1003.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.1005.0" />
<PackageReference Include="Sentry" Version="3.20.1" /> <PackageReference Include="Sentry" Version="3.20.1" />
<PackageReference Include="SharpCompress" Version="0.32.2" /> <PackageReference Include="SharpCompress" Version="0.32.2" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />

View File

@ -61,8 +61,8 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.922.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.1005.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1003.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2022.1005.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
<PropertyGroup> <PropertyGroup>
@ -82,7 +82,7 @@
<PackageReference Include="DiffPlex" Version="1.7.1" /> <PackageReference Include="DiffPlex" Version="1.7.1" />
<PackageReference Include="Humanizer" Version="2.14.1" /> <PackageReference Include="Humanizer" Version="2.14.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2022.922.0" /> <PackageReference Include="ppy.osu.Framework" Version="2022.1005.0" />
<PackageReference Include="SharpCompress" Version="0.32.1" /> <PackageReference Include="SharpCompress" Version="0.32.1" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />