mirror of
https://github.com/ppy/osu.git
synced 2024-11-06 06:17:23 +08:00
Merge branch 'master' into adjust-flashlight
This commit is contained in:
commit
bce20e0a59
@ -51,8 +51,8 @@
|
||||
<Reference Include="Java.Interop" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1003.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.922.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1005.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.1005.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Dependencies">
|
||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||
|
@ -137,12 +137,13 @@ namespace osu.Desktop
|
||||
{
|
||||
base.SetHost(host);
|
||||
|
||||
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
|
||||
|
||||
var desktopWindow = (SDL2DesktopWindow)host.Window;
|
||||
|
||||
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
|
||||
if (iconStream != null)
|
||||
desktopWindow.SetIconFromStream(iconStream);
|
||||
|
||||
desktopWindow.CursorState |= CursorState.Hidden;
|
||||
desktopWindow.SetIconFromStream(iconStream);
|
||||
desktopWindow.Title = Name;
|
||||
desktopWindow.DragDrop += f => fileDrop(new[] { f });
|
||||
}
|
||||
|
@ -1,10 +1,15 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
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.Mods;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Tests
|
||||
@ -12,11 +17,11 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
[TestFixture]
|
||||
public class TestSceneCatchTouchInput : OsuTestScene
|
||||
{
|
||||
private CatchTouchInputMapper catchTouchInputMapper = null!;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
[Test]
|
||||
public void TestBasic()
|
||||
{
|
||||
CatchTouchInputMapper catchTouchInputMapper = null!;
|
||||
|
||||
AddStep("create input overlay", () =>
|
||||
{
|
||||
Child = new CatchInputManager(new CatchRuleset().RulesetInfo)
|
||||
@ -32,12 +37,30 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("show overlay", () => catchTouchInputMapper.Show());
|
||||
}
|
||||
|
||||
[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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Input;
|
||||
using osu.Game.Beatmaps;
|
||||
@ -36,7 +37,9 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
[BackgroundDependencyLoader]
|
||||
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);
|
||||
|
@ -54,10 +54,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateInitialTransforms()
|
||||
{
|
||||
}
|
||||
|
||||
protected override void UpdateStartTimeStateTransforms() => this.FadeOut(150);
|
||||
}
|
||||
}
|
||||
|
@ -30,20 +30,15 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
|
||||
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)
|
||||
{
|
||||
// suppress the base call explicitly.
|
||||
// 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).
|
||||
// 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
|
||||
|
@ -23,10 +23,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
|
||||
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)]
|
||||
private ManiaPlayfield playfield { get; set; }
|
||||
|
||||
|
Binary file not shown.
After Width: | Height: | Size: 251 B |
BIN
osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor-smoke.png
Normal file
BIN
osu.Game.Rulesets.Osu.Tests/Resources/old-skin/cursor-smoke.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
136
osu.Game.Rulesets.Osu.Tests/TestSceneSmoke.cs
Normal file
136
osu.Game.Rulesets.Osu.Tests/TestSceneSmoke.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -204,12 +204,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
// todo: temporary / arbitrary, used for lifetime optimisation.
|
||||
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)
|
||||
{
|
||||
default:
|
||||
ApproachCircle.FadeOut();
|
||||
break;
|
||||
|
||||
case ArmedState.Idle:
|
||||
HitArea.HitAction = null;
|
||||
break;
|
||||
|
@ -11,7 +11,9 @@ using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
@ -64,6 +66,23 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
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;
|
||||
|
||||
private OsuInputManager osuActionInputManager;
|
||||
|
@ -80,6 +80,9 @@ namespace osu.Game.Rulesets.Osu
|
||||
LeftButton,
|
||||
|
||||
[Description("Right button")]
|
||||
RightButton
|
||||
RightButton,
|
||||
|
||||
[Description("Smoke")]
|
||||
Smoke,
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +59,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
{
|
||||
new KeyBinding(InputKey.Z, OsuAction.LeftButton),
|
||||
new KeyBinding(InputKey.X, OsuAction.RightButton),
|
||||
new KeyBinding(InputKey.C, OsuAction.Smoke),
|
||||
new KeyBinding(InputKey.MouseLeft, OsuAction.LeftButton),
|
||||
new KeyBinding(InputKey.MouseRight, OsuAction.RightButton),
|
||||
};
|
||||
|
@ -21,6 +21,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
SliderBall,
|
||||
SliderBody,
|
||||
SpinnerBody,
|
||||
CursorSmoke,
|
||||
ApproachCircle,
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Osu.Replays
|
||||
Position = currentFrame.Position;
|
||||
if (currentFrame.MouseLeft) Actions.Add(OsuAction.LeftButton);
|
||||
if (currentFrame.MouseRight) Actions.Add(OsuAction.RightButton);
|
||||
if (currentFrame.Smoke) Actions.Add(OsuAction.Smoke);
|
||||
}
|
||||
|
||||
public LegacyReplayFrame ToLegacy(IBeatmap beatmap)
|
||||
@ -41,6 +42,8 @@ namespace osu.Game.Rulesets.Osu.Replays
|
||||
state |= ReplayButtonState.Left1;
|
||||
if (Actions.Contains(OsuAction.RightButton))
|
||||
state |= ReplayButtonState.Right1;
|
||||
if (Actions.Contains(OsuAction.Smoke))
|
||||
state |= ReplayButtonState.Smoke;
|
||||
|
||||
return new LegacyReplayFrame(Time, Position.X, Position.Y, state);
|
||||
}
|
||||
|
@ -1,20 +1,23 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Scoring
|
||||
{
|
||||
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 =
|
||||
{
|
||||
new DifficultyRange(HitResult.Great, 80, 50, 20),
|
||||
new DifficultyRange(HitResult.Ok, 140, 100, 60),
|
||||
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)
|
||||
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
19
osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySmokeSegment.cs
Normal file
19
osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySmokeSegment.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
@ -106,6 +106,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
return null;
|
||||
|
||||
case OsuSkinComponents.CursorSmoke:
|
||||
if (GetTexture("cursor-smoke") != null)
|
||||
return new LegacySmokeSegment();
|
||||
|
||||
return null;
|
||||
|
||||
case OsuSkinComponents.HitCircleText:
|
||||
if (!this.HasFont(LegacyFont.HitCircle))
|
||||
return null;
|
||||
|
366
osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
Normal file
366
osu.Game.Rulesets.Osu/Skinning/SmokeSegment.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -54,6 +54,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
|
||||
new SmokeContainer { RelativeSizeAxes = Axes.Both },
|
||||
spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both },
|
||||
FollowPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
|
||||
judgementLayer = new JudgementContainer<DrawableOsuJudgement> { RelativeSizeAxes = Axes.Both },
|
||||
|
77
osu.Game.Rulesets.Osu/UI/SmokeContainer.cs
Normal file
77
osu.Game.Rulesets.Osu/UI/SmokeContainer.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
private int countOk;
|
||||
private int countMeh;
|
||||
private int countMiss;
|
||||
private double accuracy;
|
||||
|
||||
private double effectiveMissCount;
|
||||
|
||||
@ -36,6 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||
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.
|
||||
if (totalSuccessfulHits > 0)
|
||||
@ -87,7 +89,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>))
|
||||
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)
|
||||
@ -95,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
if (attributes.GreatHitWindow <= 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));
|
||||
accuracyValue *= lengthBonus;
|
||||
@ -110,5 +112,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
||||
|
||||
private int totalSuccessfulHits => countGreat + countOk + countMeh;
|
||||
|
||||
private double customAccuracy => totalHits > 0 ? (countGreat * 300 + countOk * 150) / (totalHits * 300.0) : 0;
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@ -78,7 +77,7 @@ namespace osu.Game.Tests.Online
|
||||
}
|
||||
};
|
||||
|
||||
beatmaps.AllowImport = new TaskCompletionSource<bool>();
|
||||
beatmaps.AllowImport.Reset();
|
||||
|
||||
testBeatmapFile = TestResources.GetQuickTestBeatmapForImport();
|
||||
|
||||
@ -132,7 +131,7 @@ namespace osu.Game.Tests.Online
|
||||
AddStep("finish download", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet))!.TriggerSuccess(testBeatmapFile));
|
||||
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("ensure beatmap available", () => beatmaps.IsAvailableLocally(testBeatmapSet));
|
||||
addAvailabilityCheckStep("state is locally available", BeatmapAvailability.LocallyAvailable);
|
||||
@ -141,7 +140,7 @@ namespace osu.Game.Tests.Online
|
||||
[Test]
|
||||
public void TestTrackerRespectsSoftDeleting()
|
||||
{
|
||||
AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true));
|
||||
AddStep("allow importing", () => beatmaps.AllowImport.Set());
|
||||
AddStep("import beatmap", () => beatmaps.Import(testBeatmapFile).WaitSafely());
|
||||
addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable);
|
||||
|
||||
@ -155,7 +154,7 @@ namespace osu.Game.Tests.Online
|
||||
[Test]
|
||||
public void TestTrackerRespectsChecksum()
|
||||
{
|
||||
AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true));
|
||||
AddStep("allow importing", () => beatmaps.AllowImport.Set());
|
||||
AddStep("import beatmap", () => beatmaps.Import(testBeatmapFile).WaitSafely());
|
||||
addAvailabilityCheckStep("initially locally available", BeatmapAvailability.LocallyAvailable);
|
||||
|
||||
@ -202,7 +201,7 @@ namespace osu.Game.Tests.Online
|
||||
|
||||
private class TestBeatmapManager : BeatmapManager
|
||||
{
|
||||
public TaskCompletionSource<bool> AllowImport = new TaskCompletionSource<bool>();
|
||||
public readonly ManualResetEventSlim AllowImport = new ManualResetEventSlim();
|
||||
|
||||
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)
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
|
||||
addSeekStep(3000);
|
||||
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());
|
||||
addSeekStep(0);
|
||||
AddAssert("none judged", () => Player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged));
|
||||
|
@ -11,8 +11,10 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -167,14 +169,39 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
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 =>
|
||||
{
|
||||
var hitObject = new TestDrawableHitObject(time);
|
||||
setAnchor(hitObject, p);
|
||||
var hitObject = new TestHitObject(size) { StartTime = time };
|
||||
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>
|
||||
@ -281,22 +310,41 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
}
|
||||
}
|
||||
|
||||
private class TestDrawableHitObject : DrawableHitObject<HitObject>
|
||||
private class TestHitObject : HitObject
|
||||
{
|
||||
public TestDrawableHitObject(double time)
|
||||
: base(new HitObject { StartTime = time, HitWindows = HitWindows.Empty })
|
||||
{
|
||||
Origin = Anchor.Custom;
|
||||
OriginPosition = new Vector2(75 / 4.0f);
|
||||
public readonly float Size;
|
||||
|
||||
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
|
||||
{
|
||||
Size = new Vector2(75),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
AddSliderStep("score", 0, 1000000, 500000, v => scoreProcessor.TotalScore.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]
|
||||
|
@ -32,6 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
protected new OutroPlayer Player => (OutroPlayer)base.Player;
|
||||
|
||||
private double currentBeatmapDuration;
|
||||
private double currentStoryboardDuration;
|
||||
|
||||
private bool showResults = true;
|
||||
@ -45,7 +46,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddStep("enable storyboard", () => LocalConfig.SetValue(OsuSetting.ShowStoryboard, true));
|
||||
AddStep("set dim level to 0", () => LocalConfig.SetValue<double>(OsuSetting.DimLevel, 0));
|
||||
AddStep("reset fail conditions", () => currentFailConditions = (_, _) => false);
|
||||
AddStep("set storyboard duration to 2s", () => currentStoryboardDuration = 2000);
|
||||
AddStep("set beatmap duration to 0s", () => currentBeatmapDuration = 0);
|
||||
AddStep("set storyboard duration to 8s", () => currentStoryboardDuration = 8000);
|
||||
AddStep("set ShowResults = true", () => showResults = true);
|
||||
}
|
||||
|
||||
@ -151,6 +153,24 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
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 Ruleset CreatePlayerRuleset() => new OsuRuleset();
|
||||
@ -160,7 +180,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
|
||||
{
|
||||
var beatmap = new Beatmap();
|
||||
beatmap.HitObjects.Add(new HitCircle());
|
||||
beatmap.HitObjects.Add(new HitCircle { StartTime = currentBeatmapDuration });
|
||||
return beatmap;
|
||||
}
|
||||
|
||||
@ -189,7 +209,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
private event Func<HealthProcessor, JudgementResult, bool> failConditions;
|
||||
|
||||
public OutroPlayer(Func<HealthProcessor, JudgementResult, bool> failConditions, bool showResults = true)
|
||||
: base(false, showResults)
|
||||
: base(showResults: showResults)
|
||||
{
|
||||
this.failConditions = failConditions;
|
||||
}
|
||||
|
@ -9,8 +9,10 @@ using NUnit.Framework;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Scoring;
|
||||
@ -92,6 +94,31 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
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]
|
||||
public void TestFromSongSelect([Values] ScorePresentType type)
|
||||
{
|
||||
|
@ -89,6 +89,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
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>
|
||||
/// "Name"
|
||||
/// </summary>
|
||||
|
@ -44,11 +44,6 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
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}";
|
||||
}
|
||||
}
|
||||
|
@ -64,6 +64,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
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}";
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,41 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
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>
|
||||
/// "Import beatmaps from stable"
|
||||
/// </summary>
|
||||
@ -84,6 +119,26 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
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}";
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
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>
|
||||
/// "Reset to full area"
|
||||
/// </summary>
|
||||
|
@ -1,9 +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.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -58,7 +58,7 @@ namespace osu.Game.Online.Spectator
|
||||
{
|
||||
await connection.InvokeAsync(nameof(ISpectatorServer.BeginPlaySession), state);
|
||||
}
|
||||
catch (HubException exception)
|
||||
catch (Exception exception)
|
||||
{
|
||||
if (exception.GetHubExceptionMessage() == HubClientConnector.SERVER_SHUTDOWN_MESSAGE)
|
||||
{
|
||||
|
@ -561,9 +561,11 @@ namespace osu.Game
|
||||
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 =>
|
||||
{
|
||||
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;
|
||||
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap);
|
||||
@ -578,7 +580,7 @@ namespace osu.Game
|
||||
screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo, false));
|
||||
break;
|
||||
}
|
||||
}, validScreens: new[] { typeof(PlaySongSelect) });
|
||||
});
|
||||
}
|
||||
|
||||
public override Task Import(params ImportTask[] imports)
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
using System;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
|
||||
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="onConfirm">An action to perform on confirmation.</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;
|
||||
BodyText = "Last chance to turn back";
|
||||
|
@ -35,7 +35,7 @@ namespace osu.Game.Overlays.Settings.Sections.DebugSettings
|
||||
},
|
||||
new SettingsButton
|
||||
{
|
||||
Text = DebugSettingsStrings.CompactRealm,
|
||||
Text = "Compact realm",
|
||||
Action = () =>
|
||||
{
|
||||
// Blocking operations implicitly causes a Compact().
|
||||
|
@ -44,9 +44,12 @@ namespace osu.Game.Overlays.Settings.Sections.General
|
||||
},
|
||||
};
|
||||
|
||||
if (!LanguageExtensions.TryParseCultureCode(frameworkLocale.Value, out var locale))
|
||||
locale = Language.en;
|
||||
languageSelection.Current.Value = locale;
|
||||
frameworkLocale.BindValueChanged(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());
|
||||
}
|
||||
|
@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
|
||||
{
|
||||
notifications?.Post(new SimpleNotification
|
||||
{
|
||||
Text = $"You are running the latest release ({game.Version})",
|
||||
Text = GeneralSettingsStrings.RunningLatestRelease(game.Version),
|
||||
Icon = FontAwesome.Solid.CheckCircle,
|
||||
});
|
||||
}
|
||||
|
@ -73,8 +73,8 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
windowModes.BindTo(host.Window.SupportedWindowModes);
|
||||
}
|
||||
|
||||
if (host.Window is WindowsWindow windowsWindow)
|
||||
fullscreenCapability.BindTo(windowsWindow.FullscreenCapability);
|
||||
if (host.Renderer is IWindowsRenderer windowsRenderer)
|
||||
fullscreenCapability.BindTo(windowsRenderer.FullscreenCapability);
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
|
@ -72,7 +72,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
private void load(OsuColour colours, LocalisationManager localisation)
|
||||
{
|
||||
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)
|
||||
{
|
||||
t.NewLine();
|
||||
t.AddText("If your tablet is not detected, please read ");
|
||||
t.AddLink("this FAQ", LinkAction.External, RuntimeInfo.OS == RuntimeInfo.Platform.Windows
|
||||
var formattedSource = MessageFormatter.FormatText(localisation.GetLocalisedBindableString(TabletSettingsStrings.NoTabletDetectedDescription(RuntimeInfo.OS == RuntimeInfo.Platform.Windows
|
||||
? @"https://opentabletdriver.net/Wiki/FAQ/Windows"
|
||||
: @"https://opentabletdriver.net/Wiki/FAQ/Linux");
|
||||
t.AddText(" for troubleshooting steps.");
|
||||
: @"https://opentabletdriver.net/Wiki/FAQ/Linux")).Value);
|
||||
t.AddLinks(formattedSource.Text, formattedSource.Links);
|
||||
}
|
||||
}),
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
{
|
||||
public class BeatmapSettings : SettingsSubsection
|
||||
{
|
||||
protected override LocalisableString Header => "Beatmaps";
|
||||
protected override LocalisableString Header => CommonStrings.Beatmaps;
|
||||
|
||||
private SettingsButton importBeatmapsButton = null!;
|
||||
private SettingsButton deleteBeatmapsButton = null!;
|
||||
|
@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
{
|
||||
public class CollectionsSettings : SettingsSubsection
|
||||
{
|
||||
protected override LocalisableString Header => "Collections";
|
||||
protected override LocalisableString Header => CommonStrings.Collections;
|
||||
|
||||
private SettingsButton importCollectionsButton = null!;
|
||||
|
||||
@ -51,7 +51,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
private void deleteAllCollections()
|
||||
{
|
||||
realm.Write(r => r.RemoveAll<BeatmapCollection>());
|
||||
notificationOverlay?.Post(new ProgressCompletionNotification { Text = "Deleted all collections!" });
|
||||
notificationOverlay?.Post(new ProgressCompletionNotification { Text = MaintenanceSettingsStrings.DeletedAllCollections });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ using osu.Framework.Screens;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Screens;
|
||||
using osuTK;
|
||||
@ -71,14 +72,14 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = "Migration in progress",
|
||||
Text = MaintenanceSettingsStrings.MigrationInProgress,
|
||||
Font = OsuFont.Default.With(size: 40)
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = 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)
|
||||
},
|
||||
new LoadingSpinner(true)
|
||||
@ -89,7 +90,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Text = "Please avoid interacting with the game!",
|
||||
Text = MaintenanceSettingsStrings.ProhibitedInteractDuringMigration,
|
||||
Font = OsuFont.Default.With(size: 30)
|
||||
},
|
||||
}
|
||||
@ -111,7 +112,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
{
|
||||
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 = () =>
|
||||
{
|
||||
originalStorage.PresentExternally();
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Game.IO;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Overlays.Dialog;
|
||||
|
||||
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 LocalisableString HeaderText => "Please select a new location";
|
||||
public override LocalisableString HeaderText => MaintenanceSettingsStrings.SelectNewLocation;
|
||||
|
||||
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.
|
||||
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);
|
||||
game.Exit();
|
||||
|
@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
{
|
||||
public class ModPresetSettings : SettingsSubsection
|
||||
{
|
||||
protected override LocalisableString Header => "Mod presets";
|
||||
protected override LocalisableString Header => CommonStrings.ModPresets;
|
||||
|
||||
[Resolved]
|
||||
private RealmAccess realm { get; set; } = null!;
|
||||
@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
deleteAllButton.Enabled.Value = true;
|
||||
|
||||
if (deletionTask.IsCompletedSuccessfully)
|
||||
notificationOverlay?.Post(new ProgressCompletionNotification { Text = "Deleted all mod presets!" });
|
||||
notificationOverlay?.Post(new ProgressCompletionNotification { Text = MaintenanceSettingsStrings.DeletedAllModPresets });
|
||||
else if (deletionTask.IsFaulted)
|
||||
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;
|
||||
|
||||
if (undeletionTask.IsCompletedSuccessfully)
|
||||
notificationOverlay?.Post(new ProgressCompletionNotification { Text = "Restored all deleted mod presets!" });
|
||||
notificationOverlay?.Post(new ProgressCompletionNotification { Text = MaintenanceSettingsStrings.RestoredAllDeletedModPresets });
|
||||
else if (undeletionTask.IsFaulted)
|
||||
Logger.Error(undeletionTask.Exception, "Failed to restore mod presets");
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
{
|
||||
public class ScoreSettings : SettingsSubsection
|
||||
{
|
||||
protected override LocalisableString Header => "Scores";
|
||||
protected override LocalisableString Header => CommonStrings.Scores;
|
||||
|
||||
private SettingsButton importScoresButton = null!;
|
||||
private SettingsButton deleteScoresButton = null!;
|
||||
|
@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
|
||||
{
|
||||
public class SkinSettings : SettingsSubsection
|
||||
{
|
||||
protected override LocalisableString Header => "Skins";
|
||||
protected override LocalisableString Header => CommonStrings.Skins;
|
||||
|
||||
private SettingsButton importSkinsButton = null!;
|
||||
private SettingsButton deleteSkinsButton = null!;
|
||||
|
@ -46,6 +46,10 @@ namespace osu.Game.Replays.Legacy
|
||||
[IgnoreMember]
|
||||
public bool MouseRight2 => ButtonState.HasFlagFast(ReplayButtonState.Right2);
|
||||
|
||||
[JsonIgnore]
|
||||
[IgnoreMember]
|
||||
public bool Smoke => ButtonState.HasFlagFast(ReplayButtonState.Smoke);
|
||||
|
||||
[Key(3)]
|
||||
public ReplayButtonState ButtonState;
|
||||
|
||||
|
@ -5,10 +5,10 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
@ -127,6 +127,16 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
|
||||
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)
|
||||
{
|
||||
base.AddDrawable(entry, drawable);
|
||||
@ -145,7 +155,6 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
|
||||
private void invalidateHitObject(DrawableHitObject hitObject)
|
||||
{
|
||||
hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject);
|
||||
layoutComputed.Remove(hitObject);
|
||||
}
|
||||
|
||||
@ -157,10 +166,8 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
|
||||
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)
|
||||
entry.SetInitialLifetime();
|
||||
setComputedLifetimeStart(entry);
|
||||
|
||||
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
|
||||
Debug.Assert(hitObject.Parent != null);
|
||||
RectangleF boundingBox = GetConservativeBoundingBox(entry);
|
||||
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)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
originAdjustment = hitObject.OriginPosition.Y;
|
||||
case ScrollingDirection.Right:
|
||||
startOffset = boundingBox.Right;
|
||||
break;
|
||||
|
||||
case ScrollingDirection.Down:
|
||||
originAdjustment = hitObject.DrawHeight - hitObject.OriginPosition.Y;
|
||||
startOffset = boundingBox.Bottom;
|
||||
break;
|
||||
|
||||
case ScrollingDirection.Left:
|
||||
originAdjustment = hitObject.OriginPosition.X;
|
||||
startOffset = -boundingBox.Left;
|
||||
break;
|
||||
|
||||
case ScrollingDirection.Right:
|
||||
originAdjustment = hitObject.DrawWidth - hitObject.OriginPosition.X;
|
||||
case ScrollingDirection.Up:
|
||||
startOffset = -boundingBox.Top;
|
||||
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
|
||||
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)
|
||||
@ -236,8 +251,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
{
|
||||
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);
|
||||
setComputedLifetimeStart(obj.Entry);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -38,6 +38,8 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
@ -39,8 +40,6 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
private const float rank_text_width = 35f;
|
||||
|
||||
private const float score_components_width = 85f;
|
||||
|
||||
private const float avatar_size = 25f;
|
||||
|
||||
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(),
|
||||
new Dimension(GridSizeMode.AutoSize, maxSize: score_components_width),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
@ -286,8 +285,19 @@ namespace osu.Game.Screens.Play.HUD
|
||||
LoadComponentAsync(new DrawableAvatar(User), avatarContainer.Add);
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
@ -303,13 +313,10 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
private void changeExpandedState(ValueChangedEvent<bool> expanded)
|
||||
{
|
||||
scoreComponents.ClearTransforms();
|
||||
|
||||
if (expanded.NewValue)
|
||||
{
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
scoreComponents.ResizeWidthTo(0, panel_transition_duration, Easing.OutQuint);
|
||||
scoreComponents.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()
|
||||
|
@ -87,7 +87,7 @@ namespace osu.Game.Screens.Play.HUD
|
||||
|
||||
local.TotalScore.BindTarget = scoreProcessor.TotalScore;
|
||||
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.DisplayOrder.Value = long.MaxValue;
|
||||
|
@ -566,9 +566,6 @@ namespace osu.Game.Screens.Play
|
||||
/// </param>
|
||||
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.
|
||||
// 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())
|
||||
@ -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 pause / fail dialog was not requested
|
||||
// - 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>
|
||||
/// <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"/>.
|
||||
///
|
||||
/// Calling this method multiple times will have no effect.
|
||||
/// </remarks>
|
||||
/// <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)
|
||||
{
|
||||
if (resultsDisplayDelegate != null)
|
||||
// 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;
|
||||
resultsDisplayDelegate?.Cancel();
|
||||
|
||||
double delay = withDelay ? RESULTS_DISPLAY_DELAY : 0;
|
||||
|
||||
|
@ -138,7 +138,8 @@ namespace osu.Game.Screens.Select
|
||||
return false;
|
||||
}
|
||||
|
||||
TriggerClick();
|
||||
if (!e.Repeat)
|
||||
TriggerClick();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -261,8 +261,8 @@ namespace osu.Game.Screens.Utility
|
||||
|
||||
string exclusive = "unknown";
|
||||
|
||||
if (host.Window is WindowsWindow windowsWindow)
|
||||
exclusive = windowsWindow.FullscreenCapability.ToString();
|
||||
if (host.Renderer is IWindowsRenderer windowsRenderer)
|
||||
exclusive = windowsRenderer.FullscreenCapability.ToString();
|
||||
|
||||
statusText.Clear();
|
||||
|
||||
|
@ -35,8 +35,8 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Realm" Version="10.15.1" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2022.922.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1003.0" />
|
||||
<PackageReference Include="ppy.osu.Framework" Version="2022.1005.0" />
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.1005.0" />
|
||||
<PackageReference Include="Sentry" Version="3.20.1" />
|
||||
<PackageReference Include="SharpCompress" Version="0.32.2" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
|
@ -61,8 +61,8 @@
|
||||
<Reference Include="System.Net.Http" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.922.0" />
|
||||
<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.iOS" Version="2022.1005.0" />
|
||||
</ItemGroup>
|
||||
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
|
||||
<PropertyGroup>
|
||||
@ -82,7 +82,7 @@
|
||||
<PackageReference Include="DiffPlex" Version="1.7.1" />
|
||||
<PackageReference Include="Humanizer" Version="2.14.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="NUnit" Version="3.13.3" />
|
||||
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
|
||||
|
Loading…
Reference in New Issue
Block a user