1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 01:02:55 +08:00

Merge branch 'master' into pp-balancing

This commit is contained in:
Dan Balasescu 2022-08-11 19:46:16 +09:00
commit 61a3758cd9
158 changed files with 2118 additions and 790 deletions

View File

@ -4,8 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
@ -50,9 +48,6 @@ namespace osu.Game.Rulesets.Pippidon
new KeyBinding(InputKey.X, PippidonAction.Button2), new KeyBinding(InputKey.X, PippidonAction.Button2),
}; };
public override Drawable CreateIcon() => new Sprite public override Drawable CreateIcon() => new PippidonRulesetIcon(this);
{
Texture = new TextureStore(new TextureLoaderStore(CreateResourceStore()), false).Get("Textures/coin"),
};
} }
} }

View File

@ -0,0 +1,26 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
namespace osu.Game.Rulesets.Pippidon
{
public class PippidonRulesetIcon : Sprite
{
private readonly Ruleset ruleset;
public PippidonRulesetIcon(Ruleset ruleset)
{
this.ruleset = ruleset;
}
[BackgroundDependencyLoader]
private void load(IRenderer renderer)
{
Texture = new TextureStore(renderer, new TextureLoaderStore(ruleset.CreateResourceStore()), false).Get("Textures/coin");
}
}
}

View File

@ -4,8 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
@ -47,10 +45,6 @@ namespace osu.Game.Rulesets.Pippidon
new KeyBinding(InputKey.S, PippidonAction.MoveDown), new KeyBinding(InputKey.S, PippidonAction.MoveDown),
}; };
public override Drawable CreateIcon() => new Sprite public override Drawable CreateIcon() => new PippidonRulesetIcon(this);
{
Margin = new MarginPadding { Top = 3 },
Texture = new TextureStore(new TextureLoaderStore(CreateResourceStore()), false).Get("Textures/coin"),
};
} }
} }

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
namespace osu.Game.Rulesets.Pippidon
{
public class PippidonRulesetIcon : Sprite
{
private readonly Ruleset ruleset;
public PippidonRulesetIcon(Ruleset ruleset)
{
this.ruleset = ruleset;
Margin = new MarginPadding { Top = 3 };
}
[BackgroundDependencyLoader]
private void load(IRenderer renderer)
{
Texture = new TextureStore(renderer, new TextureLoaderStore(ruleset.CreateResourceStore()), false).Get("Textures/coin");
}
}
}

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.722.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.810.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.730.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.810.2" />
</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

@ -0,0 +1,52 @@
// 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.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
namespace osu.Game.Rulesets.Mania.Tests.Skinning
{
public class TestSceneBarLine : ManiaSkinnableTestScene
{
[Test]
public void TestMinor()
{
AddStep("Create barlines", () => recreate());
}
private void recreate(Func<IEnumerable<BarLine>>? createBarLines = null)
{
var stageDefinitions = new List<StageDefinition>
{
new StageDefinition { Columns = 4 },
};
SetContents(_ => new ManiaPlayfield(stageDefinitions).With(s =>
{
if (createBarLines != null)
{
var barLines = createBarLines();
foreach (var b in barLines)
s.Add(b);
return;
}
for (int i = 0; i < 64; i++)
{
s.Add(new BarLine
{
StartTime = Time.Current + i * 500,
Major = i % 4 == 0,
});
}
}));
}
}
}

View File

@ -1,12 +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.
#nullable disable
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Mania.Objects.Drawables namespace osu.Game.Rulesets.Mania.Objects.Drawables
{ {
@ -16,21 +13,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
/// </summary> /// </summary>
public class DrawableBarLine : DrawableManiaHitObject<BarLine> public class DrawableBarLine : DrawableManiaHitObject<BarLine>
{ {
/// <summary>
/// Height of major bar line triangles.
/// </summary>
private const float triangle_height = 12;
/// <summary>
/// Offset of the major bar line triangles from the sides of the bar line.
/// </summary>
private const float triangle_offset = 9;
public DrawableBarLine(BarLine barLine) public DrawableBarLine(BarLine barLine)
: base(barLine) : base(barLine)
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = 2f; Height = barLine.Major ? 1.7f : 1.2f;
AddInternal(new Box AddInternal(new Box
{ {
@ -38,34 +25,33 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
Anchor = Anchor.BottomCentre, Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = new Color4(255, 204, 33, 255), Alpha = barLine.Major ? 0.5f : 0.2f
}); });
if (barLine.Major) if (barLine.Major)
{ {
AddInternal(new EquilateralTriangle Vector2 size = new Vector2(22, 6);
const float line_offset = 4;
AddInternal(new Circle
{ {
Name = "Left triangle", Name = "Left line",
Anchor = Anchor.BottomLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.TopCentre, Origin = Anchor.CentreRight,
Size = new Vector2(triangle_height),
X = -triangle_offset, Size = size,
Rotation = 90 X = -line_offset,
}); });
AddInternal(new EquilateralTriangle AddInternal(new Circle
{ {
Name = "Right triangle", Name = "Right line",
Anchor = Anchor.BottomRight, Anchor = Anchor.CentreRight,
Origin = Anchor.TopCentre, Origin = Anchor.CentreLeft,
Size = new Vector2(triangle_height), Size = size,
X = triangle_offset, X = line_offset,
Rotation = -90
}); });
} }
if (!barLine.Major)
Alpha = 0.2f;
} }
protected override void UpdateInitialTransforms() protected override void UpdateInitialTransforms()

View File

@ -9,7 +9,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;

View File

@ -9,7 +9,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.UI.Scrolling;

View File

@ -6,7 +6,8 @@ using System.Diagnostics;
using System.Linq; using System.Linq;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Allocation;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -19,6 +20,9 @@ namespace osu.Game.Rulesets.Osu.Tests
[HeadlessTest] [HeadlessTest]
public class LegacyMainCirclePieceTest : OsuTestScene public class LegacyMainCirclePieceTest : OsuTestScene
{ {
[Resolved]
private IRenderer renderer { get; set; } = null!;
private static readonly object?[][] texture_priority_cases = private static readonly object?[][] texture_priority_cases =
{ {
// default priority lookup // default priority lookup
@ -76,7 +80,12 @@ namespace osu.Game.Rulesets.Osu.Tests
skin.Setup(s => s.GetTexture(It.IsAny<string>())).CallBase(); skin.Setup(s => s.GetTexture(It.IsAny<string>())).CallBase();
skin.Setup(s => s.GetTexture(It.IsIn(textureFilenames), It.IsAny<WrapMode>(), It.IsAny<WrapMode>())) skin.Setup(s => s.GetTexture(It.IsIn(textureFilenames), It.IsAny<WrapMode>(), It.IsAny<WrapMode>()))
.Returns((string componentName, WrapMode _, WrapMode _) => new Texture(1, 1) { AssetName = componentName }); .Returns((string componentName, WrapMode _, WrapMode _) =>
{
var tex = renderer.CreateTexture(1, 1);
tex.AssetName = componentName;
return tex;
});
Child = new DependencyProvidingContainer Child = new DependencyProvidingContainer
{ {
@ -84,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Tests
Child = piece = new TestLegacyMainCirclePiece(priorityLookup), Child = piece = new TestLegacyMainCirclePiece(priorityLookup),
}; };
var sprites = this.ChildrenOfType<Sprite>().Where(s => s.Texture.AssetName != null).DistinctBy(s => s.Texture.AssetName).ToArray(); var sprites = this.ChildrenOfType<Sprite>().Where(s => !string.IsNullOrEmpty(s.Texture.AssetName)).DistinctBy(s => s.Texture.AssetName).ToArray();
Debug.Assert(sprites.Length <= 2); Debug.Assert(sprites.Length <= 2);
}); });

View File

@ -11,7 +11,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing.Input; using osu.Framework.Testing.Input;
using osu.Game.Audio; using osu.Game.Audio;
@ -25,6 +25,9 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
public class TestSceneCursorTrail : OsuTestScene public class TestSceneCursorTrail : OsuTestScene
{ {
[Resolved]
private IRenderer renderer { get; set; }
[Test] [Test]
public void TestSmoothCursorTrail() public void TestSmoothCursorTrail()
{ {
@ -44,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
createTest(() => createTest(() =>
{ {
var skinContainer = new LegacySkinContainer(false); var skinContainer = new LegacySkinContainer(renderer, false);
var legacyCursorTrail = new LegacyCursorTrail(skinContainer); var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
skinContainer.Child = legacyCursorTrail; skinContainer.Child = legacyCursorTrail;
@ -58,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
createTest(() => createTest(() =>
{ {
var skinContainer = new LegacySkinContainer(true); var skinContainer = new LegacySkinContainer(renderer, true);
var legacyCursorTrail = new LegacyCursorTrail(skinContainer); var legacyCursorTrail = new LegacyCursorTrail(skinContainer);
skinContainer.Child = legacyCursorTrail; skinContainer.Child = legacyCursorTrail;
@ -82,10 +85,12 @@ namespace osu.Game.Rulesets.Osu.Tests
[Cached(typeof(ISkinSource))] [Cached(typeof(ISkinSource))]
private class LegacySkinContainer : Container, ISkinSource private class LegacySkinContainer : Container, ISkinSource
{ {
private readonly IRenderer renderer;
private readonly bool disjoint; private readonly bool disjoint;
public LegacySkinContainer(bool disjoint) public LegacySkinContainer(IRenderer renderer, bool disjoint)
{ {
this.renderer = renderer;
this.disjoint = disjoint; this.disjoint = disjoint;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
@ -98,14 +103,14 @@ namespace osu.Game.Rulesets.Osu.Tests
switch (componentName) switch (componentName)
{ {
case "cursortrail": case "cursortrail":
var tex = new Texture(Texture.WhitePixel.TextureGL); var tex = new Texture(renderer.WhitePixel);
if (disjoint) if (disjoint)
tex.ScaleAdjust = 1 / 25f; tex.ScaleAdjust = 1 / 25f;
return tex; return tex;
case "cursormiddle": case "cursormiddle":
return disjoint ? null : Texture.WhitePixel; return disjoint ? null : renderer.WhitePixel;
} }
return null; return null;

View File

@ -10,7 +10,6 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Input; using osu.Framework.Input;

View File

@ -12,7 +12,6 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Timing; using osu.Framework.Timing;

View File

@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2); velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2);
} }
if (osuLastObj.TravelTime != 0) if (osuLastObj.BaseObject is Slider)
{ {
// Reward sliders based on velocity. // Reward sliders based on velocity.
sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime; sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime;

View File

@ -15,11 +15,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
private const double max_opacity_bonus = 0.4; private const double max_opacity_bonus = 0.4;
private const double hidden_bonus = 0.2; private const double hidden_bonus = 0.2;
private const double min_velocity = 0.5;
private const double slider_multiplier = 1.3;
/// <summary> /// <summary>
/// Evaluates the difficulty of memorising and hitting an object, based on: /// Evaluates the difficulty of memorising and hitting an object, based on:
/// <list type="bullet"> /// <list type="bullet">
/// <item><description>distance between the previous and current object,</description></item> /// <item><description>distance between a number of previous objects and the current object,</description></item>
/// <item><description>the visual opacity of the current object,</description></item> /// <item><description>the visual opacity of the current object,</description></item>
/// <item><description>length and speed of the current object (for sliders),</description></item>
/// <item><description>and whether the hidden mod is enabled.</description></item> /// <item><description>and whether the hidden mod is enabled.</description></item>
/// </list> /// </list>
/// </summary> /// </summary>
@ -73,6 +77,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
if (hidden) if (hidden)
result *= 1.0 + hidden_bonus; result *= 1.0 + hidden_bonus;
double sliderBonus = 0.0;
if (osuCurrent.BaseObject is Slider osuSlider)
{
// Invert the scaling factor to determine the true travel distance independent of circle size.
double pixelTravelDistance = osuSlider.LazyTravelDistance / scalingFactor;
// Reward sliders based on velocity.
sliderBonus = Math.Pow(Math.Max(0.0, pixelTravelDistance / osuCurrent.TravelTime - min_velocity), 0.5);
// Longer sliders require more memorisation.
sliderBonus *= pixelTravelDistance;
// Nerf sliders with repeats, as less memorisation is required.
if (osuSlider.RepeatCount > 0)
sliderBonus /= (osuSlider.RepeatCount + 1);
}
result += sliderBonus * slider_multiplier;
return result; return result;
} }
} }

View File

@ -15,10 +15,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{ {
public class OsuDifficultyHitObject : DifficultyHitObject public class OsuDifficultyHitObject : DifficultyHitObject
{ {
private const int normalised_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths. /// <summary>
/// A distance by which all distances should be scaled in order to assume a uniform circle size.
/// </summary>
public const int NORMALISED_RADIUS = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
private const int min_delta_time = 25; private const int min_delta_time = 25;
private const float maximum_slider_radius = normalised_radius * 2.4f; private const float maximum_slider_radius = NORMALISED_RADIUS * 2.4f;
private const float assumed_slider_radius = normalised_radius * 1.8f; private const float assumed_slider_radius = NORMALISED_RADIUS * 1.8f;
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject; protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
@ -64,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
public double TravelDistance { get; private set; } public double TravelDistance { get; private set; }
/// <summary> /// <summary>
/// The time taken to travel through <see cref="TravelDistance"/>, with a minimum value of 25ms for a non-zero distance. /// The time taken to travel through <see cref="TravelDistance"/>, with a minimum value of 25ms for <see cref="Slider"/> objects.
/// </summary> /// </summary>
public double TravelTime { get; private set; } public double TravelTime { get; private set; }
@ -123,7 +127,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
if (BaseObject is Slider currentSlider) if (BaseObject is Slider currentSlider)
{ {
computeSliderCursorPosition(currentSlider); computeSliderCursorPosition(currentSlider);
TravelDistance = currentSlider.LazyTravelDistance; // Bonus for repeat sliders until a better per nested object strain system can be achieved.
TravelDistance = currentSlider.LazyTravelDistance * (float)Math.Pow(1 + currentSlider.RepeatCount / 2.5, 1.0 / 2.5);
TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time); TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time);
} }
@ -132,7 +137,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
return; return;
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps. // We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
float scalingFactor = normalised_radius / (float)BaseObject.Radius; float scalingFactor = NORMALISED_RADIUS / (float)BaseObject.Radius;
if (BaseObject.Radius < 30) if (BaseObject.Radius < 30)
{ {
@ -206,7 +211,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived. slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived.
var currCursorPosition = slider.StackedPosition; var currCursorPosition = slider.StackedPosition;
double scalingFactor = normalised_radius / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used. double scalingFactor = NORMALISED_RADIUS / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used.
for (int i = 1; i < slider.NestedHitObjects.Count; i++) for (int i = 1; i < slider.NestedHitObjects.Count; i++)
{ {
@ -234,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
else if (currMovementObj is SliderRepeat) else if (currMovementObj is SliderRepeat)
{ {
// For a slider repeat, assume a tighter movement threshold to better assess repeat sliders. // For a slider repeat, assume a tighter movement threshold to better assess repeat sliders.
requiredMovement = normalised_radius; requiredMovement = NORMALISED_RADIUS;
} }
if (currMovementLength > requiredMovement) if (currMovementLength > requiredMovement)
@ -248,8 +253,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
if (i == slider.NestedHitObjects.Count - 1) if (i == slider.NestedHitObjects.Count - 1)
slider.LazyEndPosition = currCursorPosition; slider.LazyEndPosition = currCursorPosition;
} }
slider.LazyTravelDistance *= (float)Math.Pow(1 + slider.RepeatCount / 2.5, 1.0 / 2.5); // Bonus for repeat sliders until a better per nested object strain system can be achieved.
} }
private Vector2 getEndCursorPosition(OsuHitObject hitObject) private Vector2 getEndCursorPosition(OsuHitObject hitObject)

View File

@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
connectionPool = new DrawablePool<FollowPointConnection>(1, 200), connectionPool = new DrawablePool<FollowPointConnection>(10, 200),
pointPool = new DrawablePool<FollowPoint>(50, 1000) pointPool = new DrawablePool<FollowPoint>(50, 1000)
}; };
} }

View File

@ -9,9 +9,9 @@ using System.Runtime.InteropServices;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Batches;
using osu.Framework.Graphics.OpenGL.Vertices;
using osu.Framework.Graphics.Primitives; 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.Shaders;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Input; using osu.Framework.Input;
@ -68,8 +68,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ShaderManager shaders) private void load(IRenderer renderer, ShaderManager shaders)
{ {
texture ??= renderer.WhitePixel;
shader = shaders.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE); shader = shaders.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE);
} }
@ -79,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
resetTime(); resetTime();
} }
private Texture texture = Texture.WhitePixel; private Texture texture;
public Texture Texture public Texture Texture
{ {
@ -222,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private Vector2 size; private Vector2 size;
private Vector2 originPosition; private Vector2 originPosition;
private readonly QuadBatch<TexturedTrailVertex> vertexBatch = new QuadBatch<TexturedTrailVertex>(max_sprites, 1); private IVertexBatch<TexturedTrailVertex> vertexBatch;
public TrailDrawNode(CursorTrail source) public TrailDrawNode(CursorTrail source)
: base(source) : base(source)
@ -254,15 +255,17 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Source.parts.CopyTo(parts, 0); Source.parts.CopyTo(parts, 0);
} }
public override void Draw(Action<TexturedVertex2D> vertexAction) public override void Draw(IRenderer renderer)
{ {
base.Draw(vertexAction); base.Draw(renderer);
vertexBatch ??= renderer.CreateQuadBatch<TexturedTrailVertex>(max_sprites, 1);
shader.Bind(); shader.Bind();
shader.GetUniform<float>("g_FadeClock").UpdateValue(ref time); shader.GetUniform<float>("g_FadeClock").UpdateValue(ref time);
shader.GetUniform<float>("g_FadeExponent").UpdateValue(ref fadeExponent); shader.GetUniform<float>("g_FadeExponent").UpdateValue(ref fadeExponent);
texture.TextureGL.Bind(); texture.Bind();
RectangleF textureRect = texture.GetTextureRect(); RectangleF textureRect = texture.GetTextureRect();
@ -319,7 +322,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
vertexBatch.Dispose(); vertexBatch?.Dispose();
} }
} }

View File

@ -11,9 +11,11 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Pooling;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
@ -112,21 +114,36 @@ namespace osu.Game.Rulesets.Osu.UI
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(OsuRulesetConfigManager config) private void load(OsuRulesetConfigManager config, IBeatmap beatmap)
{ {
config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle); config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle);
RegisterPool<HitCircle, DrawableHitCircle>(10, 100); var osuBeatmap = (OsuBeatmap)beatmap;
RegisterPool<Slider, DrawableSlider>(10, 100); RegisterPool<HitCircle, DrawableHitCircle>(20, 100);
RegisterPool<SliderHeadCircle, DrawableSliderHead>(10, 100);
RegisterPool<SliderTailCircle, DrawableSliderTail>(10, 100); // handle edge cases where a beatmap has a slider with many repeats.
RegisterPool<SliderTick, DrawableSliderTick>(10, 100); int maxRepeatsOnOneSlider = 0;
RegisterPool<SliderRepeat, DrawableSliderRepeat>(5, 50); int maxTicksOnOneSlider = 0;
if (osuBeatmap != null)
{
foreach (var slider in osuBeatmap.HitObjects.OfType<Slider>())
{
maxRepeatsOnOneSlider = Math.Max(maxRepeatsOnOneSlider, slider.RepeatCount);
maxTicksOnOneSlider = Math.Max(maxTicksOnOneSlider, slider.NestedHitObjects.OfType<SliderTick>().Count());
}
}
RegisterPool<Slider, DrawableSlider>(20, 100);
RegisterPool<SliderHeadCircle, DrawableSliderHead>(20, 100);
RegisterPool<SliderTailCircle, DrawableSliderTail>(20, 100);
RegisterPool<SliderTick, DrawableSliderTick>(Math.Max(maxTicksOnOneSlider, 20), Math.Max(maxTicksOnOneSlider, 200));
RegisterPool<SliderRepeat, DrawableSliderRepeat>(Math.Max(maxRepeatsOnOneSlider, 20), Math.Max(maxRepeatsOnOneSlider, 200));
RegisterPool<Spinner, DrawableSpinner>(2, 20); RegisterPool<Spinner, DrawableSpinner>(2, 20);
RegisterPool<SpinnerTick, DrawableSpinnerTick>(10, 100); RegisterPool<SpinnerTick, DrawableSpinnerTick>(10, 200);
RegisterPool<SpinnerBonusTick, DrawableSpinnerBonusTick>(10, 100); RegisterPool<SpinnerBonusTick, DrawableSpinnerBonusTick>(10, 200);
} }
protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject); protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject);
@ -173,7 +190,7 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly Action<DrawableOsuJudgement> onLoaded; private readonly Action<DrawableOsuJudgement> onLoaded;
public DrawableJudgementPool(HitResult result, Action<DrawableOsuJudgement> onLoaded) public DrawableJudgementPool(HitResult result, Action<DrawableOsuJudgement> onLoaded)
: base(10) : base(20)
{ {
this.result = result; this.result = result;
this.onLoaded = onLoaded; this.onLoaded = onLoaded;

View File

@ -6,8 +6,8 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK.Graphics; using osuTK.Graphics;

View File

@ -9,6 +9,7 @@ using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using Moq; using Moq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics.Rendering.Dummy;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
@ -55,7 +56,7 @@ namespace osu.Game.Tests.Editing.Checks
[Test] [Test]
public void TestAcceptable() public void TestAcceptable()
{ {
var context = getContext(new Texture(1920, 1080)); var context = getContext(new DummyRenderer().CreateTexture(1920, 1080));
Assert.That(check.Run(context), Is.Empty); Assert.That(check.Run(context), Is.Empty);
} }
@ -63,7 +64,7 @@ namespace osu.Game.Tests.Editing.Checks
[Test] [Test]
public void TestTooHighResolution() public void TestTooHighResolution()
{ {
var context = getContext(new Texture(3840, 2160)); var context = getContext(new DummyRenderer().CreateTexture(3840, 2160));
var issues = check.Run(context).ToList(); var issues = check.Run(context).ToList();
@ -74,7 +75,7 @@ namespace osu.Game.Tests.Editing.Checks
[Test] [Test]
public void TestLowResolution() public void TestLowResolution()
{ {
var context = getContext(new Texture(640, 480)); var context = getContext(new DummyRenderer().CreateTexture(640, 480));
var issues = check.Run(context).ToList(); var issues = check.Run(context).ToList();
@ -85,7 +86,7 @@ namespace osu.Game.Tests.Editing.Checks
[Test] [Test]
public void TestTooLowResolution() public void TestTooLowResolution()
{ {
var context = getContext(new Texture(100, 100)); var context = getContext(new DummyRenderer().CreateTexture(100, 100));
var issues = check.Run(context).ToList(); var issues = check.Run(context).ToList();
@ -96,7 +97,7 @@ namespace osu.Game.Tests.Editing.Checks
[Test] [Test]
public void TestTooUncompressed() public void TestTooUncompressed()
{ {
var context = getContext(new Texture(1920, 1080), new MemoryStream(new byte[1024 * 1024 * 3])); var context = getContext(new DummyRenderer().CreateTexture(1920, 1080), new MemoryStream(new byte[1024 * 1024 * 3]));
var issues = check.Run(context).ToList(); var issues = check.Run(context).ToList();
@ -107,7 +108,7 @@ namespace osu.Game.Tests.Editing.Checks
[Test] [Test]
public void TestStreamClosed() public void TestStreamClosed()
{ {
var background = new Texture(1920, 1080); var background = new DummyRenderer().CreateTexture(1920, 1080);
var stream = new Mock<MemoryStream>(new byte[1024 * 1024]); var stream = new Mock<MemoryStream>(new byte[1024 * 1024]);
var context = getContext(background, stream.Object); var context = getContext(background, stream.Object);

View File

@ -206,7 +206,7 @@ namespace osu.Game.Tests.Editing
} }
private void assertSnapDistance(float expectedDistance, HitObject hitObject = null) private void assertSnapDistance(float expectedDistance, HitObject hitObject = null)
=> AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(hitObject ?? new HitObject()) == expectedDistance); => AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(hitObject ?? new HitObject()), () => Is.EqualTo(expectedDistance));
private void assertDurationToDistance(double duration, float expectedDistance) private void assertDurationToDistance(double duration, float expectedDistance)
=> AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(new HitObject(), duration) == expectedDistance); => AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(new HitObject(), duration) == expectedDistance);

View File

@ -10,7 +10,6 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;

View File

@ -11,8 +11,10 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -36,6 +38,9 @@ namespace osu.Game.Tests.Gameplay
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } private OsuConfigManager config { get; set; }
[Resolved]
private GameHost host { get; set; }
[Test] [Test]
public void TestRetrieveTopLevelSample() public void TestRetrieveTopLevelSample()
{ {
@ -202,6 +207,7 @@ namespace osu.Game.Tests.Gameplay
#region IResourceStorageProvider #region IResourceStorageProvider
public IRenderer Renderer => host.Renderer;
public AudioManager AudioManager => Audio; public AudioManager AudioManager => Audio;
public IResourceStore<byte[]> Files => null; public IResourceStore<byte[]> Files => null;
public new IResourceStore<byte[]> Resources => base.Resources; public new IResourceStore<byte[]> Resources => base.Resources;

View File

@ -36,6 +36,23 @@ namespace osu.Game.Tests.NonVisual
Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single())); Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
} }
[Test]
public void TestAudioEqualityCaseSensitivity()
{
var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
// empty by default so let's set it..
beatmapSetA.Beatmaps.First().Metadata.AudioFile = "audio.mp3";
beatmapSetB.Beatmaps.First().Metadata.AudioFile = "audio.mp3";
addAudioFile(beatmapSetA, "abc", "AuDiO.mP3");
addAudioFile(beatmapSetB, "abc", "audio.mp3");
Assert.AreNotEqual(beatmapSetA, beatmapSetB);
Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
}
[Test] [Test]
public void TestAudioEqualitySameHash() public void TestAudioEqualitySameHash()
{ {

View File

@ -11,7 +11,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Animations;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
@ -27,6 +27,9 @@ namespace osu.Game.Tests.NonVisual.Skinning
private const string animation_name = "animation"; private const string animation_name = "animation";
private const int frame_count = 6; private const int frame_count = 6;
[Resolved]
private IRenderer renderer { get; set; }
[Cached(typeof(IAnimationTimeReference))] [Cached(typeof(IAnimationTimeReference))]
private TestAnimationTimeReference animationTimeReference = new TestAnimationTimeReference(); private TestAnimationTimeReference animationTimeReference = new TestAnimationTimeReference();
@ -35,9 +38,12 @@ namespace osu.Game.Tests.NonVisual.Skinning
[Test] [Test]
public void TestAnimationTimeReferenceChange() public void TestAnimationTimeReferenceChange()
{ {
ISkin skin = new TestSkin(); AddStep("get animation", () =>
{
ISkin skin = new TestSkin(renderer);
Add(animation = (TextureAnimation)skin.GetAnimation(animation_name, true, false));
});
AddStep("get animation", () => Add(animation = (TextureAnimation)skin.GetAnimation(animation_name, true, false)));
AddAssert("frame count correct", () => animation.FrameCount == frame_count); AddAssert("frame count correct", () => animation.FrameCount == frame_count);
assertPlaybackPosition(0); assertPlaybackPosition(0);
@ -55,9 +61,16 @@ namespace osu.Game.Tests.NonVisual.Skinning
{ {
private static readonly string[] lookup_names = Enumerable.Range(0, frame_count).Select(frame => $"{animation_name}-{frame}").ToArray(); private static readonly string[] lookup_names = Enumerable.Range(0, frame_count).Select(frame => $"{animation_name}-{frame}").ToArray();
private readonly IRenderer renderer;
public TestSkin(IRenderer renderer)
{
this.renderer = renderer;
}
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{ {
return lookup_names.Contains(componentName) ? Texture.WhitePixel : null; return lookup_names.Contains(componentName) ? renderer.WhitePixel : null;
} }
public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotSupportedException(); public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotSupportedException();

View File

@ -11,6 +11,8 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Dummy;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Game.Database; using osu.Game.Database;
@ -141,6 +143,7 @@ namespace osu.Game.Tests.NonVisual.Skinning
this.textureStore = textureStore; this.textureStore = textureStore;
} }
public IRenderer Renderer => new DummyRenderer();
public AudioManager AudioManager => null; public AudioManager AudioManager => null;
public IResourceStore<byte[]> Files => null; public IResourceStore<byte[]> Files => null;
public IResourceStore<byte[]> Resources => null; public IResourceStore<byte[]> Resources => null;

View File

@ -6,6 +6,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -148,6 +149,16 @@ namespace osu.Game.Tests.Online
Assert.That(apiMod.Settings["speed_change"], Is.EqualTo(1.01d)); Assert.That(apiMod.Settings["speed_change"], Is.EqualTo(1.01d));
} }
[Test]
public void TestSerialisedModSettingPresence()
{
var mod = new TestMod();
mod.TestSetting.Value = mod.TestSetting.Default;
JObject serialised = JObject.Parse(JsonConvert.SerializeObject(new APIMod(mod)));
Assert.False(serialised.ContainsKey("settings"));
}
private class TestRuleset : Ruleset private class TestRuleset : Ruleset
{ {
public override IEnumerable<Mod> GetModsFor(ModType type) => new Mod[] public override IEnumerable<Mod> GetModsFor(ModType type) => new Mod[]

View File

@ -15,11 +15,12 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
@ -77,9 +78,9 @@ namespace osu.Game.Tests.Rulesets
{ {
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.CacheAs<TextureStore>(ParentTextureStore = new TestTextureStore()); dependencies.CacheAs<TextureStore>(ParentTextureStore = new TestTextureStore(parent.Get<GameHost>().Renderer));
dependencies.CacheAs<ISampleStore>(ParentSampleStore = new TestSampleStore()); dependencies.CacheAs<ISampleStore>(ParentSampleStore = new TestSampleStore());
dependencies.CacheAs<ShaderManager>(ParentShaderManager = new TestShaderManager()); dependencies.CacheAs<ShaderManager>(ParentShaderManager = new TestShaderManager(parent.Get<GameHost>().Renderer));
return new DrawableRulesetDependencies(new OsuRuleset(), dependencies); return new DrawableRulesetDependencies(new OsuRuleset(), dependencies);
} }
@ -95,6 +96,11 @@ namespace osu.Game.Tests.Rulesets
private class TestTextureStore : TextureStore private class TestTextureStore : TextureStore
{ {
public TestTextureStore(IRenderer renderer)
: base(renderer)
{
}
public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => null; public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => null;
public bool IsDisposed { get; private set; } public bool IsDisposed { get; private set; }
@ -148,8 +154,8 @@ namespace osu.Game.Tests.Rulesets
private class TestShaderManager : ShaderManager private class TestShaderManager : ShaderManager
{ {
public TestShaderManager() public TestShaderManager(IRenderer renderer)
: base(new ResourceStore<byte[]>()) : base(renderer, new ResourceStore<byte[]>())
{ {
} }

View File

@ -10,7 +10,6 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;

View File

@ -7,7 +7,6 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Game.Audio; using osu.Game.Audio;

View File

@ -10,7 +10,6 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;

View File

@ -13,7 +13,6 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;

View File

@ -6,10 +6,11 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;
@ -21,6 +22,9 @@ namespace osu.Game.Tests.Skins
[HeadlessTest] [HeadlessTest]
public class TestSceneSkinProvidingContainer : OsuTestScene public class TestSceneSkinProvidingContainer : OsuTestScene
{ {
[Resolved]
private IRenderer renderer { get; set; }
/// <summary> /// <summary>
/// Ensures that the first inserted skin after resetting (via source change) /// Ensures that the first inserted skin after resetting (via source change)
/// is always prioritised over others when providing the same resource. /// is always prioritised over others when providing the same resource.
@ -35,7 +39,7 @@ namespace osu.Game.Tests.Skins
{ {
var sources = new List<TestSkin>(); var sources = new List<TestSkin>();
for (int i = 0; i < 10; i++) for (int i = 0; i < 10; i++)
sources.Add(new TestSkin()); sources.Add(new TestSkin(renderer));
mostPrioritisedSource = sources.First(); mostPrioritisedSource = sources.First();
@ -76,12 +80,19 @@ namespace osu.Game.Tests.Skins
{ {
public const string TEXTURE_NAME = "virtual-texture"; public const string TEXTURE_NAME = "virtual-texture";
private readonly IRenderer renderer;
public TestSkin(IRenderer renderer)
{
this.renderer = renderer;
}
public Drawable GetDrawableComponent(ISkinComponent component) => throw new System.NotImplementedException(); public Drawable GetDrawableComponent(ISkinComponent component) => throw new System.NotImplementedException();
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
{ {
if (componentName == TEXTURE_NAME) if (componentName == TEXTURE_NAME)
return Texture.WhitePixel; return renderer.WhitePixel;
return null; return null;
} }

View File

@ -12,7 +12,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Rendering.Dummy;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -55,6 +55,7 @@ namespace osu.Game.Tests.Skins
lookedUpFileNames = new List<string>(); lookedUpFileNames = new List<string>();
mockResourceProvider = new Mock<IStorageResourceProvider>(); mockResourceProvider = new Mock<IStorageResourceProvider>();
mockResourceProvider.Setup(m => m.AudioManager).Returns(Audio); mockResourceProvider.Setup(m => m.AudioManager).Returns(Audio);
mockResourceProvider.Setup(m => m.Renderer).Returns(new DummyRenderer());
mockResourceStore = new Mock<IResourceStore<byte[]>>(); mockResourceStore = new Mock<IResourceStore<byte[]>>();
mockResourceStore.Setup(r => r.Get(It.IsAny<string>())) mockResourceStore.Setup(r => r.Get(It.IsAny<string>()))
.Callback<string>(n => lookedUpFileNames.Add(n)) .Callback<string>(n => lookedUpFileNames.Add(n))

View File

@ -10,6 +10,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
@ -43,6 +44,9 @@ namespace osu.Game.Tests.Visual.Background
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } private OsuConfigManager config { get; set; }
[Resolved]
private IRenderer renderer { get; set; }
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {
@ -245,7 +249,7 @@ namespace osu.Game.Tests.Visual.Background
Id = API.LocalUser.Value.Id + 1, Id = API.LocalUser.Value.Id + 1,
}); });
private WorkingBeatmap createTestWorkingBeatmapWithUniqueBackground() => new UniqueBackgroundTestWorkingBeatmap(Audio); private WorkingBeatmap createTestWorkingBeatmapWithUniqueBackground() => new UniqueBackgroundTestWorkingBeatmap(renderer, Audio);
private WorkingBeatmap createTestWorkingBeatmapWithStoryboard() => new TestWorkingBeatmapWithStoryboard(Audio); private WorkingBeatmap createTestWorkingBeatmapWithStoryboard() => new TestWorkingBeatmapWithStoryboard(Audio);
private class TestBackgroundScreenDefault : BackgroundScreenDefault private class TestBackgroundScreenDefault : BackgroundScreenDefault
@ -274,12 +278,15 @@ namespace osu.Game.Tests.Visual.Background
private class UniqueBackgroundTestWorkingBeatmap : TestWorkingBeatmap private class UniqueBackgroundTestWorkingBeatmap : TestWorkingBeatmap
{ {
public UniqueBackgroundTestWorkingBeatmap(AudioManager audioManager) private readonly IRenderer renderer;
public UniqueBackgroundTestWorkingBeatmap(IRenderer renderer, AudioManager audioManager)
: base(new Beatmap(), null, audioManager) : base(new Beatmap(), null, audioManager)
{ {
this.renderer = renderer;
} }
protected override Texture GetBackground() => new Texture(1, 1); protected override Texture GetBackground() => renderer.CreateTexture(1, 1);
} }
private class TestWorkingBeatmapWithStoryboard : TestWorkingBeatmap private class TestWorkingBeatmapWithStoryboard : TestWorkingBeatmap

View File

@ -10,7 +10,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Backgrounds;
@ -28,11 +28,9 @@ namespace osu.Game.Tests.Visual.Background
[Resolved] [Resolved]
private SessionStatics statics { get; set; } private SessionStatics statics { get; set; }
[Cached(typeof(LargeTextureStore))]
private LookupLoggingTextureStore textureStore = new LookupLoggingTextureStore();
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
private LookupLoggingTextureStore textureStore;
private SeasonalBackgroundLoader backgroundLoader; private SeasonalBackgroundLoader backgroundLoader;
private Container backgroundContainer; private Container backgroundContainer;
@ -45,15 +43,32 @@ namespace osu.Game.Tests.Visual.Background
"Backgrounds/bg3" "Backgrounds/bg3"
}; };
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
textureStore = new LookupLoggingTextureStore(dependencies.Get<IRenderer>());
dependencies.CacheAs(typeof(LargeTextureStore), textureStore);
return dependencies;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(LargeTextureStore wrappedStore) private void load(LargeTextureStore wrappedStore)
{ {
textureStore.AddStore(wrappedStore); textureStore.AddStore(wrappedStore);
Add(backgroundContainer = new Container Child = new DependencyProvidingContainer
{
CachedDependencies = new (Type, object)[]
{
(typeof(LargeTextureStore), textureStore)
},
Child = backgroundContainer = new Container
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
}); }
};
} }
[SetUp] [SetUp]
@ -193,6 +208,11 @@ namespace osu.Game.Tests.Visual.Background
{ {
public List<string> PerformedLookups { get; } = new List<string>(); public List<string> PerformedLookups { get; } = new List<string>();
public LookupLoggingTextureStore(IRenderer renderer)
: base(renderer)
{
}
public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT)
{ {
PerformedLookups.Add(name); PerformedLookups.Add(name);

View File

@ -200,10 +200,12 @@ namespace osu.Game.Tests.Visual.Collections
AddStep("click confirmation", () => AddStep("click confirmation", () =>
{ {
InputManager.MoveMouseTo(dialogOverlay.CurrentDialog.ChildrenOfType<PopupDialogButton>().First()); InputManager.MoveMouseTo(dialogOverlay.CurrentDialog.ChildrenOfType<PopupDialogButton>().First());
InputManager.Click(MouseButton.Left); InputManager.PressButton(MouseButton.Left);
}); });
assertCollectionCount(0); assertCollectionCount(0);
AddStep("release mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
} }
[Test] [Test]

View File

@ -190,7 +190,7 @@ namespace osu.Game.Tests.Visual.Components
AddAssert("track stopped", () => !track.IsRunning); AddAssert("track stopped", () => !track.IsRunning);
} }
private TestPreviewTrackManager.TestPreviewTrack getTrack() => (TestPreviewTrackManager.TestPreviewTrack)trackManager.Get(null); private TestPreviewTrackManager.TestPreviewTrack getTrack() => (TestPreviewTrackManager.TestPreviewTrack)trackManager.Get(CreateAPIBeatmapSet());
private TestPreviewTrackManager.TestPreviewTrack getOwnedTrack() private TestPreviewTrackManager.TestPreviewTrack getOwnedTrack()
{ {

View File

@ -56,8 +56,10 @@ namespace osu.Game.Tests.Visual.Editing
[Test] [Test]
public void TestCreateNewBeatmap() public void TestCreateNewBeatmap()
{ {
AddAssert("status is none", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.None);
AddStep("save beatmap", () => Editor.Save()); AddStep("save beatmap", () => Editor.Save());
AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == currentBeatmapSetID)?.Value.DeletePending == false); AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == currentBeatmapSetID)?.Value.DeletePending == false);
AddAssert("status is modified", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified);
} }
[Test] [Test]
@ -208,6 +210,8 @@ namespace osu.Game.Tests.Visual.Editing
}); });
AddAssert("created difficulty has no objects", () => EditorBeatmap.HitObjects.Count == 0); AddAssert("created difficulty has no objects", () => EditorBeatmap.HitObjects.Count == 0);
AddAssert("status is modified", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified);
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName); AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = secondDifficultyName);
AddStep("save beatmap", () => Editor.Save()); AddStep("save beatmap", () => Editor.Save());
AddAssert("new beatmap persisted", () => AddAssert("new beatmap persisted", () =>
@ -218,7 +222,7 @@ namespace osu.Game.Tests.Visual.Editing
return beatmap != null return beatmap != null
&& beatmap.DifficultyName == secondDifficultyName && beatmap.DifficultyName == secondDifficultyName
&& set != null && set != null
&& set.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName)); && set.PerformRead(s => s.Beatmaps.Count == 2 && s.Beatmaps.Any(b => b.DifficultyName == secondDifficultyName) && s.Beatmaps.All(b => s.Status == BeatmapOnlineStatus.LocallyModified));
}); });
} }
@ -294,7 +298,7 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("approach rate correctly copied", () => EditorBeatmap.Difficulty.ApproachRate == 4); AddAssert("approach rate correctly copied", () => EditorBeatmap.Difficulty.ApproachRate == 4);
AddAssert("combo colours correctly copied", () => EditorBeatmap.BeatmapSkin.AsNonNull().ComboColours.Count == 2); AddAssert("combo colours correctly copied", () => EditorBeatmap.BeatmapSkin.AsNonNull().ComboColours.Count == 2);
AddAssert("status not copied", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.None); AddAssert("status is modified", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified);
AddAssert("online ID not copied", () => EditorBeatmap.BeatmapInfo.OnlineID == -1); AddAssert("online ID not copied", () => EditorBeatmap.BeatmapInfo.OnlineID == -1);
AddStep("save beatmap", () => Editor.Save()); AddStep("save beatmap", () => Editor.Save());

View File

@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Editing
{ {
AddStep("reset clock", () => Clock.Seek(0)); AddStep("reset clock", () => Clock.Seek(0));
AddStep("start clock", Clock.Start); AddStep("start clock", () => Clock.Start());
AddAssert("clock running", () => Clock.IsRunning); AddAssert("clock running", () => Clock.IsRunning);
AddStep("seek near end", () => Clock.Seek(Clock.TrackLength - 250)); AddStep("seek near end", () => Clock.Seek(Clock.TrackLength - 250));
@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength); AddAssert("clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength);
AddStep("start clock again", Clock.Start); AddStep("start clock again", () => Clock.Start());
AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500); AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500);
} }
@ -76,20 +76,20 @@ namespace osu.Game.Tests.Visual.Editing
{ {
AddStep("reset clock", () => Clock.Seek(0)); AddStep("reset clock", () => Clock.Seek(0));
AddStep("stop clock", Clock.Stop); AddStep("stop clock", () => Clock.Stop());
AddAssert("clock stopped", () => !Clock.IsRunning); AddAssert("clock stopped", () => !Clock.IsRunning);
AddStep("seek exactly to end", () => Clock.Seek(Clock.TrackLength)); AddStep("seek exactly to end", () => Clock.Seek(Clock.TrackLength));
AddAssert("clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength); AddAssert("clock stopped at end", () => Clock.CurrentTime == Clock.TrackLength);
AddStep("start clock again", Clock.Start); AddStep("start clock again", () => Clock.Start());
AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500); AddAssert("clock looped to start", () => Clock.IsRunning && Clock.CurrentTime < 500);
} }
[Test] [Test]
public void TestClampWhenSeekOutsideBeatmapBounds() public void TestClampWhenSeekOutsideBeatmapBounds()
{ {
AddStep("stop clock", Clock.Stop); AddStep("stop clock", () => Clock.Stop());
AddStep("seek before start time", () => Clock.Seek(-1000)); AddStep("seek before start time", () => Clock.Seek(-1000));
AddAssert("time is clamped to 0", () => Clock.CurrentTime == 0); AddAssert("time is clamped to 0", () => Clock.CurrentTime == 0);

View File

@ -60,17 +60,17 @@ namespace osu.Game.Tests.Visual.Editing
// Forwards // Forwards
AddStep("Seek(0)", () => Clock.Seek(0)); AddStep("Seek(0)", () => Clock.Seek(0));
AddAssert("Time = 0", () => Clock.CurrentTime == 0); checkTime(0);
AddStep("Seek(33)", () => Clock.Seek(33)); AddStep("Seek(33)", () => Clock.Seek(33));
AddAssert("Time = 33", () => Clock.CurrentTime == 33); checkTime(33);
AddStep("Seek(89)", () => Clock.Seek(89)); AddStep("Seek(89)", () => Clock.Seek(89));
AddAssert("Time = 89", () => Clock.CurrentTime == 89); checkTime(89);
// Backwards // Backwards
AddStep("Seek(25)", () => Clock.Seek(25)); AddStep("Seek(25)", () => Clock.Seek(25));
AddAssert("Time = 25", () => Clock.CurrentTime == 25); checkTime(25);
AddStep("Seek(0)", () => Clock.Seek(0)); AddStep("Seek(0)", () => Clock.Seek(0));
AddAssert("Time = 0", () => Clock.CurrentTime == 0); checkTime(0);
} }
/// <summary> /// <summary>
@ -83,19 +83,19 @@ namespace osu.Game.Tests.Visual.Editing
reset(); reset();
AddStep("Seek(0), Snap", () => Clock.SeekSnapped(0)); AddStep("Seek(0), Snap", () => Clock.SeekSnapped(0));
AddAssert("Time = 0", () => Clock.CurrentTime == 0); checkTime(0);
AddStep("Seek(50), Snap", () => Clock.SeekSnapped(50)); AddStep("Seek(50), Snap", () => Clock.SeekSnapped(50));
AddAssert("Time = 50", () => Clock.CurrentTime == 50); checkTime(50);
AddStep("Seek(100), Snap", () => Clock.SeekSnapped(100)); AddStep("Seek(100), Snap", () => Clock.SeekSnapped(100));
AddAssert("Time = 100", () => Clock.CurrentTime == 100); checkTime(100);
AddStep("Seek(175), Snap", () => Clock.SeekSnapped(175)); AddStep("Seek(175), Snap", () => Clock.SeekSnapped(175));
AddAssert("Time = 175", () => Clock.CurrentTime == 175); checkTime(175);
AddStep("Seek(350), Snap", () => Clock.SeekSnapped(350)); AddStep("Seek(350), Snap", () => Clock.SeekSnapped(350));
AddAssert("Time = 350", () => Clock.CurrentTime == 350); checkTime(350);
AddStep("Seek(400), Snap", () => Clock.SeekSnapped(400)); AddStep("Seek(400), Snap", () => Clock.SeekSnapped(400));
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
AddStep("Seek(450), Snap", () => Clock.SeekSnapped(450)); AddStep("Seek(450), Snap", () => Clock.SeekSnapped(450));
AddAssert("Time = 450", () => Clock.CurrentTime == 450); checkTime(450);
} }
/// <summary> /// <summary>
@ -108,17 +108,17 @@ namespace osu.Game.Tests.Visual.Editing
reset(); reset();
AddStep("Seek(24), Snap", () => Clock.SeekSnapped(24)); AddStep("Seek(24), Snap", () => Clock.SeekSnapped(24));
AddAssert("Time = 0", () => Clock.CurrentTime == 0); checkTime(0);
AddStep("Seek(26), Snap", () => Clock.SeekSnapped(26)); AddStep("Seek(26), Snap", () => Clock.SeekSnapped(26));
AddAssert("Time = 50", () => Clock.CurrentTime == 50); checkTime(50);
AddStep("Seek(150), Snap", () => Clock.SeekSnapped(150)); AddStep("Seek(150), Snap", () => Clock.SeekSnapped(150));
AddAssert("Time = 100", () => Clock.CurrentTime == 100); checkTime(100);
AddStep("Seek(170), Snap", () => Clock.SeekSnapped(170)); AddStep("Seek(170), Snap", () => Clock.SeekSnapped(170));
AddAssert("Time = 175", () => Clock.CurrentTime == 175); checkTime(175);
AddStep("Seek(274), Snap", () => Clock.SeekSnapped(274)); AddStep("Seek(274), Snap", () => Clock.SeekSnapped(274));
AddAssert("Time = 175", () => Clock.CurrentTime == 175); checkTime(175);
AddStep("Seek(276), Snap", () => Clock.SeekSnapped(276)); AddStep("Seek(276), Snap", () => Clock.SeekSnapped(276));
AddAssert("Time = 350", () => Clock.CurrentTime == 350); checkTime(350);
} }
/// <summary> /// <summary>
@ -130,15 +130,15 @@ namespace osu.Game.Tests.Visual.Editing
reset(); reset();
AddStep("SeekForward", () => Clock.SeekForward()); AddStep("SeekForward", () => Clock.SeekForward());
AddAssert("Time = 50", () => Clock.CurrentTime == 50); checkTime(50);
AddStep("SeekForward", () => Clock.SeekForward()); AddStep("SeekForward", () => Clock.SeekForward());
AddAssert("Time = 100", () => Clock.CurrentTime == 100); checkTime(100);
AddStep("SeekForward", () => Clock.SeekForward()); AddStep("SeekForward", () => Clock.SeekForward());
AddAssert("Time = 200", () => Clock.CurrentTime == 200); checkTime(200);
AddStep("SeekForward", () => Clock.SeekForward()); AddStep("SeekForward", () => Clock.SeekForward());
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
AddStep("SeekForward", () => Clock.SeekForward()); AddStep("SeekForward", () => Clock.SeekForward());
AddAssert("Time = 450", () => Clock.CurrentTime == 450); checkTime(450);
} }
/// <summary> /// <summary>
@ -150,17 +150,17 @@ namespace osu.Game.Tests.Visual.Editing
reset(); reset();
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 50", () => Clock.CurrentTime == 50); checkTime(50);
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 100", () => Clock.CurrentTime == 100); checkTime(100);
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 175", () => Clock.CurrentTime == 175); checkTime(175);
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 350", () => Clock.CurrentTime == 350); checkTime(350);
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 450", () => Clock.CurrentTime == 450); checkTime(450);
} }
/// <summary> /// <summary>
@ -174,28 +174,28 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Seek(49)", () => Clock.Seek(49)); AddStep("Seek(49)", () => Clock.Seek(49));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 50", () => Clock.CurrentTime == 50); checkTime(50);
AddStep("Seek(49.999)", () => Clock.Seek(49.999)); AddStep("Seek(49.999)", () => Clock.Seek(49.999));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 100", () => Clock.CurrentTime == 100); checkTime(100);
AddStep("Seek(99)", () => Clock.Seek(99)); AddStep("Seek(99)", () => Clock.Seek(99));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 100", () => Clock.CurrentTime == 100); checkTime(100);
AddStep("Seek(99.999)", () => Clock.Seek(99.999)); AddStep("Seek(99.999)", () => Clock.Seek(99.999));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 100", () => Clock.CurrentTime == 150); checkTime(150);
AddStep("Seek(174)", () => Clock.Seek(174)); AddStep("Seek(174)", () => Clock.Seek(174));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 175", () => Clock.CurrentTime == 175); checkTime(175);
AddStep("Seek(349)", () => Clock.Seek(349)); AddStep("Seek(349)", () => Clock.Seek(349));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 350", () => Clock.CurrentTime == 350); checkTime(350);
AddStep("Seek(399)", () => Clock.Seek(399)); AddStep("Seek(399)", () => Clock.Seek(399));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
AddStep("Seek(449)", () => Clock.Seek(449)); AddStep("Seek(449)", () => Clock.Seek(449));
AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true));
AddAssert("Time = 450", () => Clock.CurrentTime == 450); checkTime(450);
} }
/// <summary> /// <summary>
@ -208,15 +208,15 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Seek(450)", () => Clock.Seek(450)); AddStep("Seek(450)", () => Clock.Seek(450));
AddStep("SeekBackward", () => Clock.SeekBackward()); AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
AddStep("SeekBackward", () => Clock.SeekBackward()); AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 350", () => Clock.CurrentTime == 350); checkTime(350);
AddStep("SeekBackward", () => Clock.SeekBackward()); AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 150", () => Clock.CurrentTime == 150); checkTime(150);
AddStep("SeekBackward", () => Clock.SeekBackward()); AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 50", () => Clock.CurrentTime == 50); checkTime(50);
AddStep("SeekBackward", () => Clock.SeekBackward()); AddStep("SeekBackward", () => Clock.SeekBackward());
AddAssert("Time = 0", () => Clock.CurrentTime == 0); checkTime(0);
} }
/// <summary> /// <summary>
@ -229,17 +229,17 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Seek(450)", () => Clock.Seek(450)); AddStep("Seek(450)", () => Clock.Seek(450));
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 350", () => Clock.CurrentTime == 350); checkTime(350);
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 175", () => Clock.CurrentTime == 175); checkTime(175);
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 100", () => Clock.CurrentTime == 100); checkTime(100);
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 50", () => Clock.CurrentTime == 50); checkTime(50);
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 0", () => Clock.CurrentTime == 0); checkTime(0);
} }
/// <summary> /// <summary>
@ -253,16 +253,16 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Seek(451)", () => Clock.Seek(451)); AddStep("Seek(451)", () => Clock.Seek(451));
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 450", () => Clock.CurrentTime == 450); checkTime(450);
AddStep("Seek(450.999)", () => Clock.Seek(450.999)); AddStep("Seek(450.999)", () => Clock.Seek(450.999));
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 450", () => Clock.CurrentTime == 450); checkTime(450);
AddStep("Seek(401)", () => Clock.Seek(401)); AddStep("Seek(401)", () => Clock.Seek(401));
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
AddStep("Seek(401.999)", () => Clock.Seek(401.999)); AddStep("Seek(401.999)", () => Clock.Seek(401.999));
AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true)); AddStep("SeekBackward, Snap", () => Clock.SeekBackward(true));
AddAssert("Time = 400", () => Clock.CurrentTime == 400); checkTime(400);
} }
/// <summary> /// <summary>
@ -297,9 +297,11 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("Time < lastTime", () => Clock.CurrentTime < lastTime); AddAssert("Time < lastTime", () => Clock.CurrentTime < lastTime);
} }
AddAssert("Time = 0", () => Clock.CurrentTime == 0); checkTime(0);
} }
private void checkTime(double expectedTime) => AddAssert($"Current time is {expectedTime}", () => Clock.CurrentTime, () => Is.EqualTo(expectedTime));
private void reset() private void reset()
{ {
AddStep("Reset", () => Clock.Seek(0)); AddStep("Reset", () => Clock.Seek(0));

View File

@ -4,7 +4,6 @@
#nullable disable #nullable disable
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -120,7 +119,7 @@ namespace osu.Game.Tests.Visual.Editing
private void pressAndCheckTime(Key key, double expectedTime) private void pressAndCheckTime(Key key, double expectedTime)
{ {
AddStep($"press {key}", () => InputManager.Key(key)); AddStep($"press {key}", () => InputManager.Key(key));
AddUntilStep($"time is {expectedTime}", () => Precision.AlmostEquals(expectedTime, EditorClock.CurrentTime, 1)); AddUntilStep($"time is {expectedTime}", () => EditorClock.CurrentTime, () => Is.EqualTo(expectedTime).Within(1));
} }
} }
} }

View File

@ -13,7 +13,6 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Audio; using osu.Game.Audio;

View File

@ -13,7 +13,6 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;

View File

@ -0,0 +1,87 @@
// 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 NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Online.API;
using osu.Game.Overlays.Toolbar;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Menus
{
[TestFixture]
public class TestSceneToolbarUserButton : OsuManualInputManagerTestScene
{
public TestSceneToolbarUserButton()
{
Container mainContainer;
Children = new Drawable[]
{
mainContainer = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = Toolbar.HEIGHT,
Children = new Drawable[]
{
new Box
{
Colour = Color4.Black,
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new Box
{
Colour = Color4.DarkRed,
RelativeSizeAxes = Axes.Y,
Width = 2,
},
new ToolbarUserButton(),
new Box
{
Colour = Color4.DarkRed,
RelativeSizeAxes = Axes.Y,
Width = 2,
},
}
},
}
},
};
AddSliderStep("scale", 0.5, 4, 1, scale => mainContainer.Scale = new Vector2((float)scale));
}
[Test]
public void TestLoginLogout()
{
AddStep("Log out", () => ((DummyAPIAccess)API).Logout());
AddStep("Log in", () => ((DummyAPIAccess)API).Login("wang", "jang"));
}
[Test]
public void TestStates()
{
AddStep("Log in", () => ((DummyAPIAccess)API).Login("wang", "jang"));
foreach (var state in Enum.GetValues<APIState>())
{
AddStep($"Change state to {state}", () => ((DummyAPIAccess)API).SetState(state));
}
}
}
}

View File

@ -432,8 +432,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
var user = playingUsers.Single(u => u.UserID == userId); var user = playingUsers.Single(u => u.UserID == userId);
OnlinePlayDependencies.MultiplayerClient.RemoveUser(user.User.AsNonNull());
SpectatorClient.SendEndPlay(userId); SpectatorClient.SendEndPlay(userId);
OnlinePlayDependencies.MultiplayerClient.RemoveUser(user.User.AsNonNull());
playingUsers.Remove(user); playingUsers.Remove(user);
}); });

View File

@ -83,6 +83,20 @@ namespace osu.Game.Tests.Visual.Online
Beatmap = dummyBeatmap, Beatmap = dummyBeatmap,
}, },
new APIRecentActivity new APIRecentActivity
{
User = dummyUser,
Type = RecentActivityType.BeatmapsetApprove,
Approval = BeatmapApproval.Approved,
Beatmapset = dummyBeatmap,
},
new APIRecentActivity
{
User = dummyUser,
Type = RecentActivityType.BeatmapsetApprove,
Approval = BeatmapApproval.Loved,
Beatmapset = dummyBeatmap,
},
new APIRecentActivity
{ {
User = dummyUser, User = dummyUser,
Type = RecentActivityType.BeatmapsetApprove, Type = RecentActivityType.BeatmapsetApprove,
@ -90,6 +104,13 @@ namespace osu.Game.Tests.Visual.Online
Beatmapset = dummyBeatmap, Beatmapset = dummyBeatmap,
}, },
new APIRecentActivity new APIRecentActivity
{
User = dummyUser,
Type = RecentActivityType.BeatmapsetApprove,
Approval = BeatmapApproval.Ranked,
Beatmapset = dummyBeatmap,
},
new APIRecentActivity
{ {
User = dummyUser, User = dummyUser,
Type = RecentActivityType.BeatmapsetDelete, Type = RecentActivityType.BeatmapsetDelete,

View File

@ -11,9 +11,11 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Mods;
@ -35,16 +37,27 @@ namespace osu.Game.Tests.Visual.UserInterface
[Cached] [Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[Cached(typeof(IDialogOverlay))]
private readonly DialogOverlay dialogOverlay = new DialogOverlay();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(Realm); Dependencies.Cache(Realm);
base.Content.Add(content = new PopoverContainer base.Content.AddRange(new Drawable[]
{
new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = content = new PopoverContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(30), Padding = new MarginPadding(30),
}
},
dialogOverlay
}); });
} }
@ -121,6 +134,7 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestSoftDeleteSupport() public void TestSoftDeleteSupport()
{ {
AddStep("set osu! ruleset", () => Ruleset.Value = rulesets.GetRuleset(0)); AddStep("set osu! ruleset", () => Ruleset.Value = rulesets.GetRuleset(0));
AddStep("clear mods", () => SelectedMods.Value = Array.Empty<Mod>());
AddStep("create content", () => Child = new ModPresetColumn AddStep("create content", () => Child = new ModPresetColumn
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
@ -140,9 +154,11 @@ namespace osu.Game.Tests.Visual.UserInterface
foreach (var preset in r.All<ModPreset>()) foreach (var preset in r.All<ModPreset>())
preset.DeletePending = true; preset.DeletePending = true;
})); }));
AddUntilStep("no panels visible", () => this.ChildrenOfType<ModPresetPanel>().Count() == 0); AddUntilStep("no panels visible", () => !this.ChildrenOfType<ModPresetPanel>().Any());
AddStep("undelete preset", () => Realm.Write(r => AddStep("select mods from first preset", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHardRock() });
AddStep("undelete presets", () => Realm.Write(r =>
{ {
foreach (var preset in r.All<ModPreset>()) foreach (var preset in r.All<ModPreset>())
preset.DeletePending = false; preset.DeletePending = false;
@ -205,6 +221,46 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("popover closed", () => !this.ChildrenOfType<OsuPopover>().Any()); AddUntilStep("popover closed", () => !this.ChildrenOfType<OsuPopover>().Any());
} }
[Test]
public void TestDeleteFlow()
{
ModPresetColumn modPresetColumn = null!;
AddStep("create content", () => Child = modPresetColumn = new ModPresetColumn
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
AddUntilStep("items loaded", () => modPresetColumn.IsLoaded && modPresetColumn.ItemsLoaded);
AddStep("right click first panel", () =>
{
var panel = this.ChildrenOfType<ModPresetPanel>().First();
InputManager.MoveMouseTo(panel);
InputManager.Click(MouseButton.Right);
});
AddUntilStep("wait for context menu", () => this.ChildrenOfType<OsuContextMenu>().Any());
AddStep("click delete", () =>
{
var deleteItem = this.ChildrenOfType<DrawableOsuMenuItem>().Single();
InputManager.MoveMouseTo(deleteItem);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for dialog", () => dialogOverlay.CurrentDialog is DeleteModPresetDialog);
AddStep("hold confirm", () =>
{
var confirmButton = this.ChildrenOfType<PopupDialogDangerousButton>().Single();
InputManager.MoveMouseTo(confirmButton);
InputManager.PressButton(MouseButton.Left);
});
AddUntilStep("wait for dialog to close", () => dialogOverlay.CurrentDialog == null);
AddStep("release mouse", () => InputManager.ReleaseButton(MouseButton.Left));
AddUntilStep("preset deletion occurred", () => this.ChildrenOfType<ModPresetPanel>().Count() == 2);
AddAssert("preset soft-deleted", () => Realm.Run(r => r.All<ModPreset>().Count(preset => preset.DeletePending) == 1));
}
private ICollection<ModPreset> createTestPresets() => new[] private ICollection<ModPreset> createTestPresets() => new[]
{ {
new ModPreset new ModPreset

View File

@ -1,12 +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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
@ -23,6 +26,12 @@ namespace osu.Game.Tests.Visual.UserInterface
[Cached] [Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[SetUpSteps]
public void SetUpSteps()
{
AddStep("reset selected mods", () => SelectedMods.SetDefault());
}
[Test] [Test]
public void TestVariousModPresets() public void TestVariousModPresets()
{ {
@ -37,6 +46,78 @@ namespace osu.Game.Tests.Visual.UserInterface
}); });
} }
[Test]
public void TestPresetSelectionStateAfterExternalModChanges()
{
ModPresetPanel? panel = null;
AddStep("create panel", () => Child = panel = new ModPresetPanel(createTestPresets().First().ToLiveUnmanaged())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f
});
AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value);
AddStep("set mods to HR", () => SelectedMods.Value = new[] { new OsuModHardRock() });
AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value);
AddStep("set mods to DT", () => SelectedMods.Value = new[] { new OsuModDoubleTime() });
AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value);
AddStep("set mods to HR+DT", () => SelectedMods.Value = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() });
AddAssert("panel is active", () => panel.AsNonNull().Active.Value);
AddStep("set mods to HR+customised DT", () => SelectedMods.Value = new Mod[]
{
new OsuModHardRock(),
new OsuModDoubleTime
{
SpeedChange = { Value = 1.25 }
}
});
AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value);
AddStep("set mods to HR+DT", () => SelectedMods.Value = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() });
AddAssert("panel is active", () => panel.AsNonNull().Active.Value);
AddStep("customise mod in place", () => SelectedMods.Value.OfType<OsuModDoubleTime>().Single().SpeedChange.Value = 1.33);
AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value);
AddStep("set mods to HD+HR+DT", () => SelectedMods.Value = new Mod[] { new OsuModHidden(), new OsuModHardRock(), new OsuModDoubleTime() });
AddAssert("panel is not active", () => !panel.AsNonNull().Active.Value);
}
[Test]
public void TestActivatingPresetTogglesIncludedMods()
{
ModPresetPanel? panel = null;
AddStep("create panel", () => Child = panel = new ModPresetPanel(createTestPresets().First().ToLiveUnmanaged())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f
});
AddStep("activate panel", () => panel.AsNonNull().TriggerClick());
assertSelectedModsEquivalentTo(new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() });
AddStep("deactivate panel", () => panel.AsNonNull().TriggerClick());
assertSelectedModsEquivalentTo(Array.Empty<Mod>());
AddStep("set different mod", () => SelectedMods.Value = new[] { new OsuModHidden() });
AddStep("activate panel", () => panel.AsNonNull().TriggerClick());
assertSelectedModsEquivalentTo(new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() });
AddStep("set customised mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.25 } } });
AddStep("activate panel", () => panel.AsNonNull().TriggerClick());
assertSelectedModsEquivalentTo(new Mod[] { new OsuModHardRock(), new OsuModDoubleTime { SpeedChange = { Value = 1.5 } } });
}
private void assertSelectedModsEquivalentTo(IEnumerable<Mod> mods)
=> AddAssert("selected mods changed correctly", () => new HashSet<Mod>(SelectedMods.Value).SetEquals(mods));
private static IEnumerable<ModPreset> createTestPresets() => new[] private static IEnumerable<ModPreset> createTestPresets() => new[]
{ {
new ModPreset new ModPreset

View File

@ -137,7 +137,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("any column dimmed", () => this.ChildrenOfType<ModColumn>().Any(column => !column.Active.Value)); AddUntilStep("any column dimmed", () => this.ChildrenOfType<ModColumn>().Any(column => !column.Active.Value));
ModColumn lastColumn = null; ModSelectColumn lastColumn = null;
AddAssert("last column dimmed", () => !this.ChildrenOfType<ModColumn>().Last().Active.Value); AddAssert("last column dimmed", () => !this.ChildrenOfType<ModColumn>().Last().Active.Value);
AddStep("request scroll to last column", () => AddStep("request scroll to last column", () =>

View File

@ -105,5 +105,11 @@ namespace osu.Game.Audio
/// Retrieves the audio track. /// Retrieves the audio track.
/// </summary> /// </summary>
protected abstract Track? GetTrack(); protected abstract Track? GetTrack();
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
Track?.Dispose();
}
} }
} }

View File

@ -121,6 +121,7 @@ namespace osu.Game.Beatmaps
OnlineID = -1; OnlineID = -1;
LastOnlineUpdate = null; LastOnlineUpdate = null;
OnlineMD5Hash = string.Empty; OnlineMD5Hash = string.Empty;
if (Status != BeatmapOnlineStatus.LocallyModified)
Status = BeatmapOnlineStatus.None; Status = BeatmapOnlineStatus.None;
} }
@ -198,8 +199,8 @@ namespace osu.Game.Beatmaps
Debug.Assert(x.BeatmapSet != null); Debug.Assert(x.BeatmapSet != null);
Debug.Assert(y.BeatmapSet != null); Debug.Assert(y.BeatmapSet != null);
string? fileHashX = x.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(x.Metadata))?.File.Hash; string? fileHashX = x.BeatmapSet.GetFile(getFilename(x.Metadata))?.File.Hash;
string? fileHashY = y.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(y.Metadata))?.File.Hash; string? fileHashY = y.BeatmapSet.GetFile(getFilename(y.Metadata))?.File.Hash;
return fileHashX == fileHashY; return fileHashX == fileHashY;
} }

View File

@ -1,8 +1,6 @@
// 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 System.Linq; using System.Linq;
using osu.Framework.Localisation; using osu.Framework.Localisation;

View File

@ -300,7 +300,7 @@ namespace osu.Game.Beatmaps
stream.Seek(0, SeekOrigin.Begin); stream.Seek(0, SeekOrigin.Begin);
// AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity. // AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity.
var existingFileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)); var existingFileInfo = beatmapInfo.Path != null ? setInfo.GetFile(beatmapInfo.Path) : null;
string targetFilename = createBeatmapFilenameFromMetadata(beatmapInfo); string targetFilename = createBeatmapFilenameFromMetadata(beatmapInfo);
// ensure that two difficulties from the set don't point at the same beatmap file. // ensure that two difficulties from the set don't point at the same beatmap file.
@ -313,9 +313,12 @@ namespace osu.Game.Beatmaps
beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
beatmapInfo.Hash = stream.ComputeSHA2Hash(); beatmapInfo.Hash = stream.ComputeSHA2Hash();
beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;
AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo)); AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));
setInfo.Hash = beatmapImporter.ComputeHash(setInfo); setInfo.Hash = beatmapImporter.ComputeHash(setInfo);
setInfo.Status = BeatmapOnlineStatus.LocallyModified;
Realm.Write(r => Realm.Write(r =>
{ {

View File

@ -3,13 +3,23 @@
#nullable disable #nullable disable
using System.ComponentModel;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Localisation;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
public enum BeatmapOnlineStatus public enum BeatmapOnlineStatus
{ {
/// <summary>
/// This is a special status given when local changes are made via the editor.
/// Once in this state, online status changes should be ignored unless the beatmap is reverted or submitted.
/// </summary>
[Description("Local")]
[LocalisableDescription(typeof(SongSelectStrings), nameof(SongSelectStrings.LocallyModified))]
LocallyModified = -4,
None = -3, None = -3,
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowStatusGraveyard))] [LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowStatusGraveyard))]

View File

@ -84,13 +84,6 @@ namespace osu.Game.Beatmaps
{ {
} }
/// <summary>
/// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null.
/// The path returned is relative to the user file storage.
/// </summary>
/// <param name="filename">The name of the file to get the storage path of.</param>
public string? GetPathForFile(string filename) => Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.File.GetStoragePath();
public bool Equals(BeatmapSetInfo? other) public bool Equals(BeatmapSetInfo? other)
{ {
if (ReferenceEquals(this, other)) return true; if (ReferenceEquals(this, other)) return true;

View File

@ -0,0 +1,33 @@
// 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.Linq;
using osu.Game.Database;
using osu.Game.Extensions;
using osu.Game.Models;
namespace osu.Game.Beatmaps
{
public static class BeatmapSetInfoExtensions
{
/// <summary>
/// Returns the storage path for the file in this beatmapset with the given filename, if any exists, otherwise null.
/// The path returned is relative to the user file storage.
/// The lookup is case insensitive.
/// </summary>
/// <param name="model">The model to operate on.</param>
/// <param name="filename">The name of the file to get the storage path of.</param>
public static string? GetPathForFile(this IHasRealmFiles model, string filename) => model.GetFile(filename)?.File.GetStoragePath();
/// <summary>
/// Returns the file usage for the file in this beatmapset with the given filename, if any exists, otherwise null.
/// The path returned is relative to the user file storage.
/// The lookup is case insensitive.
/// </summary>
/// <param name="model">The model to operate on.</param>
/// <param name="filename">The name of the file to get the storage path of.</param>
public static RealmNamedFileUsage? GetFile(this IHasRealmFiles model, string filename) =>
model.Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase));
}
}

View File

@ -6,6 +6,7 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Data.Sqlite; using Microsoft.Data.Sqlite;
using osu.Framework.Development; using osu.Framework.Development;
@ -51,6 +52,9 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// Queue an update for a beatmap set. /// Queue an update for a beatmap set.
/// </summary> /// </summary>
/// <remarks>
/// This may happen during initial import, or at a later stage in response to a user action or server event.
/// </remarks>
/// <param name="beatmapSet">The beatmap set to update. Updates will be applied directly (so a transaction should be started if this instance is managed).</param> /// <param name="beatmapSet">The beatmap set to update. Updates will be applied directly (so a transaction should be started if this instance is managed).</param>
/// <param name="preferOnlineFetch">Whether metadata from an online source should be preferred. If <c>true</c>, the local cache will be skipped to ensure the freshest data state possible.</param> /// <param name="preferOnlineFetch">Whether metadata from an online source should be preferred. If <c>true</c>, the local cache will be skipped to ensure the freshest data state possible.</param>
public void Update(BeatmapSetInfo beatmapSet, bool preferOnlineFetch) public void Update(BeatmapSetInfo beatmapSet, bool preferOnlineFetch)
@ -89,21 +93,26 @@ namespace osu.Game.Beatmaps
if (res != null) if (res != null)
{ {
beatmapInfo.Status = res.Status; beatmapInfo.OnlineID = res.OnlineID;
Debug.Assert(beatmapInfo.BeatmapSet != null);
beatmapInfo.BeatmapSet.Status = res.BeatmapSet?.Status ?? BeatmapOnlineStatus.None;
beatmapInfo.BeatmapSet.OnlineID = res.OnlineBeatmapSetID;
beatmapInfo.BeatmapSet.DateRanked = res.BeatmapSet?.Ranked;
beatmapInfo.BeatmapSet.DateSubmitted = res.BeatmapSet?.Submitted;
beatmapInfo.OnlineMD5Hash = res.MD5Hash; beatmapInfo.OnlineMD5Hash = res.MD5Hash;
beatmapInfo.LastOnlineUpdate = res.LastUpdated; beatmapInfo.LastOnlineUpdate = res.LastUpdated;
beatmapInfo.OnlineID = res.OnlineID; Debug.Assert(beatmapInfo.BeatmapSet != null);
beatmapInfo.BeatmapSet.OnlineID = res.OnlineBeatmapSetID;
// Some metadata should only be applied if there's no local changes.
if (shouldSaveOnlineMetadata(beatmapInfo))
{
beatmapInfo.Status = res.Status;
beatmapInfo.Metadata.Author.OnlineID = res.AuthorID; beatmapInfo.Metadata.Author.OnlineID = res.AuthorID;
}
if (beatmapInfo.BeatmapSet.Beatmaps.All(shouldSaveOnlineMetadata))
{
beatmapInfo.BeatmapSet.Status = res.BeatmapSet?.Status ?? BeatmapOnlineStatus.None;
beatmapInfo.BeatmapSet.DateRanked = res.BeatmapSet?.Ranked;
beatmapInfo.BeatmapSet.DateSubmitted = res.BeatmapSet?.Submitted;
}
logForModel(set, $"Online retrieval mapped {beatmapInfo} to {res.OnlineBeatmapSetID} / {res.OnlineID}."); logForModel(set, $"Online retrieval mapped {beatmapInfo} to {res.OnlineBeatmapSetID} / {res.OnlineID}.");
} }
@ -202,19 +211,26 @@ namespace osu.Game.Beatmaps
{ {
var status = (BeatmapOnlineStatus)reader.GetByte(2); var status = (BeatmapOnlineStatus)reader.GetByte(2);
// Some metadata should only be applied if there's no local changes.
if (shouldSaveOnlineMetadata(beatmapInfo))
{
beatmapInfo.Status = status; beatmapInfo.Status = status;
beatmapInfo.Metadata.Author.OnlineID = reader.GetInt32(3);
}
Debug.Assert(beatmapInfo.BeatmapSet != null);
beatmapInfo.BeatmapSet.Status = status;
beatmapInfo.BeatmapSet.OnlineID = reader.GetInt32(0);
// TODO: DateSubmitted and DateRanked are not provided by local cache. // TODO: DateSubmitted and DateRanked are not provided by local cache.
beatmapInfo.OnlineID = reader.GetInt32(1); beatmapInfo.OnlineID = reader.GetInt32(1);
beatmapInfo.Metadata.Author.OnlineID = reader.GetInt32(3);
beatmapInfo.OnlineMD5Hash = reader.GetString(4); beatmapInfo.OnlineMD5Hash = reader.GetString(4);
beatmapInfo.LastOnlineUpdate = reader.GetDateTimeOffset(5); beatmapInfo.LastOnlineUpdate = reader.GetDateTimeOffset(5);
Debug.Assert(beatmapInfo.BeatmapSet != null);
beatmapInfo.BeatmapSet.OnlineID = reader.GetInt32(0);
if (beatmapInfo.BeatmapSet.Beatmaps.All(shouldSaveOnlineMetadata))
{
beatmapInfo.BeatmapSet.Status = status;
}
logForModel(set, $"Cached local retrieval for {beatmapInfo}."); logForModel(set, $"Cached local retrieval for {beatmapInfo}.");
return true; return true;
} }
@ -233,6 +249,12 @@ namespace osu.Game.Beatmaps
private void logForModel(BeatmapSetInfo set, string message) => private void logForModel(BeatmapSetInfo set, string message) =>
RealmArchiveModelImporter<BeatmapSetInfo>.LogForModel(set, $"[{nameof(BeatmapUpdaterMetadataLookup)}] {message}"); RealmArchiveModelImporter<BeatmapSetInfo>.LogForModel(set, $"[{nameof(BeatmapUpdaterMetadataLookup)}] {message}");
/// <summary>
/// Check whether the provided beatmap is in a state where online "ranked" status metadata should be saved against it.
/// Handles the case where a user may have locally modified a beatmap in the editor and expects the local status to stick.
/// </summary>
private static bool shouldSaveOnlineMetadata(BeatmapInfo beatmapInfo) => beatmapInfo.MatchesOnlineVersion || beatmapInfo.Status != BeatmapOnlineStatus.LocallyModified;
public void Dispose() public void Dispose()
{ {
cacheDownloadRequest?.Dispose(); cacheDownloadRequest?.Dispose();

View File

@ -6,15 +6,18 @@ using osu.Framework.Extensions;
using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
using osu.Game.Overlays; using osu.Game.Overlays;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Beatmaps.Drawables namespace osu.Game.Beatmaps.Drawables
{ {
public class BeatmapSetOnlineStatusPill : CircularContainer public class BeatmapSetOnlineStatusPill : CircularContainer, IHasTooltip
{ {
private BeatmapOnlineStatus status; private BeatmapOnlineStatus status;
@ -96,5 +99,19 @@ namespace osu.Game.Beatmaps.Drawables
background.Colour = OsuColour.ForBeatmapSetOnlineStatus(Status) ?? colourProvider?.Light1 ?? colours.GreySeaFoamLighter; background.Colour = OsuColour.ForBeatmapSetOnlineStatus(Status) ?? colourProvider?.Light1 ?? colours.GreySeaFoamLighter;
} }
public LocalisableString TooltipText
{
get
{
switch (Status)
{
case BeatmapOnlineStatus.LocallyModified:
return SongSelectStrings.LocallyModifiedTooltip;
}
return string.Empty;
}
}
} }
} }

View File

@ -118,7 +118,10 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
// another async load might have completed before this one. // another async load might have completed before this one.
// if so, do not make any changes. // if so, do not make any changes.
if (loadedPreview != previewTrack) if (loadedPreview != previewTrack)
{
loadedPreview.Dispose();
return; return;
}
AddInternal(loadedPreview); AddInternal(loadedPreview);
toggleLoading(false); toggleLoading(false);

View File

@ -9,6 +9,8 @@ using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Rendering.Dummy;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Lists; using osu.Framework.Lists;
@ -56,7 +58,7 @@ namespace osu.Game.Beatmaps
this.resources = resources; this.resources = resources;
this.host = host; this.host = host;
this.files = files; this.files = files;
largeTextureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(files)); largeTextureStore = new LargeTextureStore(host?.Renderer ?? new DummyRenderer(), host?.CreateTextureLoaderStore(files));
this.trackStore = trackStore; this.trackStore = trackStore;
} }
@ -110,6 +112,7 @@ namespace osu.Game.Beatmaps
TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore; TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore;
ITrackStore IBeatmapResourceProvider.Tracks => trackStore; ITrackStore IBeatmapResourceProvider.Tracks => trackStore;
IRenderer IStorageResourceProvider.Renderer => host?.Renderer ?? new DummyRenderer();
AudioManager IStorageResourceProvider.AudioManager => audioManager; AudioManager IStorageResourceProvider.AudioManager => audioManager;
RealmAccess IStorageResourceProvider.RealmAccess => null; RealmAccess IStorageResourceProvider.RealmAccess => null;
IResourceStore<byte[]> IStorageResourceProvider.Files => files; IResourceStore<byte[]> IStorageResourceProvider.Files => files;

View File

@ -3,33 +3,17 @@
using System; using System;
using Humanizer; using Humanizer;
using osu.Framework.Graphics.Sprites;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Dialog;
namespace osu.Game.Collections namespace osu.Game.Collections
{ {
public class DeleteCollectionDialog : PopupDialog public class DeleteCollectionDialog : DeleteConfirmationDialog
{ {
public DeleteCollectionDialog(Live<BeatmapCollection> collection, Action deleteAction) public DeleteCollectionDialog(Live<BeatmapCollection> collection, Action deleteAction)
{ {
HeaderText = "Confirm deletion of";
BodyText = collection.PerformRead(c => $"{c.Name} ({"beatmap".ToQuantity(c.BeatmapMD5Hashes.Count)})"); BodyText = collection.PerformRead(c => $"{c.Name} ({"beatmap".ToQuantity(c.BeatmapMD5Hashes.Count)})");
DeleteAction = deleteAction;
Icon = FontAwesome.Regular.TrashAlt;
Buttons = new PopupDialogButton[]
{
new PopupDialogOkButton
{
Text = @"Yes. Go for it.",
Action = deleteAction
},
new PopupDialogCancelButton
{
Text = @"No! Abort mission!",
},
};
} }
} }
} }

View File

@ -3,16 +3,20 @@
#nullable disable #nullable disable
using System.ComponentModel; using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum BackgroundSource public enum BackgroundSource
{ {
[LocalisableDescription(typeof(SkinSettingsStrings), nameof(SkinSettingsStrings.SkinSectionHeader))]
Skin, Skin,
[LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.BeatmapHeader))]
Beatmap, Beatmap,
[Description("Beatmap (with storyboard / video)")] [LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.BeatmapWithStoryboard))]
BeatmapWithStoryboard, BeatmapWithStoryboard,
} }
} }

View File

@ -3,17 +3,20 @@
#nullable disable #nullable disable
using System.ComponentModel; using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum DiscordRichPresenceMode public enum DiscordRichPresenceMode
{ {
[LocalisableDescription(typeof(OnlineSettingsStrings), nameof(OnlineSettingsStrings.DiscordPresenceOff))]
Off, Off,
[Description("Hide identifiable information")] [LocalisableDescription(typeof(OnlineSettingsStrings), nameof(OnlineSettingsStrings.HideIdentifiableInformation))]
Limited, Limited,
[LocalisableDescription(typeof(OnlineSettingsStrings), nameof(OnlineSettingsStrings.DiscordPresenceFull))]
Full Full
} }
} }

View File

@ -3,17 +3,20 @@
#nullable disable #nullable disable
using System.ComponentModel; using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum HUDVisibilityMode public enum HUDVisibilityMode
{ {
[LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.NeverShowHUD))]
Never, Never,
[Description("Hide during gameplay")] [LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.HideDuringGameplay))]
HideDuringGameplay, HideDuringGameplay,
[LocalisableDescription(typeof(GameplaySettingsStrings), nameof(GameplaySettingsStrings.AlwaysShowHUD))]
Always Always
} }
} }

View File

@ -3,16 +3,17 @@
#nullable disable #nullable disable
using System.ComponentModel; using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum RandomSelectAlgorithm public enum RandomSelectAlgorithm
{ {
[Description("Never repeat")] [LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.NeverRepeat))]
RandomPermutation, RandomPermutation,
[Description("True Random")] [LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.TrueRandom))]
Random Random
} }
} }

View File

@ -3,17 +3,23 @@
#nullable disable #nullable disable
using System.ComponentModel; using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum ScalingMode public enum ScalingMode
{ {
[LocalisableDescription(typeof(LayoutSettingsStrings), nameof(LayoutSettingsStrings.ScalingOff))]
Off, Off,
[LocalisableDescription(typeof(LayoutSettingsStrings), nameof(LayoutSettingsStrings.ScaleEverything))]
Everything, Everything,
[Description("Excluding overlays")] [LocalisableDescription(typeof(LayoutSettingsStrings), nameof(LayoutSettingsStrings.ScaleEverythingExcludingOverlays))]
ExcludeOverlays, ExcludeOverlays,
[LocalisableDescription(typeof(LayoutSettingsStrings), nameof(LayoutSettingsStrings.ScaleGameplay))]
Gameplay, Gameplay,
} }
} }

View File

@ -3,16 +3,17 @@
#nullable disable #nullable disable
using System.ComponentModel; using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum ScreenshotFormat public enum ScreenshotFormat
{ {
[Description("JPG (web-friendly)")] [LocalisableDescription(typeof(GraphicsSettingsStrings), nameof(GraphicsSettingsStrings.Jpg))]
Jpg = 1, Jpg = 1,
[Description("PNG (lossless)")] [LocalisableDescription(typeof(GraphicsSettingsStrings), nameof(GraphicsSettingsStrings.Png))]
Png = 2 Png = 2
} }
} }

View File

@ -3,6 +3,9 @@
#nullable disable #nullable disable
using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Configuration namespace osu.Game.Configuration
{ {
public enum SeasonalBackgroundMode public enum SeasonalBackgroundMode
@ -10,16 +13,19 @@ namespace osu.Game.Configuration
/// <summary> /// <summary>
/// Seasonal backgrounds are shown regardless of season, if at all available. /// Seasonal backgrounds are shown regardless of season, if at all available.
/// </summary> /// </summary>
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.AlwaysSeasonalBackground))]
Always, Always,
/// <summary> /// <summary>
/// Seasonal backgrounds are shown only during their corresponding season. /// Seasonal backgrounds are shown only during their corresponding season.
/// </summary> /// </summary>
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.SometimesSeasonalBackground))]
Sometimes, Sometimes,
/// <summary> /// <summary>
/// Seasonal backgrounds are never shown. /// Seasonal backgrounds are never shown.
/// </summary> /// </summary>
[LocalisableDescription(typeof(UserInterfaceStrings), nameof(UserInterfaceStrings.NeverSeasonalBackground))]
Never Never
} }
} }

View File

@ -2,6 +2,7 @@
// 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.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Models; using osu.Game.Models;
namespace osu.Game.Database namespace osu.Game.Database
@ -11,8 +12,16 @@ namespace osu.Game.Database
/// </summary> /// </summary>
public interface IHasRealmFiles public interface IHasRealmFiles
{ {
/// <summary>
/// Available files in this model, with locally filenames.
/// When performing lookups, consider using <see cref="BeatmapSetInfoExtensions.GetFile"/> or <see cref="BeatmapSetInfoExtensions.GetPathForFile"/> to do case-insensitive lookups.
/// </summary>
IList<RealmNamedFileUsage> Files { get; } IList<RealmNamedFileUsage> Files { get; }
/// <summary>
/// A combined hash representing the model, based on the files it contains.
/// Implementation specific.
/// </summary>
string Hash { get; set; } string Hash { get; set; }
} }
} }

View File

@ -1,8 +1,6 @@
// 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 System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading; using System.Threading;
@ -27,27 +25,30 @@ namespace osu.Game.Database
public class LegacyImportManager : Component public class LegacyImportManager : Component
{ {
[Resolved] [Resolved]
private SkinManager skins { get; set; } private SkinManager skins { get; set; } = null!;
[Resolved] [Resolved]
private BeatmapManager beatmaps { get; set; } private BeatmapManager beatmaps { get; set; } = null!;
[Resolved] [Resolved]
private ScoreManager scores { get; set; } private ScoreManager scores { get; set; } = null!;
[Resolved(canBeNull: true)]
private OsuGame game { get; set; }
[Resolved] [Resolved]
private IDialogOverlay dialogOverlay { get; set; } private OsuGame? game { get; set; }
[Resolved] [Resolved]
private RealmAccess realmAccess { get; set; } private IDialogOverlay dialogOverlay { get; set; } = null!;
[Resolved(canBeNull: true)] [Resolved]
private DesktopGameHost desktopGameHost { get; set; } private RealmAccess realmAccess { get; set; } = null!;
private StableStorage cachedStorage; [Resolved(canBeNull: true)] // canBeNull required while we remain on mono for mobile platforms.
private DesktopGameHost? desktopGameHost { get; set; }
[Resolved]
private INotificationOverlay? notifications { get; set; }
private StableStorage? cachedStorage;
public bool SupportsImportFromStable => RuntimeInfo.IsDesktop; public bool SupportsImportFromStable => RuntimeInfo.IsDesktop;
@ -98,6 +99,9 @@ namespace osu.Game.Database
stableStorage = GetCurrentStableStorage(); stableStorage = GetCurrentStableStorage();
} }
if (stableStorage == null)
return;
var importTasks = new List<Task>(); var importTasks = new List<Task>();
Task beatmapImportTask = Task.CompletedTask; Task beatmapImportTask = Task.CompletedTask;
@ -108,7 +112,14 @@ namespace osu.Game.Database
importTasks.Add(new LegacySkinImporter(skins).ImportFromStableAsync(stableStorage)); importTasks.Add(new LegacySkinImporter(skins).ImportFromStableAsync(stableStorage));
if (content.HasFlagFast(StableContent.Collections)) if (content.HasFlagFast(StableContent.Collections))
importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyCollectionImporter(realmAccess).ImportFromStorage(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion)); {
importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyCollectionImporter(realmAccess)
{
// Other legacy importers import via model managers which handle the posting of notifications.
// Collections are an exception.
PostNotification = n => notifications?.Post(n)
}.ImportFromStorage(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
}
if (content.HasFlagFast(StableContent.Scores)) if (content.HasFlagFast(StableContent.Scores))
importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyScoreImporter(scores).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion)); importTasks.Add(beatmapImportTask.ContinueWith(_ => new LegacyScoreImporter(scores).ImportFromStableAsync(stableStorage), TaskContinuationOptions.OnlyOnRanToCompletion));
@ -116,7 +127,7 @@ namespace osu.Game.Database
await Task.WhenAll(importTasks.ToArray()).ConfigureAwait(false); await Task.WhenAll(importTasks.ToArray()).ConfigureAwait(false);
} }
public StableStorage GetCurrentStableStorage() public StableStorage? GetCurrentStableStorage()
{ {
if (cachedStorage != null) if (cachedStorage != null)
return cachedStorage; return cachedStorage;

View File

@ -7,6 +7,7 @@ using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Models; using osu.Game.Models;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
@ -79,7 +80,7 @@ namespace osu.Game.Database
/// </summary> /// </summary>
public void AddFile(TModel item, Stream contents, string filename, Realm realm) public void AddFile(TModel item, Stream contents, string filename, Realm realm)
{ {
var existing = item.Files.FirstOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase)); var existing = item.GetFile(filename);
if (existing != null) if (existing != null)
{ {

View File

@ -25,6 +25,7 @@ using osu.Game.Configuration;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Models; using osu.Game.Models;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
using Realms; using Realms;
@ -172,6 +173,11 @@ namespace osu.Game.Database
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal)) if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
Filename += realm_extension; Filename += realm_extension;
#if DEBUG
if (!DebugUtils.IsNUnitRunning)
applyFilenameSchemaSuffix(ref Filename);
#endif
string newerVersionFilename = $"{Filename.Replace(realm_extension, string.Empty)}_newer_version{realm_extension}"; string newerVersionFilename = $"{Filename.Replace(realm_extension, string.Empty)}_newer_version{realm_extension}";
// Attempt to recover a newer database version if available. // Attempt to recover a newer database version if available.
@ -211,6 +217,51 @@ namespace osu.Game.Database
} }
} }
/// <summary>
/// Some developers may be annoyed if a newer version migration (ie. caused by testing a pull request)
/// cause their test database to be unusable with previous versions.
/// To get around this, store development databases against their realm version.
/// Note that this means changes made on newer realm versions will disappear.
/// </summary>
private void applyFilenameSchemaSuffix(ref string filename)
{
string originalFilename = filename;
filename = getVersionedFilename(schema_version);
// First check if the current realm version already exists...
if (storage.Exists(filename))
return;
// Check for a previous version we can use as a base database to migrate from...
for (int i = schema_version - 1; i >= 0; i--)
{
string previousFilename = getVersionedFilename(i);
if (storage.Exists(previousFilename))
{
copyPreviousVersion(previousFilename, filename);
return;
}
}
// Finally, check for a non-versioned file exists (aka before this method was added)...
if (storage.Exists(originalFilename))
copyPreviousVersion(originalFilename, filename);
void copyPreviousVersion(string previousFilename, string newFilename)
{
using (var previous = storage.GetStream(previousFilename))
using (var current = storage.CreateFileSafely(newFilename))
{
Logger.Log(@$"Copying previous realm database {previousFilename} to {newFilename} for migration to schema version {schema_version}");
previous.CopyTo(current);
}
}
string getVersionedFilename(int version) => originalFilename.Replace(realm_extension, $"_{version}{realm_extension}");
}
private void attemptRecoverFromFile(string recoveryFilename) private void attemptRecoverFromFile(string recoveryFilename)
{ {
Logger.Log($@"Performing recovery from {recoveryFilename}", LoggingTarget.Database); Logger.Log($@"Performing recovery from {recoveryFilename}", LoggingTarget.Database);
@ -292,6 +343,11 @@ namespace osu.Game.Database
foreach (var s in pendingDeleteSkins) foreach (var s in pendingDeleteSkins)
realm.Remove(s); realm.Remove(s);
var pendingDeletePresets = realm.All<ModPreset>().Where(s => s.DeletePending);
foreach (var s in pendingDeletePresets)
realm.Remove(s);
transaction.Commit(); transaction.Commit();
} }

View File

@ -14,9 +14,8 @@ using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Graphics.Batches; using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.OpenGL.Buffers; using osu.Framework.Graphics.Rendering.Vertices;
using osu.Framework.Graphics.OpenGL.Vertices;
using osu.Framework.Lists; using osu.Framework.Lists;
namespace osu.Game.Graphics.Backgrounds namespace osu.Game.Graphics.Backgrounds
@ -88,7 +87,7 @@ namespace osu.Game.Graphics.Backgrounds
private Random stableRandom; private Random stableRandom;
private IShader shader; private IShader shader;
private readonly Texture texture; private Texture texture;
/// <summary> /// <summary>
/// Construct a new triangle visualisation. /// Construct a new triangle visualisation.
@ -98,13 +97,12 @@ namespace osu.Game.Graphics.Backgrounds
{ {
if (seed != null) if (seed != null)
stableRandom = new Random(seed.Value); stableRandom = new Random(seed.Value);
texture = Texture.WhitePixel;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ShaderManager shaders) private void load(IRenderer renderer, ShaderManager shaders)
{ {
texture = renderer.WhitePixel;
shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED); shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE_ROUNDED);
} }
@ -184,8 +182,8 @@ namespace osu.Game.Graphics.Backgrounds
private void addTriangles(bool randomY) private void addTriangles(bool randomY)
{ {
// limited by the maximum size of QuadVertexBuffer for safety. // Limited by the maximum size of QuadVertexBuffer for safety.
const int max_triangles = QuadVertexBuffer<TexturedVertex2D>.MAX_QUADS; const int max_triangles = ushort.MaxValue / (IRenderer.VERTICES_PER_QUAD + 2);
AimCount = (int)Math.Min(max_triangles, (DrawWidth * DrawHeight * 0.002f / (triangleScale * triangleScale) * SpawnRatio)); AimCount = (int)Math.Min(max_triangles, (DrawWidth * DrawHeight * 0.002f / (triangleScale * triangleScale) * SpawnRatio));
@ -251,7 +249,7 @@ namespace osu.Game.Graphics.Backgrounds
private readonly List<TriangleParticle> parts = new List<TriangleParticle>(); private readonly List<TriangleParticle> parts = new List<TriangleParticle>();
private Vector2 size; private Vector2 size;
private QuadBatch<TexturedVertex2D> vertexBatch; private IVertexBatch<TexturedVertex2D> vertexBatch;
public TrianglesDrawNode(Triangles source) public TrianglesDrawNode(Triangles source)
: base(source) : base(source)
@ -270,14 +268,14 @@ namespace osu.Game.Graphics.Backgrounds
parts.AddRange(Source.parts); parts.AddRange(Source.parts);
} }
public override void Draw(Action<TexturedVertex2D> vertexAction) public override void Draw(IRenderer renderer)
{ {
base.Draw(vertexAction); base.Draw(renderer);
if (Source.AimCount > 0 && (vertexBatch == null || vertexBatch.Size != Source.AimCount)) if (Source.AimCount > 0 && (vertexBatch == null || vertexBatch.Size != Source.AimCount))
{ {
vertexBatch?.Dispose(); vertexBatch?.Dispose();
vertexBatch = new QuadBatch<TexturedVertex2D>(Source.AimCount, 1); vertexBatch = renderer.CreateQuadBatch<TexturedVertex2D>(Source.AimCount, 1);
} }
shader.Bind(); shader.Bind();
@ -297,7 +295,7 @@ namespace osu.Game.Graphics.Backgrounds
ColourInfo colourInfo = DrawColourInfo.Colour; ColourInfo colourInfo = DrawColourInfo.Colour;
colourInfo.ApplyChild(particle.Colour); colourInfo.ApplyChild(particle.Colour);
DrawTriangle( renderer.DrawTriangle(
texture, texture,
triangle, triangle,
colourInfo, colourInfo,

View File

@ -5,7 +5,7 @@
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using osu.Framework.Graphics.OpenGL.Vertices; using osu.Framework.Graphics.Rendering.Vertices;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Graphics.ES30; using osuTK.Graphics.ES30;

View File

@ -137,6 +137,9 @@ namespace osu.Game.Graphics
{ {
switch (status) switch (status)
{ {
case BeatmapOnlineStatus.LocallyModified:
return Color4.OrangeRed;
case BeatmapOnlineStatus.Ranked: case BeatmapOnlineStatus.Ranked:
case BeatmapOnlineStatus.Approved: case BeatmapOnlineStatus.Approved:
return Color4Extensions.FromHex(@"b3ff66"); return Color4Extensions.FromHex(@"b3ff66");

View File

@ -6,8 +6,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Vertices;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Utils; using osu.Framework.Utils;
@ -89,7 +89,7 @@ namespace osu.Game.Graphics
currentTime = source.Time.Current; currentTime = source.Time.Current;
} }
protected override void Blit(Action<TexturedVertex2D> vertexAction) protected override void Blit(IRenderer renderer)
{ {
double time = currentTime - startTime; double time = currentTime - startTime;
@ -112,9 +112,9 @@ namespace osu.Game.Graphics
Vector2Extensions.Transform(rect.BottomRight, DrawInfo.Matrix) Vector2Extensions.Transform(rect.BottomRight, DrawInfo.Matrix)
); );
DrawQuad(Texture, quad, DrawColourInfo.Colour.MultiplyAlpha(alpha), null, vertexAction, renderer.DrawQuad(Texture, quad, DrawColourInfo.Colour.MultiplyAlpha(alpha),
new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height), inflationPercentage: new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height),
null, TextureCoords); textureCoords: TextureCoords);
} }
} }

View File

@ -7,8 +7,8 @@ using System;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Vertices;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Utils; using osu.Framework.Utils;
@ -107,7 +107,7 @@ namespace osu.Game.Graphics
sourceSize = Source.DrawSize; sourceSize = Source.DrawSize;
} }
protected override void Blit(Action<TexturedVertex2D> vertexAction) protected override void Blit(IRenderer renderer)
{ {
foreach (var p in particles) foreach (var p in particles)
{ {
@ -136,9 +136,9 @@ namespace osu.Game.Graphics
transformPosition(rect.BottomRight, rect.Centre, angle) transformPosition(rect.BottomRight, rect.Centre, angle)
); );
DrawQuad(Texture, quad, DrawColourInfo.Colour.MultiplyAlpha(alpha), null, vertexAction, renderer.DrawQuad(Texture, quad, DrawColourInfo.Colour.MultiplyAlpha(alpha),
new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height), inflationPercentage: new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height),
null, TextureCoords); textureCoords: TextureCoords);
} }
} }

View File

@ -3,10 +3,9 @@
#nullable disable #nullable disable
using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Vertices; using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
@ -57,11 +56,11 @@ namespace osu.Game.Graphics.Sprites
progress = source.animationProgress; progress = source.animationProgress;
} }
protected override void Blit(Action<TexturedVertex2D> vertexAction) protected override void Blit(IRenderer renderer)
{ {
Shader.GetUniform<float>("progress").UpdateValue(ref progress); GetAppropriateShader(renderer).GetUniform<float>("progress").UpdateValue(ref progress);
base.Blit(vertexAction); base.Blit(renderer);
} }
protected override bool CanDrawOpaqueInterior => false; protected override bool CanDrawOpaqueInterior => false;

View File

@ -167,6 +167,11 @@ namespace osu.Game.Graphics.UserInterface
{ {
base.Update(); base.Update();
// If the game goes into a suspended state (ie. debugger attached or backgrounded on a mobile device)
// we want to ignore really long periods of no processing.
if (updateClock.ElapsedFrameTime > 10000)
return;
mainContent.Width = Math.Max(mainContent.Width, counters.DrawWidth); mainContent.Width = Math.Max(mainContent.Width, counters.DrawWidth);
// Handle the case where the window has become inactive or the user changed the // Handle the case where the window has become inactive or the user changed the
@ -177,15 +182,15 @@ namespace osu.Game.Graphics.UserInterface
// use elapsed frame time rather then FramesPerSecond to better catch stutter frames. // use elapsed frame time rather then FramesPerSecond to better catch stutter frames.
bool hasDrawSpike = displayedFpsCount > (1000 / spike_time_ms) && drawClock.ElapsedFrameTime > spike_time_ms; bool hasDrawSpike = displayedFpsCount > (1000 / spike_time_ms) && drawClock.ElapsedFrameTime > spike_time_ms;
// note that we use an elapsed time here of 1 intentionally. const float damp_time = 100;
// this weights all updates equally. if we passed in the elapsed time, longer frames would be weighted incorrectly lower.
displayedFrameTime = Interpolation.DampContinuously(displayedFrameTime, updateClock.ElapsedFrameTime, hasUpdateSpike ? 0 : 100, 1); displayedFrameTime = Interpolation.DampContinuously(displayedFrameTime, updateClock.ElapsedFrameTime, hasUpdateSpike ? 0 : damp_time, updateClock.ElapsedFrameTime);
if (hasDrawSpike) if (hasDrawSpike)
// show spike time using raw elapsed value, to account for `FramesPerSecond` being so averaged spike frames don't show. // show spike time using raw elapsed value, to account for `FramesPerSecond` being so averaged spike frames don't show.
displayedFpsCount = 1000 / drawClock.ElapsedFrameTime; displayedFpsCount = 1000 / drawClock.ElapsedFrameTime;
else else
displayedFpsCount = Interpolation.DampContinuously(displayedFpsCount, drawClock.FramesPerSecond, 100, Time.Elapsed); displayedFpsCount = Interpolation.DampContinuously(displayedFpsCount, drawClock.FramesPerSecond, damp_time, Time.Elapsed);
if (Time.Current - lastUpdate > min_time_between_updates) if (Time.Current - lastUpdate > min_time_between_updates)
{ {
@ -203,7 +208,7 @@ namespace osu.Game.Graphics.UserInterface
if (hasSignificantChanges) if (hasSignificantChanges)
requestDisplay(); requestDisplay();
else if (isDisplayed && Time.Current - lastDisplayRequiredTime > 2000) else if (isDisplayed && Time.Current - lastDisplayRequiredTime > 2000 && !IsHovered)
{ {
mainContent.FadeTo(0, 300, Easing.OutQuint); mainContent.FadeTo(0, 300, Easing.OutQuint);
isDisplayed = false; isDisplayed = false;

View File

@ -20,6 +20,8 @@ namespace osu.Game.Graphics.UserInterface
/// </summary> /// </summary>
public class LoadingLayer : LoadingSpinner public class LoadingLayer : LoadingSpinner
{ {
private readonly bool blockInput;
[CanBeNull] [CanBeNull]
protected Box BackgroundDimLayer { get; } protected Box BackgroundDimLayer { get; }
@ -28,9 +30,11 @@ namespace osu.Game.Graphics.UserInterface
/// </summary> /// </summary>
/// <param name="dimBackground">Whether the full background area should be dimmed while loading.</param> /// <param name="dimBackground">Whether the full background area should be dimmed while loading.</param>
/// <param name="withBox">Whether the spinner should have a surrounding black box for visibility.</param> /// <param name="withBox">Whether the spinner should have a surrounding black box for visibility.</param>
public LoadingLayer(bool dimBackground = false, bool withBox = true) /// <param name="blockInput">Whether to block input of components behind the loading layer.</param>
public LoadingLayer(bool dimBackground = false, bool withBox = true, bool blockInput = true)
: base(withBox) : base(withBox)
{ {
this.blockInput = blockInput;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
Size = new Vector2(1); Size = new Vector2(1);
@ -52,6 +56,9 @@ namespace osu.Game.Graphics.UserInterface
protected override bool Handle(UIEvent e) protected override bool Handle(UIEvent e)
{ {
if (!blockInput)
return false;
switch (e) switch (e)
{ {
// blocking scroll can cause weird behaviour when this layer is used within a ScrollContainer. // blocking scroll can cause weird behaviour when this layer is used within a ScrollContainer.
@ -83,7 +90,7 @@ namespace osu.Game.Graphics.UserInterface
{ {
base.Update(); base.Update();
MainContents.Size = new Vector2(Math.Clamp(Math.Min(DrawWidth, DrawHeight) * 0.25f, 30, 100)); MainContents.Size = new Vector2(Math.Clamp(Math.Min(DrawWidth, DrawHeight) * 0.25f, 20, 100));
} }
} }
} }

View File

@ -1,9 +1,8 @@
// 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.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Graphics.Rendering;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Game.Database; using osu.Game.Database;
@ -12,10 +11,15 @@ namespace osu.Game.IO
{ {
public interface IStorageResourceProvider public interface IStorageResourceProvider
{ {
/// <summary>
/// The game renderer.
/// </summary>
IRenderer Renderer { get; }
/// <summary> /// <summary>
/// Retrieve the game-wide audio manager. /// Retrieve the game-wide audio manager.
/// </summary> /// </summary>
AudioManager AudioManager { get; } AudioManager? AudioManager { get; }
/// <summary> /// <summary>
/// Access game-wide user files. /// Access game-wide user files.

View File

@ -1,8 +1,6 @@
// 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 System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -15,8 +13,9 @@ namespace osu.Game.Input.Bindings
{ {
public class GlobalActionContainer : DatabasedKeyBindingContainer<GlobalAction>, IHandleGlobalKeyboardInput public class GlobalActionContainer : DatabasedKeyBindingContainer<GlobalAction>, IHandleGlobalKeyboardInput
{ {
private readonly Drawable handler; private readonly Drawable? handler;
private InputManager parentInputManager;
private InputManager? parentInputManager;
public GlobalActionContainer(OsuGameBase game) public GlobalActionContainer(OsuGameBase game)
: base(matchingMode: KeyCombinationMatchingMode.Modifiers) : base(matchingMode: KeyCombinationMatchingMode.Modifiers)
@ -32,7 +31,10 @@ namespace osu.Game.Input.Bindings
parentInputManager = GetContainingInputManager(); parentInputManager = GetContainingInputManager();
} }
// IMPORTANT: Do not change the order of key bindings in this list.
// It is used to decide the order of precedence (see note in DatabasedKeyBindingContainer).
public override IEnumerable<IKeyBinding> DefaultKeyBindings => GlobalKeyBindings public override IEnumerable<IKeyBinding> DefaultKeyBindings => GlobalKeyBindings
.Concat(OverlayKeyBindings)
.Concat(EditorKeyBindings) .Concat(EditorKeyBindings)
.Concat(InGameKeyBindings) .Concat(InGameKeyBindings)
.Concat(SongSelectKeyBindings) .Concat(SongSelectKeyBindings)
@ -40,25 +42,6 @@ namespace osu.Game.Input.Bindings
public IEnumerable<KeyBinding> GlobalKeyBindings => new[] public IEnumerable<KeyBinding> GlobalKeyBindings => new[]
{ {
new KeyBinding(InputKey.F6, GlobalAction.ToggleNowPlaying),
new KeyBinding(InputKey.F8, GlobalAction.ToggleChat),
new KeyBinding(InputKey.F9, GlobalAction.ToggleSocial),
new KeyBinding(InputKey.F10, GlobalAction.ToggleGameplayMouseButtons),
new KeyBinding(InputKey.F12, GlobalAction.TakeScreenshot),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.F }, GlobalAction.ToggleFPSDisplay),
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings),
new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar),
new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings),
new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleBeatmapListing),
new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor),
new KeyBinding(InputKey.Escape, GlobalAction.Back),
new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back),
new KeyBinding(new[] { InputKey.Alt, InputKey.Home }, GlobalAction.Home),
new KeyBinding(InputKey.Up, GlobalAction.SelectPrevious), new KeyBinding(InputKey.Up, GlobalAction.SelectPrevious),
new KeyBinding(InputKey.Down, GlobalAction.SelectNext), new KeyBinding(InputKey.Down, GlobalAction.SelectNext),
@ -69,7 +52,31 @@ namespace osu.Game.Input.Bindings
new KeyBinding(InputKey.Enter, GlobalAction.Select), new KeyBinding(InputKey.Enter, GlobalAction.Select),
new KeyBinding(InputKey.KeypadEnter, GlobalAction.Select), new KeyBinding(InputKey.KeypadEnter, GlobalAction.Select),
new KeyBinding(InputKey.Escape, GlobalAction.Back),
new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back),
new KeyBinding(new[] { InputKey.Alt, InputKey.Home }, GlobalAction.Home),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.F }, GlobalAction.ToggleFPSDisplay),
new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.S }, GlobalAction.ToggleSkinEditor),
new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings),
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.R }, GlobalAction.RandomSkin), new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.R }, GlobalAction.RandomSkin),
new KeyBinding(InputKey.F10, GlobalAction.ToggleGameplayMouseButtons),
new KeyBinding(InputKey.F12, GlobalAction.TakeScreenshot),
};
public IEnumerable<KeyBinding> OverlayKeyBindings => new[]
{
new KeyBinding(InputKey.F8, GlobalAction.ToggleChat),
new KeyBinding(InputKey.F6, GlobalAction.ToggleNowPlaying),
new KeyBinding(InputKey.F9, GlobalAction.ToggleSocial),
new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleBeatmapListing),
new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings),
new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications),
}; };
public IEnumerable<KeyBinding> EditorKeyBindings => new[] public IEnumerable<KeyBinding> EditorKeyBindings => new[]

View File

@ -3,8 +3,9 @@
#nullable disable #nullable disable
using System.ComponentModel;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Input namespace osu.Game.Input
{ {
@ -17,18 +18,20 @@ namespace osu.Game.Input
/// <summary> /// <summary>
/// The mouse cursor will be free to move outside the game window. /// The mouse cursor will be free to move outside the game window.
/// </summary> /// </summary>
[LocalisableDescription(typeof(MouseSettingsStrings), nameof(MouseSettingsStrings.NeverConfine))]
Never, Never,
/// <summary> /// <summary>
/// The mouse cursor will be locked to the window bounds during gameplay, /// The mouse cursor will be locked to the window bounds during gameplay,
/// but may otherwise move freely. /// but may otherwise move freely.
/// </summary> /// </summary>
[Description("During Gameplay")] [LocalisableDescription(typeof(MouseSettingsStrings), nameof(MouseSettingsStrings.ConfineDuringGameplay))]
DuringGameplay, DuringGameplay,
/// <summary> /// <summary>
/// The mouse cursor will always be locked to the window bounds while the game has focus. /// The mouse cursor will always be locked to the window bounds while the game has focus.
/// </summary> /// </summary>
[LocalisableDescription(typeof(MouseSettingsStrings), nameof(MouseSettingsStrings.AlwaysConfine))]
Always Always
} }
} }

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class DeleteConfirmationDialogStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.DeleteConfirmationDialog";
/// <summary>
/// "Confirm deletion of"
/// </summary>
public static LocalisableString HeaderText => new TranslatableString(getKey(@"header_text"), @"Confirm deletion of");
/// <summary>
/// "Yes. Go for it."
/// </summary>
public static LocalisableString Confirm => new TranslatableString(getKey(@"confirm"), @"Yes. Go for it.");
/// <summary>
/// "No! Abort mission"
/// </summary>
public static LocalisableString Cancel => new TranslatableString(getKey(@"cancel"), @"No! Abort mission");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -104,6 +104,31 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString IncreaseFirstObjectVisibility => new TranslatableString(getKey(@"increase_first_object_visibility"), @"Increase visibility of first object when visual impairment mods are enabled"); public static LocalisableString IncreaseFirstObjectVisibility => new TranslatableString(getKey(@"increase_first_object_visibility"), @"Increase visibility of first object when visual impairment mods are enabled");
/// <summary>
/// "Hide during gameplay"
/// </summary>
public static LocalisableString HideDuringGameplay => new TranslatableString(getKey(@"hide_during_gameplay"), @"Hide during gameplay");
/// <summary>
/// "Always"
/// </summary>
public static LocalisableString AlwaysShowHUD => new TranslatableString(getKey(@"always_show_hud"), @"Always");
/// <summary>
/// "Never"
/// </summary>
public static LocalisableString NeverShowHUD => new TranslatableString(getKey(@"never_show_hud"), @"Never");
/// <summary>
/// "Standardised"
/// </summary>
public static LocalisableString StandardisedScoreDisplay => new TranslatableString(getKey(@"standardised_score_display"), @"Standardised");
/// <summary>
/// "Classic"
/// </summary>
public static LocalisableString ClassicScoreDisplay => new TranslatableString(getKey(@"classic_score_display"), @"Classic");
private static string getKey(string key) => $"{prefix}:{key}"; private static string getKey(string key) => $"{prefix}:{key}";
} }
} }

View File

@ -129,6 +129,16 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString UseHardwareAcceleration => new TranslatableString(getKey(@"use_hardware_acceleration"), @"Use hardware acceleration"); public static LocalisableString UseHardwareAcceleration => new TranslatableString(getKey(@"use_hardware_acceleration"), @"Use hardware acceleration");
/// <summary>
/// "JPG (web-friendly)"
/// </summary>
public static LocalisableString Jpg => new TranslatableString(getKey(@"jpg_web_friendly"), @"JPG (web-friendly)");
/// <summary>
/// "PNG (lossless)"
/// </summary>
public static LocalisableString Png => new TranslatableString(getKey(@"png_lossless"), @"PNG (lossless)");
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 GlobalKeyBindingHeader => new TranslatableString(getKey(@"global_key_binding_header"), @"Global"); public static LocalisableString GlobalKeyBindingHeader => new TranslatableString(getKey(@"global_key_binding_header"), @"Global");
/// <summary>
/// "Overlays"
/// </summary>
public static LocalisableString OverlaysSection => new TranslatableString(getKey(@"overlays_section"), @"Overlays");
/// <summary> /// <summary>
/// "Song Select" /// "Song Select"
/// </summary> /// </summary>

View File

@ -29,6 +29,26 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString FullscreenMacOSNote => new TranslatableString(getKey(@"fullscreen_macos_note"), @"Using fullscreen on macOS makes interacting with the menu bar and spaces no longer work, and may lead to freezes if a system dialog is presented. Using borderless is recommended."); public static LocalisableString FullscreenMacOSNote => new TranslatableString(getKey(@"fullscreen_macos_note"), @"Using fullscreen on macOS makes interacting with the menu bar and spaces no longer work, and may lead to freezes if a system dialog is presented. Using borderless is recommended.");
/// <summary>
/// "Excluding overlays"
/// </summary>
public static LocalisableString ScaleEverythingExcludingOverlays => new TranslatableString(getKey(@"scale_everything_excluding_overlays"), @"Excluding overlays");
/// <summary>
/// "Everything"
/// </summary>
public static LocalisableString ScaleEverything => new TranslatableString(getKey(@"scale_everything"), @"Everything");
/// <summary>
/// "Gameplay"
/// </summary>
public static LocalisableString ScaleGameplay => new TranslatableString(getKey(@"scale_gameplay"), @"Gameplay");
/// <summary>
/// "Off"
/// </summary>
public static LocalisableString ScalingOff => new TranslatableString(getKey(@"scaling_off"), @"Off");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -74,6 +74,16 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString RestoreAllRecentlyDeletedBeatmaps => new TranslatableString(getKey(@"restore_all_recently_deleted_beatmaps"), @"Restore all recently deleted beatmaps"); public static LocalisableString RestoreAllRecentlyDeletedBeatmaps => new TranslatableString(getKey(@"restore_all_recently_deleted_beatmaps"), @"Restore all recently deleted beatmaps");
/// <summary>
/// "Delete ALL mod presets"
/// </summary>
public static LocalisableString DeleteAllModPresets => new TranslatableString(getKey(@"delete_all_mod_presets"), @"Delete ALL mod presets");
/// <summary>
/// "Restore all recently deleted mod presets"
/// </summary>
public static LocalisableString RestoreAllRecentlyDeletedModPresets => new TranslatableString(getKey(@"restore_all_recently_deleted_mod_presets"), @"Restore all recently deleted mod presets");
private static string getKey(string key) => $"{prefix}:{key}"; private static string getKey(string key) => $"{prefix}:{key}";
} }
} }

View File

@ -64,6 +64,21 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString HighPrecisionPlatformWarning => new TranslatableString(getKey(@"high_precision_platform_warning"), @"This setting has known issues on your platform. If you encounter problems, it is recommended to adjust sensitivity externally and keep this disabled for now."); public static LocalisableString HighPrecisionPlatformWarning => new TranslatableString(getKey(@"high_precision_platform_warning"), @"This setting has known issues on your platform. If you encounter problems, it is recommended to adjust sensitivity externally and keep this disabled for now.");
/// <summary>
/// "Always"
/// </summary>
public static LocalisableString AlwaysConfine => new TranslatableString(getKey(@"always_confine"), @"Always");
/// <summary>
/// "During Gameplay"
/// </summary>
public static LocalisableString ConfineDuringGameplay => new TranslatableString(getKey(@"confine_during_gameplay"), @"During Gameplay");
/// <summary>
/// "Never"
/// </summary>
public static LocalisableString NeverConfine => new TranslatableString(getKey(@"never_confine"), @"Never");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -64,6 +64,21 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString ShowExplicitContent => new TranslatableString(getKey(@"show_explicit_content"), @"Show explicit content in search results"); public static LocalisableString ShowExplicitContent => new TranslatableString(getKey(@"show_explicit_content"), @"Show explicit content in search results");
/// <summary>
/// "Hide identifiable information"
/// </summary>
public static LocalisableString HideIdentifiableInformation => new TranslatableString(getKey(@"hide_identifiable_information"), @"Hide identifiable information");
/// <summary>
/// "Full"
/// </summary>
public static LocalisableString DiscordPresenceFull => new TranslatableString(getKey(@"discord_presence_full"), @"Full");
/// <summary>
/// "Off"
/// </summary>
public static LocalisableString DiscordPresenceOff => new TranslatableString(getKey(@"discord_presence_off"), @"Off");
private static string getKey(string key) => $"{prefix}:{key}"; private static string getKey(string key) => $"{prefix}:{key}";
} }
} }

View File

@ -14,6 +14,21 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString Rulesets => new TranslatableString(getKey(@"rulesets"), @"Rulesets"); public static LocalisableString Rulesets => new TranslatableString(getKey(@"rulesets"), @"Rulesets");
/// <summary>
/// "None"
/// </summary>
public static LocalisableString BorderNone => new TranslatableString(getKey(@"no_borders"), @"None");
/// <summary>
/// "Corners"
/// </summary>
public static LocalisableString BorderCorners => new TranslatableString(getKey(@"corner_borders"), @"Corners");
/// <summary>
/// "Full"
/// </summary>
public static LocalisableString BorderFull => new TranslatableString(getKey(@"full_borders"), @"Full");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class SongSelectStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.SongSelect";
/// <summary>
/// "Local"
/// </summary>
public static LocalisableString LocallyModified => new TranslatableString(getKey(@"locally_modified"), @"Local");
/// <summary>
/// "Has been locally modified"
/// </summary>
public static LocalisableString LocallyModifiedTooltip => new TranslatableString(getKey(@"locally_modified_tooltip"), @"Has been locally modified");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class ToolbarStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.Toolbar";
/// <summary>
/// "Connection interrupted, will try to reconnect..."
/// </summary>
public static LocalisableString AttemptingToReconnect => new TranslatableString(getKey(@"attempting_to_reconnect"), @"Connection interrupted, will try to reconnect...");
/// <summary>
/// "Connecting..."
/// </summary>
public static LocalisableString Connecting => new TranslatableString(getKey(@"connecting"), @"Connecting...");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -114,6 +114,46 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString NoLimit => new TranslatableString(getKey(@"no_limit"), @"no limit"); public static LocalisableString NoLimit => new TranslatableString(getKey(@"no_limit"), @"no limit");
/// <summary>
/// "Beatmap (with storyboard / video)"
/// </summary>
public static LocalisableString BeatmapWithStoryboard => new TranslatableString(getKey(@"beatmap_with_storyboard"), @"Beatmap (with storyboard / video)");
/// <summary>
/// "Always"
/// </summary>
public static LocalisableString AlwaysSeasonalBackground => new TranslatableString(getKey(@"always_seasonal_backgrounds"), @"Always");
/// <summary>
/// "Never"
/// </summary>
public static LocalisableString NeverSeasonalBackground => new TranslatableString(getKey(@"never_seasonal_backgrounds"), @"Never");
/// <summary>
/// "Sometimes"
/// </summary>
public static LocalisableString SometimesSeasonalBackground => new TranslatableString(getKey(@"sometimes_seasonal_backgrounds"), @"Sometimes");
/// <summary>
/// "Sequential"
/// </summary>
public static LocalisableString SequentialHotkeyStyle => new TranslatableString(getKey(@"mods_sequential_hotkeys"), @"Sequential");
/// <summary>
/// "Classic"
/// </summary>
public static LocalisableString ClassicHotkeyStyle => new TranslatableString(getKey(@"mods_classic_hotkeys"), @"Classic");
/// <summary>
/// "Never repeat"
/// </summary>
public static LocalisableString NeverRepeat => new TranslatableString(getKey(@"never_repeat_random"), @"Never repeat");
/// <summary>
/// "True Random"
/// </summary>
public static LocalisableString TrueRandom => new TranslatableString(getKey(@"true_random"), @"True Random");
private static string getKey(string key) => $"{prefix}:{key}"; private static string getKey(string key) => $"{prefix}:{key}";
} }
} }

View File

@ -104,30 +104,23 @@ namespace osu.Game.Online.API
/// </summary> /// </summary>
private int failureCount; private int failureCount;
/// <summary>
/// The main API thread loop, which will continue to run until the game is shut down.
/// </summary>
private void run() private void run()
{ {
while (!cancellationToken.IsCancellationRequested) while (!cancellationToken.IsCancellationRequested)
{ {
switch (State.Value) if (state.Value == APIState.Failing)
{ {
case APIState.Failing: // To recover from a failing state, falling through and running the full reconnection process seems safest for now.
//todo: replace this with a ping request. // This could probably be replaced with a ping-style request if we want to avoid the reconnection overheads.
log.Add(@"In a failing state, waiting a bit before we try again..."); log.Add($@"{nameof(APIAccess)} is in a failing state, waiting a bit before we try again...");
Thread.Sleep(5000); Thread.Sleep(5000);
if (!IsLoggedIn) goto case APIState.Connecting;
if (queue.Count == 0)
{
log.Add(@"Queueing a ping request");
Queue(new GetUserRequest());
} }
break; // Ensure that we have valid credentials.
// If not, setting the offline state will allow the game to prompt the user to provide new credentials.
case APIState.Offline:
case APIState.Connecting:
// work to restore a connection...
if (!HasLogin) if (!HasLogin)
{ {
state.Value = APIState.Offline; state.Value = APIState.Offline;
@ -135,8 +128,74 @@ namespace osu.Game.Online.API
continue; continue;
} }
Debug.Assert(HasLogin);
// Ensure that we are in an online state. If not, attempt a connect.
if (state.Value != APIState.Online)
{
attemptConnect();
if (state.Value != APIState.Online)
continue;
}
// hard bail if we can't get a valid access token.
if (authentication.RequestAccessToken() == null)
{
Logout();
continue;
}
processQueuedRequests();
Thread.Sleep(50);
}
}
/// <summary>
/// Dequeue from the queue and run each request synchronously until the queue is empty.
/// </summary>
private void processQueuedRequests()
{
while (true)
{
APIRequest req;
lock (queue)
{
if (queue.Count == 0) return;
req = queue.Dequeue();
}
handleRequest(req);
}
}
/// <summary>
/// From a non-connected state, perform a full connection flow, obtaining OAuth tokens and populating the local user and friends.
/// </summary>
/// <remarks>
/// This method takes control of <see cref="state"/> and transitions from <see cref="APIState.Connecting"/> to either
/// - <see cref="APIState.Online"/> (successful connection)
/// - <see cref="APIState.Failing"/> (failed connection but retrying)
/// - <see cref="APIState.Offline"/> (failed and can't retry, clear credentials and require user interaction)
/// </remarks>
/// <returns>Whether the connection attempt was successful.</returns>
private void attemptConnect()
{
state.Value = APIState.Connecting; state.Value = APIState.Connecting;
if (localUser.IsDefault)
{
// Show a placeholder user if saved credentials are available.
// This is useful for storing local scores and showing a placeholder username after starting the game,
// until a valid connection has been established.
setLocalUser(new APIUser
{
Username = ProvidedUsername,
});
}
// save the username at this point, if the user requested for it to be. // save the username at this point, if the user requested for it to be.
config.SetValue(OsuSetting.Username, config.Get<bool>(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty); config.SetValue(OsuSetting.Username, config.Get<bool>(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty);
@ -152,21 +211,20 @@ namespace osu.Game.Online.API
{ {
//todo: this fails even on network-related issues. we should probably handle those differently. //todo: this fails even on network-related issues. we should probably handle those differently.
LastLoginError = e; LastLoginError = e;
log.Add(@"Login failed!"); log.Add($@"Login failed for username {ProvidedUsername} ({LastLoginError.Message})!");
password = null;
authentication.Clear(); Logout();
continue; return;
} }
} }
var userReq = new GetUserRequest(); var userReq = new GetUserRequest();
userReq.Failure += ex => userReq.Failure += ex =>
{ {
if (ex is APIException) if (ex is APIException)
{ {
LastLoginError = ex; LastLoginError = ex;
log.Add("Login failed on local user retrieval!"); log.Add($@"Login failed for username {ProvidedUsername} on user retrieval ({LastLoginError.Message})!");
Logout(); Logout();
} }
else if (ex is WebException webException && webException.Message == @"Unauthorized") else if (ex is WebException webException && webException.Message == @"Unauthorized")
@ -175,81 +233,43 @@ namespace osu.Game.Online.API
Logout(); Logout();
} }
else else
failConnectionProcess();
};
userReq.Success += u =>
{ {
localUser.Value = u; state.Value = APIState.Failing;
}
};
userReq.Success += user =>
{
// todo: save/pull from settings // todo: save/pull from settings
localUser.Value.Status.Value = new UserStatusOnline(); user.Status.Value = new UserStatusOnline();
setLocalUser(user);
// we're connected!
state.Value = APIState.Online;
failureCount = 0; failureCount = 0;
}; };
if (!handleRequest(userReq)) if (!handleRequest(userReq))
{ {
failConnectionProcess(); state.Value = APIState.Failing;
continue; return;
} }
// getting user's friends is considered part of the connection process.
var friendsReq = new GetFriendsRequest(); var friendsReq = new GetFriendsRequest();
friendsReq.Failure += _ => state.Value = APIState.Failing;
friendsReq.Failure += _ => failConnectionProcess(); friendsReq.Success += res => friends.AddRange(res);
friendsReq.Success += res =>
{
friends.AddRange(res);
//we're connected!
state.Value = APIState.Online;
};
if (!handleRequest(friendsReq)) if (!handleRequest(friendsReq))
{ {
failConnectionProcess(); state.Value = APIState.Failing;
continue; return;
} }
// The Success callback event is fired on the main thread, so we should wait for that to run before proceeding. // The Success callback event is fired on the main thread, so we should wait for that to run before proceeding.
// Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests // Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests
// before actually going online. // before actually going online.
while (State.Value > APIState.Offline && State.Value < APIState.Online) while (State.Value == APIState.Connecting && !cancellationToken.IsCancellationRequested)
Thread.Sleep(500); Thread.Sleep(500);
break;
}
// hard bail if we can't get a valid access token.
if (authentication.RequestAccessToken() == null)
{
Logout();
continue;
}
while (true)
{
APIRequest req;
lock (queue)
{
if (queue.Count == 0) break;
req = queue.Dequeue();
}
handleRequest(req);
}
Thread.Sleep(50);
}
void failConnectionProcess()
{
// if something went wrong during the connection process, we want to reset the state (but only if still connecting).
if (State.Value == APIState.Connecting)
state.Value = APIState.Failing;
}
} }
public void Perform(APIRequest request) public void Perform(APIRequest request)
@ -327,8 +347,7 @@ namespace osu.Game.Online.API
if (req.CompletionState != APIRequestCompletionState.Completed) if (req.CompletionState != APIRequestCompletionState.Completed)
return false; return false;
// we could still be in initialisation, at which point we don't want to say we're Online yet. // Reset failure count if this request succeeded.
if (IsLoggedIn) state.Value = APIState.Online;
failureCount = 0; failureCount = 0;
return true; return true;
} }
@ -402,7 +421,7 @@ namespace osu.Game.Online.API
} }
} }
public bool IsLoggedIn => localUser.Value.Id > 1; // TODO: should this also be true if attempting to connect? public bool IsLoggedIn => State.Value > APIState.Offline;
public void Queue(APIRequest request) public void Queue(APIRequest request)
{ {
@ -442,7 +461,7 @@ namespace osu.Game.Online.API
// Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present // Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present
Schedule(() => Schedule(() =>
{ {
localUser.Value = createGuestUser(); setLocalUser(createGuestUser());
friends.Clear(); friends.Clear();
}); });
@ -452,6 +471,8 @@ namespace osu.Game.Online.API
private static APIUser createGuestUser() => new GuestUser(); private static APIUser createGuestUser() => new GuestUser();
private void setLocalUser(APIUser user) => Scheduler.Add(() => localUser.Value = user, false);
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);

View File

@ -1,11 +1,8 @@
// 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 System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using MessagePack; using MessagePack;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -23,7 +20,7 @@ namespace osu.Game.Online.API
{ {
[JsonProperty("acronym")] [JsonProperty("acronym")]
[Key(0)] [Key(0)]
public string Acronym { get; set; } public string Acronym { get; set; } = string.Empty;
[JsonProperty("settings")] [JsonProperty("settings")]
[Key(1)] [Key(1)]
@ -49,7 +46,7 @@ namespace osu.Game.Online.API
} }
} }
public Mod ToMod([NotNull] Ruleset ruleset) public Mod ToMod(Ruleset ruleset)
{ {
Mod resultMod = ruleset.CreateModFromAcronym(Acronym); Mod resultMod = ruleset.CreateModFromAcronym(Acronym);
@ -80,6 +77,8 @@ namespace osu.Game.Online.API
return resultMod; return resultMod;
} }
public bool ShouldSerializeSettings() => Settings.Count > 0;
public bool Equals(APIMod other) public bool Equals(APIMod other)
{ {
if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(null, other)) return false;

View File

@ -13,19 +13,16 @@ namespace osu.Game.Online.API
{ {
/// <summary> /// <summary>
/// The local user. /// The local user.
/// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
/// </summary> /// </summary>
IBindable<APIUser> LocalUser { get; } IBindable<APIUser> LocalUser { get; }
/// <summary> /// <summary>
/// The user's friends. /// The user's friends.
/// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
/// </summary> /// </summary>
IBindableList<APIUser> Friends { get; } IBindableList<APIUser> Friends { get; }
/// <summary> /// <summary>
/// The current user's activity. /// The current user's activity.
/// This is not thread-safe and should be scheduled locally if consumed from a drawable component.
/// </summary> /// </summary>
IBindable<UserActivity> Activity { get; } IBindable<UserActivity> Activity { get; }

Some files were not shown because too many files have changed in this diff Show More