diff --git a/.github/ISSUE_TEMPLATE/01-bug-issues.md b/.github/ISSUE_TEMPLATE/01-bug-issues.md
index 6050036cbf..e45893b97a 100644
--- a/.github/ISSUE_TEMPLATE/01-bug-issues.md
+++ b/.github/ISSUE_TEMPLATE/01-bug-issues.md
@@ -1,7 +1,18 @@
---
name: Bug Report
-about: Issues regarding encountered bugs.
+about: Report a bug or crash to desktop
---
+
+
+
+
**Describe the bug:**
**Screenshots or videos showing encountered issue:**
@@ -9,6 +20,7 @@ about: Issues regarding encountered bugs.
**osu!lazer version:**
**Logs:**
+
-
-**Computer Specifications:**
diff --git a/.github/ISSUE_TEMPLATE/03-feature-request-issues.md b/.github/ISSUE_TEMPLATE/02-feature-request-issues.md
similarity index 62%
rename from .github/ISSUE_TEMPLATE/03-feature-request-issues.md
rename to .github/ISSUE_TEMPLATE/02-feature-request-issues.md
index 54c4ff94e5..c3357dd780 100644
--- a/.github/ISSUE_TEMPLATE/03-feature-request-issues.md
+++ b/.github/ISSUE_TEMPLATE/02-feature-request-issues.md
@@ -1,6 +1,6 @@
---
name: Feature Request
-about: Features you would like to see in the game!
+about: Propose a feature you would like to see in the game!
---
**Describe the new feature:**
diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___legacy_osuTK_.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___legacy_osuTK_.xml
deleted file mode 100644
index 9ece926b34..0000000000
--- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___legacy_osuTK_.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/osu.Android.props b/osu.Android.props
index e0392bd687..75ac298626 100644
--- a/osu.Android.props
+++ b/osu.Android.props
@@ -52,6 +52,6 @@
-
+
diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj
index d9d23dea6b..3e0f0cb7f6 100644
--- a/osu.Desktop/osu.Desktop.csproj
+++ b/osu.Desktop/osu.Desktop.csproj
@@ -27,8 +27,8 @@
-
-
+
+
diff --git a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
index 42f70151ac..728af5124e 100644
--- a/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
+++ b/osu.Game.Rulesets.Catch.Tests/osu.Game.Rulesets.Catch.Tests.csproj
@@ -5,7 +5,7 @@
-
+
WinExe
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs
index a5248c7712..399a46aa77 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneAutoGeneration.cs
@@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.Tests
Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
- Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
+ Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Special1), "Special1 has not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Special1), "Special1 has not been released");
}
@@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Mania.Tests
Assert.IsTrue(generated.Frames.Count == frame_offset + 2, "Replay must have 3 frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect hit time");
- Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
+ Assert.AreEqual(3000, generated.Frames[frame_offset + 1].Time, "Incorrect release time");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been released");
@@ -148,9 +148,9 @@ namespace osu.Game.Rulesets.Mania.Tests
Assert.IsTrue(generated.Frames.Count == frame_offset + 4, "Replay must have 4 generated frames");
Assert.AreEqual(1000, generated.Frames[frame_offset].Time, "Incorrect first note hit time");
- Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 2].Time, "Incorrect first note release time");
+ Assert.AreEqual(3000, generated.Frames[frame_offset + 2].Time, "Incorrect first note release time");
Assert.AreEqual(2000, generated.Frames[frame_offset + 1].Time, "Incorrect second note hit time");
- Assert.AreEqual(4000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[frame_offset + 3].Time, "Incorrect second note release time");
+ Assert.AreEqual(4000, generated.Frames[frame_offset + 3].Time, "Incorrect second note release time");
Assert.IsTrue(checkContains(generated.Frames[frame_offset], ManiaAction.Key1), "Key1 has not been pressed");
Assert.IsTrue(checkContains(generated.Frames[frame_offset + 1], ManiaAction.Key1, ManiaAction.Key2), "Key1 & Key2 have not been pressed");
Assert.IsFalse(checkContains(generated.Frames[frame_offset + 2], ManiaAction.Key1), "Key1 has not been released");
@@ -168,7 +168,7 @@ namespace osu.Game.Rulesets.Mania.Tests
// | | |
var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 2 });
- beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 - ManiaAutoGenerator.RELEASE_DELAY });
+ beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 });
beatmap.HitObjects.Add(new Note { StartTime = 3000, Column = 1 });
var generated = new ManiaAutoGenerator(beatmap).Generate();
diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
index 596430f9e5..7ae69bf7d7 100644
--- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
+++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs
@@ -5,11 +5,13 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
+using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Objects;
+using osu.Game.Rulesets.Mania.Objects.Drawables;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Objects;
@@ -345,6 +347,14 @@ namespace osu.Game.Rulesets.Mania.Tests
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
+
+ AddUntilStep("wait for head", () => currentPlayer.GameplayClockContainer.GameplayClock.CurrentTime >= time_head);
+ AddAssert("head is visible",
+ () => currentPlayer.ChildrenOfType()
+ .Single(note => note.HitObject == beatmap.HitObjects[0])
+ .Head
+ .Alpha == 1);
+
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
}
@@ -352,6 +362,8 @@ namespace osu.Game.Rulesets.Mania.Tests
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
+ public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer;
+
protected override bool PauseOnFocusLost => false;
public ScoreAccessibleReplayPlayer(Score score)
diff --git a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
index e51b20c9fe..af16f39563 100644
--- a/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
+++ b/osu.Game.Rulesets.Mania.Tests/osu.Game.Rulesets.Mania.Tests.csproj
@@ -5,7 +5,7 @@
-
+
WinExe
diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
index 75dcf0e55e..35ba2465fa 100644
--- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
+++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using osu.Game.Rulesets.Objects.Drawables;
+
namespace osu.Game.Rulesets.Mania.Objects.Drawables
{
///
@@ -25,6 +27,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
LifetimeEnd = LifetimeStart + 30000;
}
+ protected override void UpdateHitStateTransforms(ArmedState state)
+ {
+ // suppress the base call explicitly.
+ // the hold note head should never change its visual state on its own due to the "freezing" mechanic
+ // (when hit, it remains visible in place at the judgement line; when dropped, it will scroll past the line).
+ // it will be hidden along with its parenting hold note when required.
+ }
+
public override bool OnPressed(ManiaAction action) => false; // Handled by the hold note
public override void OnReleased(ManiaAction action)
diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
index 3ebbe5af8e..7c51d58b74 100644
--- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
+++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using osu.Game.Replays;
using osu.Game.Rulesets.Mania.Beatmaps;
+using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays;
@@ -85,20 +86,28 @@ namespace osu.Game.Rulesets.Mania.Replays
{
var currentObject = Beatmap.HitObjects[i];
var nextObjectInColumn = GetNextObject(i); // Get the next object that requires pressing the same button
-
- double endTime = currentObject.GetEndTime();
-
- bool canDelayKeyUp = nextObjectInColumn == null ||
- nextObjectInColumn.StartTime > endTime + RELEASE_DELAY;
-
- double calculatedDelay = canDelayKeyUp ? RELEASE_DELAY : (nextObjectInColumn.StartTime - endTime) * 0.9;
+ var releaseTime = calculateReleaseTime(currentObject, nextObjectInColumn);
yield return new HitPoint { Time = currentObject.StartTime, Column = currentObject.Column };
- yield return new ReleasePoint { Time = endTime + calculatedDelay, Column = currentObject.Column };
+ yield return new ReleasePoint { Time = releaseTime, Column = currentObject.Column };
}
}
+ private double calculateReleaseTime(HitObject currentObject, HitObject nextObject)
+ {
+ double endTime = currentObject.GetEndTime();
+
+ if (currentObject is HoldNote)
+ // hold note releases must be timed exactly.
+ return endTime;
+
+ bool canDelayKeyUpFully = nextObject == null ||
+ nextObject.StartTime > endTime + RELEASE_DELAY;
+
+ return endTime + (canDelayKeyUpFully ? RELEASE_DELAY : (nextObject.StartTime - endTime) * 0.9);
+ }
+
protected override HitObject GetNextObject(int currentIndex)
{
int desiredColumn = Beatmap.HitObjects[currentIndex].Column;
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs
index 73aece1ed4..e4d466dca5 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs
@@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
public class LegacyHitExplosion : LegacyManiaColumnElement, IHitExplosion
{
+ public const double FADE_IN_DURATION = 80;
+
private readonly IBindable direction = new Bindable();
private Drawable explosion;
@@ -72,7 +74,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
(explosion as IFramedAnimation)?.GotoFrame(0);
- explosion?.FadeInFromZero(80)
+ explosion?.FadeInFromZero(FADE_IN_DURATION)
.Then().FadeOut(120);
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs
index 78ccb83a8c..10319a7d4d 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs
@@ -101,8 +101,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
{
if (action == column.Action.Value)
{
- upSprite.FadeTo(1);
- downSprite.FadeTo(0);
+ upSprite.Delay(LegacyHitExplosion.FADE_IN_DURATION).FadeTo(1);
+ downSprite.Delay(LegacyHitExplosion.FADE_IN_DURATION).FadeTo(0);
}
}
}
diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
index cbbbacfe19..24ccae895d 100644
--- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs
@@ -140,7 +140,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
return animation == null ? null : new LegacyManiaJudgementPiece(result, animation);
}
- public override Sample GetSample(ISampleInfo sampleInfo)
+ public override ISample GetSample(ISampleInfo sampleInfo)
{
// layered hit sounds never play in mania
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered)
diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
index a365ea10d4..c2119585ab 100644
--- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
+++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs
@@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Osu.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
- [TestCase(6.9311451172608853d, "diffcalc-test")]
- [TestCase(1.0736587013228804d, "zero-length-sliders")]
+ [TestCase(6.9311451172574934d, "diffcalc-test")]
+ [TestCase(1.0736586907780401d, "zero-length-sliders")]
public void Test(double expected, string name)
=> base.Test(expected, name);
- [TestCase(8.6228371119393064d, "diffcalc-test")]
- [TestCase(1.2864585434597433d, "zero-length-sliders")]
+ [TestCase(8.6228371119271454d, "diffcalc-test")]
+ [TestCase(1.2864585280364178d, "zero-length-sliders")]
public void TestClockRateAdjusted(double expected, string name)
=> Test(expected, name, new OsuModDoubleTime());
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
index e2d9f144c0..8fd13c7417 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs
@@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Tests
return null;
}
- public Sample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
+ public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException();
diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
index 56f6fb85fa..6c6f05c5c5 100644
--- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
+++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs
@@ -162,7 +162,7 @@ namespace osu.Game.Rulesets.Osu.Tests
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
- public Sample GetSample(ISampleInfo sampleInfo) => null;
+ public ISample GetSample(ISampleInfo sampleInfo) => null;
public TValue GetValue(Func query) where TConfiguration : SkinConfiguration => default;
public IBindable GetConfig(TLookup lookup) => null;
diff --git a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
index f1f75148ef..3d2d1f3fec 100644
--- a/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
+++ b/osu.Game.Rulesets.Osu.Tests/osu.Game.Rulesets.Osu.Tests.csproj
@@ -5,7 +5,7 @@
-
+
WinExe
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
index e9838de63d..1390675a1a 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs
@@ -7,11 +7,13 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit;
using osuTK;
@@ -23,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
///
/// A visualisation of a single in a .
///
- public class PathControlPointPiece : BlueprintPiece
+ public class PathControlPointPiece : BlueprintPiece, IHasTooltip
{
public Action RequestSelection;
@@ -195,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
markerRing.Alpha = IsSelected.Value ? 1 : 0;
- Color4 colour = ControlPoint.Type.Value != null ? colours.Red : colours.Yellow;
+ Color4 colour = getColourFromNodeType();
if (IsHovered || IsSelected.Value)
colour = colour.Lighten(1);
@@ -203,5 +205,28 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
marker.Colour = colour;
marker.Scale = new Vector2(slider.Scale);
}
+
+ private Color4 getColourFromNodeType()
+ {
+ if (!(ControlPoint.Type.Value is PathType pathType))
+ return colours.Yellow;
+
+ switch (pathType)
+ {
+ case PathType.Catmull:
+ return colours.Seafoam;
+
+ case PathType.Bezier:
+ return colours.Pink;
+
+ case PathType.PerfectCurve:
+ return colours.PurpleDark;
+
+ default:
+ return colours.Red;
+ }
+ }
+
+ public string TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty;
}
}
diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
index 3d3dff653a..ba9bb3c485 100644
--- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
+++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -28,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected SliderBodyPiece BodyPiece { get; private set; }
protected SliderCircleSelectionBlueprint HeadBlueprint { get; private set; }
protected SliderCircleSelectionBlueprint TailBlueprint { get; private set; }
+
+ [CanBeNull]
protected PathControlPointVisualiser ControlPointVisualiser { get; private set; }
private readonly DrawableSlider slider;
@@ -114,6 +117,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
// throw away frame buffers on deselection.
ControlPointVisualiser?.Expire();
+ ControlPointVisualiser = null;
+
BodyPiece.RecyclePath();
}
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
index 77094f928b..189003875d 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs
@@ -164,28 +164,29 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ApproachCircle.Expire(true);
}
+ protected override void UpdateStartTimeStateTransforms()
+ {
+ base.UpdateStartTimeStateTransforms();
+
+ ApproachCircle.FadeOut(50);
+ }
+
protected override void UpdateHitStateTransforms(ArmedState state)
{
Debug.Assert(HitObject.HitWindows != null);
+ // todo: temporary / arbitrary, used for lifetime optimisation.
+ this.Delay(800).FadeOut();
+
switch (state)
{
case ArmedState.Idle:
- this.Delay(HitObject.TimePreempt).FadeOut(500);
HitArea.HitAction = null;
break;
case ArmedState.Miss:
- ApproachCircle.FadeOut(50);
this.FadeOut(100);
break;
-
- case ArmedState.Hit:
- ApproachCircle.FadeOut(50);
-
- // todo: temporary / arbitrary
- this.Delay(800).FadeOut();
- break;
}
Expire();
diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
index 3d614c2dbd..32a0a14dc0 100644
--- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
+++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs
@@ -39,6 +39,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private Bindable isSpinning;
private bool spinnerFrequencyModulate;
+ private const double fade_out_duration = 160;
+
public DrawableSpinner()
: this(null)
{
@@ -131,12 +133,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (tracking.NewValue)
{
if (!spinningSample.IsPlaying)
- spinningSample?.Play();
- spinningSample?.VolumeTo(1, 300);
+ spinningSample.Play();
+
+ spinningSample.VolumeTo(1, 300);
}
else
{
- spinningSample?.VolumeTo(0, 300).OnComplete(_ => spinningSample.Stop());
+ spinningSample.VolumeTo(0, fade_out_duration);
}
}
@@ -173,7 +176,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.UpdateHitStateTransforms(state);
- this.FadeOut(160).Expire();
+ this.FadeOut(fade_out_duration).OnComplete(_ =>
+ {
+ // looping sample should be stopped here as it is safer than running in the OnComplete
+ // of the volume transition above.
+ spinningSample.Stop();
+ });
+
+ Expire();
// skin change does a rewind of transforms, which will stop the spinning sound from playing if it's currently in playback.
isSpinning?.TriggerChange();
diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json
index 96e4bf1637..1a0bd66246 100644
--- a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json
+++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve-expected-conversion.json
@@ -1,5 +1,18 @@
{
"Mappings": [{
+ "StartTime": 114993,
+ "Objects": [{
+ "StartTime": 114993,
+ "EndTime": 114993,
+ "X": 493,
+ "Y": 92
+ }, {
+ "StartTime": 115290,
+ "EndTime": 115290,
+ "X": 451.659241,
+ "Y": 267.188
+ }]
+ }, {
"StartTime": 118858.0,
"Objects": [{
"StartTime": 118858.0,
diff --git a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu
index 8c3edc9571..dd35098502 100644
--- a/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu
+++ b/osu.Game.Rulesets.Osu/Resources/Testing/Beatmaps/colinear-perfect-curve.osu
@@ -9,7 +9,9 @@ SliderMultiplier:1.87
SliderTickRate:1
[TimingPoints]
-49051,230.769230769231,4,2,1,15,1,0
+114000,346.820809248555,4,2,1,71,1,0
+118000,230.769230769231,4,2,1,15,1,0
[HitObjects]
+493,92,114993,2,0,P|472:181|442:308,1,180,12|0,0:0|0:0,0:0:0:0:
219,215,118858,2,0,P|224:170|244:-10,1,187,8|2,0:0|0:0,0:0:0:0:
diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs
index fcbe4c1b28..46aeadc59b 100644
--- a/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs
+++ b/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs
@@ -74,10 +74,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
private void updateState(DrawableHitObject drawableObject, ArmedState state)
{
- using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true))
- {
+ using (BeginAbsoluteSequence(drawableObject.StateUpdateTime))
glow.FadeOut(400);
+ using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
+ {
switch (state)
{
case ArmedState.Hit:
diff --git a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
index c9a320bdd5..fa00922706 100644
--- a/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
+++ b/osu.Game.Rulesets.Taiko.Tests/osu.Game.Rulesets.Taiko.Tests.csproj
@@ -5,7 +5,7 @@
-
+
WinExe
diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
index d97da40ef2..3e506f69ce 100644
--- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
+++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs
@@ -152,7 +152,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
throw new ArgumentOutOfRangeException(nameof(component), $"Invalid component type: {component}");
}
- public override Sample GetSample(ISampleInfo sampleInfo) => Source.GetSample(new LegacyTaikoSampleInfo(sampleInfo));
+ public override ISample GetSample(ISampleInfo sampleInfo) => Source.GetSample(new LegacyTaikoSampleInfo(sampleInfo));
public override IBindable GetConfig(TLookup lookup) => Source.GetConfig(lookup);
diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
index 3ded3009bd..883791c35c 100644
--- a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs
@@ -121,7 +121,7 @@ namespace osu.Game.Tests.Gameplay
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
- public Sample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
+ public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
public IBindable GetConfig(TLookup lookup)
{
diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
index 10a1a13ba0..cae5f20332 100644
--- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
+++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs
@@ -36,7 +36,7 @@ namespace osu.Game.Tests.Gameplay
public void TestRetrieveTopLevelSample()
{
ISkin skin = null;
- Sample channel = null;
+ ISample channel = null;
AddStep("create skin", () => skin = new TestSkin("test-sample", this));
AddStep("retrieve sample", () => channel = skin.GetSample(new SampleInfo("test-sample")));
@@ -48,7 +48,7 @@ namespace osu.Game.Tests.Gameplay
public void TestRetrieveSampleInSubFolder()
{
ISkin skin = null;
- Sample channel = null;
+ ISample channel = null;
AddStep("create skin", () => skin = new TestSkin("folder/test-sample", this));
AddStep("retrieve sample", () => channel = skin.GetSample(new SampleInfo("folder/test-sample")));
diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs
index da004b9088..b08a228de3 100644
--- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs
+++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs
@@ -59,7 +59,7 @@ namespace osu.Game.Tests.NonVisual.Skinning
}
public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotSupportedException();
- public Sample GetSample(ISampleInfo sampleInfo) => throw new NotSupportedException();
+ public ISample GetSample(ISampleInfo sampleInfo) => throw new NotSupportedException();
public IBindable GetConfig(TLookup lookup) => throw new NotSupportedException();
}
diff --git a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs
index 42948c3731..aa29d76843 100644
--- a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs
+++ b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs
@@ -23,8 +23,10 @@ namespace osu.Game.Tests.Online
{
case CommentVoteRequest cRequest:
cRequest.TriggerSuccess(new CommentBundle());
- break;
+ return true;
}
+
+ return false;
});
CommentVoteRequest request = null;
@@ -108,8 +110,10 @@ namespace osu.Game.Tests.Online
{
case LeaveChannelRequest cRequest:
cRequest.TriggerSuccess();
- break;
+ return true;
}
+
+ return false;
});
}
}
diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
index a5b4b04ef5..8124bd4199 100644
--- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
+++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs
@@ -113,6 +113,31 @@ namespace osu.Game.Tests.Skins.IO
}
}
+ [Test]
+ public async Task TestImportUpperCasedOskArchive()
+ {
+ using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest)))
+ {
+ try
+ {
+ var osu = LoadOsuIntoHost(host);
+
+ var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("name 1", "author 1"), "skin1.OsK"));
+
+ Assert.That(imported.Name, Is.EqualTo("name 1"));
+ Assert.That(imported.Creator, Is.EqualTo("author 1"));
+
+ var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("name 1", "author 1"), "skin1.oSK"));
+
+ Assert.That(imported2.Hash, Is.EqualTo(imported.Hash));
+ }
+ finally
+ {
+ host.Exit();
+ }
+ }
+ }
+
private MemoryStream createOsk(string name, string author)
{
var zipStream = new MemoryStream();
diff --git a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
index 414f7d3f88..732a3f3f42 100644
--- a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
+++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs
@@ -219,7 +219,7 @@ namespace osu.Game.Tests.Skins
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => skin.GetTexture(componentName, wrapModeS, wrapModeT);
- public Sample GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo);
+ public ISample GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo);
public IBindable GetConfig(TLookup lookup) => skin.GetConfig(lookup);
}
diff --git a/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs
index e7cf830db0..dc5a4f4a3e 100644
--- a/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs
+++ b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs
@@ -135,13 +135,15 @@ namespace osu.Game.Tests.Visual.Background
dummyAPI.HandleRequest = request =>
{
if (dummyAPI.State.Value != APIState.Online || !(request is GetSeasonalBackgroundsRequest backgroundsRequest))
- return;
+ return false;
backgroundsRequest.TriggerSuccess(new APISeasonalBackgrounds
{
Backgrounds = seasonal_background_urls.Select(url => new APISeasonalBackground { Url = url }).ToList(),
EndDate = endDate
});
+
+ return true;
};
});
diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelection.cs b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelection.cs
new file mode 100644
index 0000000000..fd9c09fd5f
--- /dev/null
+++ b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelection.cs
@@ -0,0 +1,70 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Linq;
+using NUnit.Framework;
+using osu.Framework.Testing;
+using osu.Game.Beatmaps;
+using osu.Game.Rulesets;
+using osu.Game.Rulesets.Objects;
+using osu.Game.Rulesets.Osu;
+using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components;
+using osu.Game.Rulesets.Osu.Objects;
+using osu.Game.Screens.Edit.Compose.Components;
+using osu.Game.Tests.Beatmaps;
+using osuTK;
+using osuTK.Input;
+
+namespace osu.Game.Tests.Visual.Editing
+{
+ public class TestSceneBlueprintSelection : EditorTestScene
+ {
+ protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
+
+ protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
+
+ private BlueprintContainer blueprintContainer
+ => Editor.ChildrenOfType().First();
+
+ [Test]
+ public void TestSelectedObjectHasPriorityWhenOverlapping()
+ {
+ var firstSlider = new Slider
+ {
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(new Vector2()),
+ new PathControlPoint(new Vector2(150, -50)),
+ new PathControlPoint(new Vector2(300, 0))
+ }),
+ Position = new Vector2(0, 100)
+ };
+ var secondSlider = new Slider
+ {
+ Path = new SliderPath(new[]
+ {
+ new PathControlPoint(new Vector2()),
+ new PathControlPoint(new Vector2(-50, 50)),
+ new PathControlPoint(new Vector2(-100, 100))
+ }),
+ Position = new Vector2(200, 0)
+ };
+
+ AddStep("add overlapping sliders", () =>
+ {
+ EditorBeatmap.Add(firstSlider);
+ EditorBeatmap.Add(secondSlider);
+ });
+ AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.Add(firstSlider));
+
+ AddStep("move mouse to common point", () =>
+ {
+ var pos = blueprintContainer.ChildrenOfType().ElementAt(1).ScreenSpaceDrawQuad.Centre;
+ InputManager.MoveMouseTo(pos);
+ });
+ AddStep("right click", () => InputManager.Click(MouseButton.Right));
+
+ AddAssert("selection is unchanged", () => EditorBeatmap.SelectedHitObjects.Single() == firstSlider);
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
index 44142b69d7..7a6e2f54c2 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs
@@ -298,7 +298,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
- public Sample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
+ public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException();
}
@@ -309,7 +309,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
- public Sample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
+ public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException();
}
@@ -321,7 +321,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException();
- public Sample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
+ public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
public IBindable GetConfig(TLookup lookup) => throw new NotImplementedException();
diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
index d688e9cb21..d792405eeb 100644
--- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
+++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs
@@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public Drawable GetDrawableComponent(ISkinComponent component) => source?.GetDrawableComponent(component);
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT);
- public Sample GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo);
+ public ISample GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo);
public IBindable GetConfig(TLookup lookup) => source?.GetConfig(lookup);
public void TriggerSourceChanged()
diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
index 8cfe5d8af2..faa5d9e6fc 100644
--- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
+++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs
@@ -56,7 +56,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
beatmaps.Add(new BeatmapInfo
{
Ruleset = rulesets.GetRuleset(i % 4),
- RulesetID = i % 4, // workaround for efcore 5 compatibility.
OnlineBeatmapID = beatmapId,
Length = length,
BPM = bpm,
diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs
index 21d3bdaae3..2791952b66 100644
--- a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs
+++ b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs
@@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Navigation
public void TestPerformAtSongSelectFromPlayerLoader()
{
PushAndConfirm(() => new PlaySongSelect());
- PushAndConfirm(() => new PlayerLoader(() => new Player()));
+ PushAndConfirm(() => new PlayerLoader(() => new SoloPlayer()));
AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(PlaySongSelect) }));
AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
@@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Navigation
public void TestPerformAtMenuFromPlayerLoader()
{
PushAndConfirm(() => new PlaySongSelect());
- PushAndConfirm(() => new PlayerLoader(() => new Player()));
+ PushAndConfirm(() => new PlayerLoader(() => new SoloPlayer()));
AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true));
AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is MainMenu);
diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
index 1349264bf9..156d6b744e 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs
@@ -30,13 +30,14 @@ namespace osu.Game.Tests.Visual.Online
((DummyAPIAccess)API).HandleRequest = req =>
{
- if (req is SearchBeatmapSetsRequest searchBeatmapSetsRequest)
+ if (!(req is SearchBeatmapSetsRequest searchBeatmapSetsRequest)) return false;
+
+ searchBeatmapSetsRequest.TriggerSuccess(new SearchBeatmapSetsResponse
{
- searchBeatmapSetsRequest.TriggerSuccess(new SearchBeatmapSetsResponse
- {
- BeatmapSets = setsForResponse,
- });
- }
+ BeatmapSets = setsForResponse,
+ });
+
+ return true;
};
}
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs
index cd2c4e9346..8818ac75b1 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs
@@ -63,13 +63,15 @@ namespace osu.Game.Tests.Visual.Online
Builds = builds.Values.ToList()
};
changelogRequest.TriggerSuccess(changelogResponse);
- break;
+ return true;
case GetChangelogBuildRequest buildRequest:
if (requestedBuild != null)
buildRequest.TriggerSuccess(requestedBuild);
- break;
+ return true;
}
+
+ return false;
};
Child = changelog = new TestChangelogOverlay();
diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
index fca642ad6c..b13dd34ebc 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs
@@ -11,6 +11,8 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Online.API;
+using osu.Game.Online.API.Requests;
using osu.Game.Online.Chat;
using osu.Game.Overlays;
using osu.Game.Overlays.Chat.Selection;
@@ -64,6 +66,24 @@ namespace osu.Game.Tests.Visual.Online
});
}
+ [SetUpSteps]
+ public void SetUpSteps()
+ {
+ AddStep("register request handling", () =>
+ {
+ ((DummyAPIAccess)API).HandleRequest = req =>
+ {
+ switch (req)
+ {
+ case JoinChannelRequest _:
+ return true;
+ }
+
+ return false;
+ };
+ });
+ }
+
[Test]
public void TestHideOverlay()
{
diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs
index c2a18330c9..cd22bb2513 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs
@@ -85,9 +85,10 @@ namespace osu.Game.Tests.Visual.Online
dummyAPI.HandleRequest = request =>
{
if (!(request is GetCommentsRequest getCommentsRequest))
- return;
+ return false;
getCommentsRequest.TriggerSuccess(commentBundle);
+ return true;
};
});
diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs
index 37d51c16d2..6ebe8fcc07 100644
--- a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs
+++ b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs
@@ -33,9 +33,10 @@ namespace osu.Game.Tests.Visual.Online
dummyAPI.HandleRequest = request =>
{
if (!(request is GetNewsRequest getNewsRequest))
- return;
+ return false;
getNewsRequest.TriggerSuccess(r);
+ return true;
};
});
diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
index be8032cde8..61d49e4018 100644
--- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
+++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs
@@ -170,6 +170,17 @@ namespace osu.Game.Tests.Visual.Playlists
private void bindHandler(bool delayed = false, ScoreInfo userScore = null, bool failRequests = false) => ((DummyAPIAccess)API).HandleRequest = request =>
{
+ // pre-check for requests we should be handling (as they are scheduled below).
+ switch (request)
+ {
+ case ShowPlaylistUserScoreRequest _:
+ case IndexPlaylistScoresRequest _:
+ break;
+
+ default:
+ return false;
+ }
+
requestComplete = false;
double delay = delayed ? 3000 : 0;
@@ -196,6 +207,8 @@ namespace osu.Game.Tests.Visual.Playlists
break;
}
}, delay);
+
+ return true;
};
private void triggerSuccess(APIRequest req, T result)
diff --git a/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
new file mode 100644
index 0000000000..a7f6c8c0d3
--- /dev/null
+++ b/osu.Game.Tests/Visual/Settings/TestSceneTabletSettings.cs
@@ -0,0 +1,73 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using NUnit.Framework;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Input.Handlers.Tablet;
+using osu.Framework.Platform;
+using osu.Framework.Utils;
+using osu.Game.Overlays;
+using osu.Game.Overlays.Settings.Sections.Input;
+using osuTK;
+
+namespace osu.Game.Tests.Visual.Settings
+{
+ [TestFixture]
+ public class TestSceneTabletSettings : OsuTestScene
+ {
+ [BackgroundDependencyLoader]
+ private void load(GameHost host)
+ {
+ var tabletHandler = new TestTabletHandler();
+
+ AddRange(new Drawable[]
+ {
+ new TabletSettings(tabletHandler)
+ {
+ RelativeSizeAxes = Axes.None,
+ Width = SettingsPanel.WIDTH,
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ }
+ });
+
+ AddStep("Test with wide tablet", () => tabletHandler.SetTabletSize(new Vector2(160, 100)));
+ AddStep("Test with square tablet", () => tabletHandler.SetTabletSize(new Vector2(300, 300)));
+ AddStep("Test with tall tablet", () => tabletHandler.SetTabletSize(new Vector2(100, 300)));
+ AddStep("Test with very tall tablet", () => tabletHandler.SetTabletSize(new Vector2(100, 700)));
+ AddStep("Test no tablet present", () => tabletHandler.SetTabletSize(Vector2.Zero));
+ }
+
+ public class TestTabletHandler : ITabletHandler
+ {
+ public Bindable AreaOffset { get; } = new Bindable();
+ public Bindable AreaSize { get; } = new Bindable();
+
+ public IBindable Tablet => tablet;
+
+ private readonly Bindable tablet = new Bindable();
+
+ public BindableBool Enabled { get; } = new BindableBool(true);
+
+ public void SetTabletSize(Vector2 size)
+ {
+ tablet.Value = size != Vector2.Zero ? new TabletInfo($"test tablet T-{RNG.Next(999):000}", size) : null;
+
+ AreaSize.Default = new Vector2(size.X, size.Y);
+
+ // if it's clear the user has not configured the area, take the full area from the tablet that was just found.
+ if (AreaSize.Value == Vector2.Zero)
+ AreaSize.SetDefault();
+
+ AreaOffset.Default = new Vector2(size.X / 2, size.Y / 2);
+
+ // likewise with the position, use the centre point if it has not been configured.
+ // it's safe to assume no user would set their centre point to 0,0 for now.
+ if (AreaOffset.Value == Vector2.Zero)
+ AreaOffset.SetDefault();
+ }
+ }
+ }
+}
diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
index 9b8b74e6f6..5e2d5eba5d 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs
@@ -32,8 +32,10 @@ namespace osu.Game.Tests.Visual.SongSelect
{
case GetUserRequest userRequest:
userRequest.TriggerSuccess(getUser(userRequest.Ruleset.ID));
- break;
+ return true;
}
+
+ return false;
};
});
@@ -186,7 +188,6 @@ namespace osu.Game.Tests.Visual.SongSelect
Metadata = metadata,
BaseDifficulty = new BeatmapDifficulty(),
Ruleset = ruleset,
- RulesetID = ruleset.ID.GetValueOrDefault(), // workaround for efcore 5 compatibility.
StarDifficulty = difficultyIndex + 1,
Version = $"SR{difficultyIndex + 1}"
}).ToList()
diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
index 057b539e44..5731b1ac2c 100644
--- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
+++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs
@@ -911,11 +911,9 @@ namespace osu.Game.Tests.Visual.SongSelect
int length = RNG.Next(30000, 200000);
double bpm = RNG.NextSingle(80, 200);
- var ruleset = getRuleset();
beatmaps.Add(new BeatmapInfo
{
- Ruleset = ruleset,
- RulesetID = ruleset.ID.GetValueOrDefault(), // workaround for efcore 5 compatibility.
+ Ruleset = getRuleset(),
OnlineBeatmapID = beatmapId,
Version = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
Length = length,
diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj
index 6f8e0fac6f..e36b3cdc74 100644
--- a/osu.Game.Tests/osu.Game.Tests.csproj
+++ b/osu.Game.Tests/osu.Game.Tests.csproj
@@ -6,7 +6,7 @@
-
+
diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs
index fadb821bef..87e23e3404 100644
--- a/osu.Game.Tournament/TournamentGame.cs
+++ b/osu.Game.Tournament/TournamentGame.cs
@@ -2,18 +2,21 @@
// See the LICENCE file in the repository root for full licence text.
using System.Drawing;
-using osu.Framework.Extensions.Color4Extensions;
+using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
+using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
-using osu.Framework.Graphics.Colour;
-using osu.Game.Graphics.Cursor;
-using osu.Game.Tournament.Models;
+using osu.Framework.Input.Handlers.Mouse;
+using osu.Framework.Platform;
using osu.Game.Graphics;
+using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
+using osu.Game.Tournament.Models;
using osuTK;
using osuTK.Graphics;
@@ -36,7 +39,7 @@ namespace osu.Game.Tournament
private LoadingSpinner loadingSpinner;
[BackgroundDependencyLoader]
- private void load(FrameworkConfigManager frameworkConfig)
+ private void load(FrameworkConfigManager frameworkConfig, GameHost host)
{
windowSize = frameworkConfig.GetBindable(FrameworkSetting.WindowedSize);
windowMode = frameworkConfig.GetBindable(FrameworkSetting.WindowMode);
@@ -48,6 +51,13 @@ namespace osu.Game.Tournament
Margin = new MarginPadding(40),
});
+ // in order to have the OS mouse cursor visible, relative mode needs to be disabled.
+ // can potentially be removed when https://github.com/ppy/osu-framework/issues/4309 is resolved.
+ var mouseHandler = host.AvailableInputHandlers.OfType().FirstOrDefault();
+
+ if (mouseHandler != null)
+ mouseHandler.UseRelativeMode.Value = false;
+
loadingSpinner.Show();
BracketLoadTask.ContinueWith(_ => LoadComponentsAsync(new[]
diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs
index 115d1b33bb..b4ea898b7d 100644
--- a/osu.Game/Beatmaps/BeatmapManager.cs
+++ b/osu.Game/Beatmaps/BeatmapManager.cs
@@ -171,8 +171,6 @@ namespace osu.Game.Beatmaps
if (beatmapSet.Beatmaps.Any(b => b.BaseDifficulty == null))
throw new InvalidOperationException($"Cannot import {nameof(BeatmapInfo)} with null {nameof(BeatmapInfo.BaseDifficulty)}.");
- beatmapSet.Requery(ContextFactory);
-
// check if a set already exists with the same online id, delete if it does.
if (beatmapSet.OnlineBeatmapSetID != null)
{
diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
index 0bc5605051..73337ab6f5 100644
--- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
+++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Beatmaps.ControlPoints
///
public readonly BindableDouble SpeedMultiplierBindable = new BindableDouble(1)
{
- Precision = 0.1,
+ Precision = 0.01,
Default = 1,
MinValue = 0.1,
MaxValue = 10
diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
index 10a716963e..b39890084f 100644
--- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
+++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs
@@ -36,7 +36,13 @@ namespace osu.Game.Beatmaps.Formats
if (ShouldSkipLine(line))
continue;
- line = StripComments(line).TrimEnd();
+ if (section != Section.Metadata)
+ {
+ // comments should not be stripped from metadata lines, as the song metadata may contain "//" as valid data.
+ line = StripComments(line);
+ }
+
+ line = line.TrimEnd();
if (line.StartsWith('[') && line.EndsWith(']'))
{
diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs
index 64428882ac..d809dbcb01 100644
--- a/osu.Game/Database/ArchiveModelManager.cs
+++ b/osu.Game/Database/ArchiveModelManager.cs
@@ -462,8 +462,6 @@ namespace osu.Game.Database
// Dereference the existing file info, since the file model will be removed.
if (file.FileInfo != null)
{
- file.Requery(usage.Context);
-
Files.Dereference(file.FileInfo);
// This shouldn't be required, but here for safety in case the provided TModel is not being change tracked
@@ -637,12 +635,10 @@ namespace osu.Game.Database
{
using (Stream s = reader.GetStream(file))
{
- var fileInfo = files.Add(s);
fileInfos.Add(new TFileModel
{
Filename = file.Substring(prefix.Length).ToStandardisedPath(),
- FileInfo = fileInfo,
- FileInfoID = fileInfo.ID // workaround for efcore 5 compatibility.
+ FileInfo = files.Add(s)
});
}
}
diff --git a/osu.Game/Database/DatabaseWorkaroundExtensions.cs b/osu.Game/Database/DatabaseWorkaroundExtensions.cs
deleted file mode 100644
index a3a982f232..0000000000
--- a/osu.Game/Database/DatabaseWorkaroundExtensions.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using System.Collections.Generic;
-using osu.Game.Beatmaps;
-using osu.Game.Scoring;
-using osu.Game.Skinning;
-
-namespace osu.Game.Database
-{
- ///
- /// Extension methods which contain workarounds to make EFcore 5.x work with our existing (incorrect) thread safety.
- /// The intention is to avoid blocking package updates while we consider the future of the database backend, with a potential backend switch imminent.
- ///
- public static class DatabaseWorkaroundExtensions
- {
- ///
- /// Re-query the provided model to ensure it is in a sane state. This method requires explicit implementation per model type.
- ///
- ///
- ///
- public static void Requery(this IHasPrimaryKey model, IDatabaseContextFactory contextFactory)
- {
- switch (model)
- {
- case SkinInfo skinInfo:
- requeryFiles(skinInfo.Files, contextFactory);
- break;
-
- case ScoreInfo scoreInfo:
- requeryFiles(scoreInfo.Beatmap.BeatmapSet.Files, contextFactory);
- requeryFiles(scoreInfo.Files, contextFactory);
- break;
-
- case BeatmapSetInfo beatmapSetInfo:
- var context = contextFactory.Get();
-
- foreach (var beatmap in beatmapSetInfo.Beatmaps)
- {
- // Workaround System.InvalidOperationException
- // The instance of entity type 'RulesetInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
- beatmap.Ruleset = context.RulesetInfo.Find(beatmap.RulesetID);
- }
-
- requeryFiles(beatmapSetInfo.Files, contextFactory);
- break;
-
- default:
- throw new ArgumentException($"{nameof(Requery)} does not have support for the provided model type", nameof(model));
- }
-
- void requeryFiles(List files, IDatabaseContextFactory databaseContextFactory) where T : class, INamedFileInfo
- {
- var dbContext = databaseContextFactory.Get();
-
- foreach (var file in files)
- {
- Requery(file, dbContext);
- }
- }
- }
-
- public static void Requery(this INamedFileInfo file, OsuDbContext dbContext)
- {
- // Workaround System.InvalidOperationException
- // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked.
- file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID);
- }
- }
-}
diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs
index 2342ab07d4..2aae62edea 100644
--- a/osu.Game/Database/OsuDbContext.cs
+++ b/osu.Game/Database/OsuDbContext.cs
@@ -3,6 +3,7 @@
using System;
using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Logging;
using osu.Framework.Logging;
using osu.Framework.Statistics;
@@ -110,10 +111,10 @@ namespace osu.Game.Database
{
base.OnConfiguring(optionsBuilder);
optionsBuilder
- .UseSqlite(connectionString,
- sqliteOptions => sqliteOptions
- .CommandTimeout(10)
- .UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery))
+ // this is required for the time being due to the way we are querying in places like BeatmapStore.
+ // if we ever move to having consumers file their own .Includes, or get eager loading support, this could be re-enabled.
+ .ConfigureWarnings(warnings => warnings.Ignore(CoreEventId.IncludeIgnoredWarning))
+ .UseSqlite(connectionString, sqliteOptions => sqliteOptions.CommandTimeout(10))
.UseLoggerFactory(logger.Value);
}
diff --git a/osu.Game/IO/Serialization/Converters/Vector2Converter.cs b/osu.Game/IO/Serialization/Converters/Vector2Converter.cs
deleted file mode 100644
index 46447b607b..0000000000
--- a/osu.Game/IO/Serialization/Converters/Vector2Converter.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
-// See the LICENCE file in the repository root for full licence text.
-
-using System;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
-using osuTK;
-
-namespace osu.Game.IO.Serialization.Converters
-{
- ///
- /// A type of that serializes only the X and Y coordinates of a .
- ///
- public class Vector2Converter : JsonConverter
- {
- public override Vector2 ReadJson(JsonReader reader, Type objectType, Vector2 existingValue, bool hasExistingValue, JsonSerializer serializer)
- {
- var obj = JObject.Load(reader);
- return new Vector2((float)obj["x"], (float)obj["y"]);
- }
-
- public override void WriteJson(JsonWriter writer, Vector2 value, JsonSerializer serializer)
- {
- writer.WriteStartObject();
-
- writer.WritePropertyName("x");
- writer.WriteValue(value.X);
- writer.WritePropertyName("y");
- writer.WriteValue(value.Y);
-
- writer.WriteEndObject();
- }
- }
-}
diff --git a/osu.Game/IO/Serialization/IJsonSerializable.cs b/osu.Game/IO/Serialization/IJsonSerializable.cs
index ac95d47c4b..ba188963ea 100644
--- a/osu.Game/IO/Serialization/IJsonSerializable.cs
+++ b/osu.Game/IO/Serialization/IJsonSerializable.cs
@@ -1,8 +1,9 @@
// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
+using System.Collections.Generic;
using Newtonsoft.Json;
-using osu.Game.IO.Serialization.Converters;
+using osu.Framework.IO.Serialization;
namespace osu.Game.IO.Serialization
{
@@ -28,7 +29,7 @@ namespace osu.Game.IO.Serialization
Formatting = Formatting.Indented,
ObjectCreationHandling = ObjectCreationHandling.Replace,
DefaultValueHandling = DefaultValueHandling.IgnoreAndPopulate,
- Converters = new JsonConverter[] { new Vector2Converter() },
+ Converters = new List { new Vector2Converter() },
ContractResolver = new KeyContractResolver()
};
}
diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs
index a7174324d8..1a6868cfa4 100644
--- a/osu.Game/Online/API/APIRequest.cs
+++ b/osu.Game/Online/API/APIRequest.cs
@@ -131,8 +131,11 @@ namespace osu.Game.Online.API
{
}
+ private bool succeeded;
+
internal virtual void TriggerSuccess()
{
+ succeeded = true;
Success?.Invoke();
}
@@ -145,10 +148,7 @@ namespace osu.Game.Online.API
public void Fail(Exception e)
{
- if (WebRequest?.Completed == true)
- return;
-
- if (cancelled)
+ if (succeeded || cancelled)
return;
cancelled = true;
@@ -181,9 +181,13 @@ namespace osu.Game.Online.API
/// Whether we are in a failed or cancelled state.
private bool checkAndScheduleFailure()
{
- if (API == null || pendingFailure == null) return cancelled;
+ if (pendingFailure == null) return cancelled;
+
+ if (API == null)
+ pendingFailure();
+ else
+ API.Schedule(pendingFailure);
- API.Schedule(pendingFailure);
pendingFailure = null;
return true;
}
diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs
index 943b52db88..52f2365165 100644
--- a/osu.Game/Online/API/DummyAPIAccess.cs
+++ b/osu.Game/Online/API/DummyAPIAccess.cs
@@ -34,8 +34,9 @@ namespace osu.Game.Online.API
///
/// Provide handling logic for an arbitrary API request.
+ /// Should return true is a request was handled. If null or false return, the request will be failed with a .
///
- public Action HandleRequest;
+ public Func HandleRequest;
private readonly Bindable state = new Bindable(APIState.Online);
@@ -55,7 +56,12 @@ namespace osu.Game.Online.API
public virtual void Queue(APIRequest request)
{
- HandleRequest?.Invoke(request);
+ if (HandleRequest?.Invoke(request) != true)
+ {
+ // this will fail due to not receiving an APIAccess, and trigger a failure on the request.
+ // this is intended - any request in testing that needs non-failures should use HandleRequest.
+ request.Perform(this);
+ }
}
public void Perform(APIRequest request) => HandleRequest?.Invoke(request);
diff --git a/osu.Game/Online/Rooms/APIScoreToken.cs b/osu.Game/Online/Rooms/APIScoreToken.cs
index f652c1720d..6b559876de 100644
--- a/osu.Game/Online/Rooms/APIScoreToken.cs
+++ b/osu.Game/Online/Rooms/APIScoreToken.cs
@@ -8,6 +8,6 @@ namespace osu.Game.Online.Rooms
public class APIScoreToken
{
[JsonProperty("id")]
- public int ID { get; set; }
+ public long ID { get; set; }
}
}
diff --git a/osu.Game/Online/Solo/CreateSoloScoreRequest.cs b/osu.Game/Online/Solo/CreateSoloScoreRequest.cs
new file mode 100644
index 0000000000..ae5ac5e26c
--- /dev/null
+++ b/osu.Game/Online/Solo/CreateSoloScoreRequest.cs
@@ -0,0 +1,32 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Net.Http;
+using osu.Framework.IO.Network;
+using osu.Game.Online.API;
+using osu.Game.Online.Rooms;
+
+namespace osu.Game.Online.Solo
+{
+ public class CreateSoloScoreRequest : APIRequest
+ {
+ private readonly int beatmapId;
+ private readonly string versionHash;
+
+ public CreateSoloScoreRequest(int beatmapId, string versionHash)
+ {
+ this.beatmapId = beatmapId;
+ this.versionHash = versionHash;
+ }
+
+ protected override WebRequest CreateWebRequest()
+ {
+ var req = base.CreateWebRequest();
+ req.Method = HttpMethod.Post;
+ req.AddParameter("version_hash", versionHash);
+ return req;
+ }
+
+ protected override string Target => $@"solo/{beatmapId}/scores";
+ }
+}
diff --git a/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs
new file mode 100644
index 0000000000..98ba4fa052
--- /dev/null
+++ b/osu.Game/Online/Solo/SubmitSoloScoreRequest.cs
@@ -0,0 +1,45 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System.Net.Http;
+using Newtonsoft.Json;
+using osu.Framework.IO.Network;
+using osu.Game.Online.API;
+using osu.Game.Online.Rooms;
+using osu.Game.Scoring;
+
+namespace osu.Game.Online.Solo
+{
+ public class SubmitSoloScoreRequest : APIRequest
+ {
+ private readonly long scoreId;
+
+ private readonly int beatmapId;
+
+ private readonly ScoreInfo scoreInfo;
+
+ public SubmitSoloScoreRequest(int beatmapId, long scoreId, ScoreInfo scoreInfo)
+ {
+ this.beatmapId = beatmapId;
+ this.scoreId = scoreId;
+ this.scoreInfo = scoreInfo;
+ }
+
+ protected override WebRequest CreateWebRequest()
+ {
+ var req = base.CreateWebRequest();
+
+ req.ContentType = "application/json";
+ req.Method = HttpMethod.Put;
+
+ req.AddRaw(JsonConvert.SerializeObject(scoreInfo, new JsonSerializerSettings
+ {
+ ReferenceLoopHandling = ReferenceLoopHandling.Ignore
+ }));
+
+ return req;
+ }
+
+ protected override string Target => $@"solo/{beatmapId}/scores/{scoreId}";
+ }
+}
diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs
index 66b9141ce8..dd1fa32ad9 100644
--- a/osu.Game/OsuGame.cs
+++ b/osu.Game/OsuGame.cs
@@ -531,6 +531,13 @@ namespace osu.Game
SentryLogger.Dispose();
}
+ protected override IDictionary GetFrameworkConfigDefaults()
+ => new Dictionary
+ {
+ // General expectation that osu! starts in fullscreen by default (also gives the most predictable performance)
+ { FrameworkSetting.WindowMode, WindowMode.Fullscreen }
+ };
+
protected override void LoadComplete()
{
base.LoadComplete();
@@ -758,9 +765,15 @@ namespace osu.Game
{
otherOverlays.Where(o => o != overlay).ForEach(o => o.Hide());
- // show above others if not visible at all, else leave at current depth.
- if (!overlay.IsPresent)
+ // Partially visible so leave it at the current depth.
+ if (overlay.IsPresent)
+ return;
+
+ // Show above all other overlays.
+ if (overlay.IsLoaded)
overlayContent.ChangeChildDepth(overlay, (float)-Clock.CurrentTime);
+ else
+ overlay.Depth = (float)-Clock.CurrentTime;
}
private void forwardLoggedErrorsToNotifications()
diff --git a/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs b/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs
index 56588ef0a8..8c40d79f7a 100644
--- a/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs
+++ b/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs
@@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Comments
{
new SpriteIcon
{
- Icon = FontAwesome.Solid.Trash,
+ Icon = FontAwesome.Regular.TrashAlt,
Size = new Vector2(14),
},
countText = new OsuSpriteText
diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
index 662f55317b..fe61e532e1 100644
--- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
+++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs
@@ -3,6 +3,7 @@
using System;
using System.Linq;
+using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
@@ -113,7 +114,12 @@ namespace osu.Game.Overlays.Profile.Header
}
topLinkContainer.AddText("Contributed ");
- topLinkContainer.AddLink($@"{user.PostCount:#,##0} forum posts", $"{api.WebsiteRootUrl}/users/{user.Id}/posts", creationParameters: embolden);
+ topLinkContainer.AddLink("forum post".ToQuantity(user.PostCount, "#,##0"), $"{api.WebsiteRootUrl}/users/{user.Id}/posts", creationParameters: embolden);
+
+ addSpacer(topLinkContainer);
+
+ topLinkContainer.AddText("Posted ");
+ topLinkContainer.AddLink("comment".ToQuantity(user.CommentsCount, "#,##0"), $"{api.WebsiteRootUrl}/comments?user_id={user.Id}", creationParameters: embolden);
string websiteWithoutProtocol = user.Website;
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
new file mode 100644
index 0000000000..ecb8acce54
--- /dev/null
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletAreaSelection.cs
@@ -0,0 +1,185 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Graphics.Shapes;
+using osu.Framework.Input.Handlers.Tablet;
+using osu.Game.Graphics;
+using osu.Game.Graphics.Sprites;
+using osuTK;
+using osuTK.Graphics;
+
+namespace osu.Game.Overlays.Settings.Sections.Input
+{
+ public class TabletAreaSelection : CompositeDrawable
+ {
+ private readonly ITabletHandler handler;
+
+ private Container tabletContainer;
+ private Container usableAreaContainer;
+
+ private readonly Bindable areaOffset = new Bindable();
+ private readonly Bindable areaSize = new Bindable();
+
+ private readonly IBindable tablet = new Bindable();
+
+ private OsuSpriteText tabletName;
+
+ private Box usableFill;
+ private OsuSpriteText usableAreaText;
+
+ public TabletAreaSelection(ITabletHandler handler)
+ {
+ this.handler = handler;
+
+ Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS };
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ InternalChild = tabletContainer = new Container
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Masking = true,
+ CornerRadius = 5,
+ BorderThickness = 2,
+ BorderColour = colour.Gray3,
+ Children = new Drawable[]
+ {
+ new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Colour = colour.Gray1,
+ },
+ usableAreaContainer = new Container
+ {
+ Origin = Anchor.Centre,
+ Children = new Drawable[]
+ {
+ usableFill = new Box
+ {
+ RelativeSizeAxes = Axes.Both,
+ Alpha = 0.6f,
+ },
+ new Box
+ {
+ Colour = Color4.White,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Height = 5,
+ },
+ new Box
+ {
+ Colour = Color4.White,
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Width = 5,
+ },
+ usableAreaText = new OsuSpriteText
+ {
+ Anchor = Anchor.Centre,
+ Origin = Anchor.Centre,
+ Colour = Color4.White,
+ Font = OsuFont.Default.With(size: 12),
+ Y = 10
+ }
+ }
+ },
+ tabletName = new OsuSpriteText
+ {
+ Padding = new MarginPadding(3),
+ Font = OsuFont.Default.With(size: 8)
+ },
+ }
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ areaOffset.BindTo(handler.AreaOffset);
+ areaOffset.BindValueChanged(val =>
+ {
+ usableAreaContainer.MoveTo(val.NewValue, 100, Easing.OutQuint)
+ .OnComplete(_ => checkBounds()); // required as we are using SSDQ.
+ }, true);
+
+ areaSize.BindTo(handler.AreaSize);
+ areaSize.BindValueChanged(val =>
+ {
+ usableAreaContainer.ResizeTo(val.NewValue, 100, Easing.OutQuint)
+ .OnComplete(_ => checkBounds()); // required as we are using SSDQ.
+
+ int x = (int)val.NewValue.X;
+ int y = (int)val.NewValue.Y;
+ int commonDivider = greatestCommonDivider(x, y);
+
+ usableAreaText.Text = $"{(float)x / commonDivider}:{(float)y / commonDivider}";
+ }, true);
+
+ tablet.BindTo(handler.Tablet);
+ tablet.BindValueChanged(_ => Scheduler.AddOnce(updateTabletDetails));
+
+ updateTabletDetails();
+ // initial animation should be instant.
+ FinishTransforms(true);
+ }
+
+ private void updateTabletDetails()
+ {
+ tabletContainer.Size = tablet.Value?.Size ?? Vector2.Zero;
+ tabletName.Text = tablet.Value?.Name ?? string.Empty;
+ checkBounds();
+ }
+
+ private static int greatestCommonDivider(int a, int b)
+ {
+ while (b != 0)
+ {
+ int remainder = a % b;
+ a = b;
+ b = remainder;
+ }
+
+ return a;
+ }
+
+ [Resolved]
+ private OsuColour colour { get; set; }
+
+ private void checkBounds()
+ {
+ if (tablet.Value == null)
+ return;
+
+ var usableSsdq = usableAreaContainer.ScreenSpaceDrawQuad;
+
+ bool isWithinBounds = tabletContainer.ScreenSpaceDrawQuad.Contains(usableSsdq.TopLeft + new Vector2(1)) &&
+ tabletContainer.ScreenSpaceDrawQuad.Contains(usableSsdq.BottomRight - new Vector2(1));
+
+ usableFill.FadeColour(isWithinBounds ? colour.Blue : colour.RedLight, 100);
+ }
+
+ protected override void Update()
+ {
+ base.Update();
+
+ if (!(tablet.Value?.Size is Vector2 size))
+ return;
+
+ float fitX = size.X / (DrawWidth - Padding.Left - Padding.Right);
+ float fitY = size.Y / DrawHeight;
+
+ float adjust = MathF.Max(fitX, fitY);
+
+ tabletContainer.Scale = new Vector2(1 / adjust);
+ }
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
new file mode 100644
index 0000000000..bd0f7ddc4c
--- /dev/null
+++ b/osu.Game/Overlays/Settings/Sections/Input/TabletSettings.cs
@@ -0,0 +1,285 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Framework.Graphics;
+using osu.Framework.Graphics.Containers;
+using osu.Framework.Input.Handlers.Tablet;
+using osu.Framework.Platform;
+using osu.Framework.Threading;
+using osu.Game.Graphics.Sprites;
+using osuTK;
+
+namespace osu.Game.Overlays.Settings.Sections.Input
+{
+ public class TabletSettings : SettingsSubsection
+ {
+ private readonly ITabletHandler tabletHandler;
+
+ private readonly Bindable areaOffset = new Bindable();
+ private readonly Bindable areaSize = new Bindable();
+ private readonly IBindable tablet = new Bindable();
+
+ private readonly BindableNumber offsetX = new BindableNumber { MinValue = 0 };
+ private readonly BindableNumber offsetY = new BindableNumber { MinValue = 0 };
+
+ private readonly BindableNumber sizeX = new BindableNumber { MinValue = 10 };
+ private readonly BindableNumber sizeY = new BindableNumber { MinValue = 10 };
+
+ [Resolved]
+ private GameHost host { get; set; }
+
+ ///
+ /// Based on ultrawide monitor configurations.
+ ///
+ private const float largest_feasible_aspect_ratio = 21f / 9;
+
+ private readonly BindableNumber aspectRatio = new BindableFloat(1)
+ {
+ MinValue = 1 / largest_feasible_aspect_ratio,
+ MaxValue = largest_feasible_aspect_ratio,
+ Precision = 0.01f,
+ };
+
+ private readonly BindableBool aspectLock = new BindableBool();
+
+ private ScheduledDelegate aspectRatioApplication;
+
+ private FillFlowContainer mainSettings;
+
+ private OsuSpriteText noTabletMessage;
+
+ protected override string Header => "Tablet";
+
+ public TabletSettings(ITabletHandler tabletHandler)
+ {
+ this.tabletHandler = tabletHandler;
+ }
+
+ [BackgroundDependencyLoader]
+ private void load()
+ {
+ Children = new Drawable[]
+ {
+ new SettingsCheckbox
+ {
+ LabelText = "Enabled",
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Current = tabletHandler.Enabled
+ },
+ noTabletMessage = new OsuSpriteText
+ {
+ Text = "No tablet detected!",
+ Anchor = Anchor.TopCentre,
+ Origin = Anchor.TopCentre,
+ Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS }
+ },
+ mainSettings = new FillFlowContainer
+ {
+ Alpha = 0,
+ RelativeSizeAxes = Axes.X,
+ AutoSizeAxes = Axes.Y,
+ Spacing = new Vector2(0, 8),
+ Direction = FillDirection.Vertical,
+ Children = new Drawable[]
+ {
+ new TabletAreaSelection(tabletHandler)
+ {
+ RelativeSizeAxes = Axes.X,
+ Height = 300,
+ },
+ new DangerousSettingsButton
+ {
+ Text = "Reset to full area",
+ Action = () =>
+ {
+ aspectLock.Value = false;
+
+ areaOffset.SetDefault();
+ areaSize.SetDefault();
+ },
+ },
+ new SettingsButton
+ {
+ Text = "Conform to current game aspect ratio",
+ Action = () =>
+ {
+ forceAspectRatio((float)host.Window.ClientSize.Width / host.Window.ClientSize.Height);
+ }
+ },
+ new SettingsSlider
+ {
+ TransferValueOnCommit = true,
+ LabelText = "Aspect Ratio",
+ Current = aspectRatio
+ },
+ new SettingsSlider
+ {
+ TransferValueOnCommit = true,
+ LabelText = "X Offset",
+ Current = offsetX
+ },
+ new SettingsSlider
+ {
+ TransferValueOnCommit = true,
+ LabelText = "Y Offset",
+ Current = offsetY
+ },
+ new SettingsCheckbox
+ {
+ LabelText = "Lock aspect ratio",
+ Current = aspectLock
+ },
+ new SettingsSlider
+ {
+ TransferValueOnCommit = true,
+ LabelText = "Width",
+ Current = sizeX
+ },
+ new SettingsSlider
+ {
+ TransferValueOnCommit = true,
+ LabelText = "Height",
+ Current = sizeY
+ },
+ }
+ },
+ };
+ }
+
+ protected override void LoadComplete()
+ {
+ base.LoadComplete();
+
+ areaOffset.BindTo(tabletHandler.AreaOffset);
+ areaOffset.BindValueChanged(val =>
+ {
+ offsetX.Value = val.NewValue.X;
+ offsetY.Value = val.NewValue.Y;
+ }, true);
+
+ offsetX.BindValueChanged(val => areaOffset.Value = new Vector2(val.NewValue, areaOffset.Value.Y));
+ offsetY.BindValueChanged(val => areaOffset.Value = new Vector2(areaOffset.Value.X, val.NewValue));
+
+ areaSize.BindTo(tabletHandler.AreaSize);
+ areaSize.BindValueChanged(val =>
+ {
+ sizeX.Value = val.NewValue.X;
+ sizeY.Value = val.NewValue.Y;
+ }, true);
+
+ sizeX.BindValueChanged(val =>
+ {
+ areaSize.Value = new Vector2(val.NewValue, areaSize.Value.Y);
+
+ aspectRatioApplication?.Cancel();
+ aspectRatioApplication = Schedule(() => applyAspectRatio(sizeX));
+ });
+
+ sizeY.BindValueChanged(val =>
+ {
+ areaSize.Value = new Vector2(areaSize.Value.X, val.NewValue);
+
+ aspectRatioApplication?.Cancel();
+ aspectRatioApplication = Schedule(() => applyAspectRatio(sizeY));
+ });
+
+ updateAspectRatio();
+ aspectRatio.BindValueChanged(aspect =>
+ {
+ aspectRatioApplication?.Cancel();
+ aspectRatioApplication = Schedule(() => forceAspectRatio(aspect.NewValue));
+ });
+
+ tablet.BindTo(tabletHandler.Tablet);
+ tablet.BindValueChanged(val =>
+ {
+ Scheduler.AddOnce(toggleVisibility);
+
+ var tab = val.NewValue;
+
+ bool tabletFound = tab != null;
+ if (!tabletFound)
+ return;
+
+ offsetX.MaxValue = tab.Size.X;
+ offsetX.Default = tab.Size.X / 2;
+ sizeX.Default = sizeX.MaxValue = tab.Size.X;
+
+ offsetY.MaxValue = tab.Size.Y;
+ offsetY.Default = tab.Size.Y / 2;
+ sizeY.Default = sizeY.MaxValue = tab.Size.Y;
+
+ areaSize.Default = new Vector2(sizeX.Default, sizeY.Default);
+ }, true);
+ }
+
+ private void toggleVisibility()
+ {
+ bool tabletFound = tablet.Value != null;
+
+ if (!tabletFound)
+ {
+ mainSettings.Hide();
+ noTabletMessage.Show();
+ return;
+ }
+
+ mainSettings.Show();
+ noTabletMessage.Hide();
+ }
+
+ private void applyAspectRatio(BindableNumber sizeChanged)
+ {
+ try
+ {
+ if (!aspectLock.Value)
+ {
+ float proposedAspectRatio = currentAspectRatio;
+
+ if (proposedAspectRatio >= aspectRatio.MinValue && proposedAspectRatio <= aspectRatio.MaxValue)
+ {
+ // aspect ratio was in a valid range.
+ updateAspectRatio();
+ return;
+ }
+ }
+
+ // if lock is applied (or the specified values were out of range) aim to adjust the axis the user was not adjusting to conform.
+ if (sizeChanged == sizeX)
+ sizeY.Value = (int)(areaSize.Value.X / aspectRatio.Value);
+ else
+ sizeX.Value = (int)(areaSize.Value.Y * aspectRatio.Value);
+ }
+ finally
+ {
+ // cancel any event which may have fired while updating variables as a result of aspect ratio limitations.
+ // this avoids a potential feedback loop.
+ aspectRatioApplication?.Cancel();
+ }
+ }
+
+ private void forceAspectRatio(float aspectRatio)
+ {
+ aspectLock.Value = false;
+
+ int proposedHeight = (int)(sizeX.Value / aspectRatio);
+
+ if (proposedHeight < sizeY.MaxValue)
+ sizeY.Value = proposedHeight;
+ else
+ sizeX.Value = (int)(sizeY.Value * aspectRatio);
+
+ updateAspectRatio();
+
+ aspectRatioApplication?.Cancel();
+ aspectLock.Value = true;
+ }
+
+ private void updateAspectRatio() => aspectRatio.Value = currentAspectRatio;
+
+ private float currentAspectRatio => sizeX.Value / sizeY.Value;
+ }
+}
diff --git a/osu.Game/Overlays/Settings/Sections/InputSection.cs b/osu.Game/Overlays/Settings/Sections/InputSection.cs
index 8d5944f5bf..6e99891794 100644
--- a/osu.Game/Overlays/Settings/Sections/InputSection.cs
+++ b/osu.Game/Overlays/Settings/Sections/InputSection.cs
@@ -8,6 +8,7 @@ using osu.Framework.Input.Handlers;
using osu.Framework.Input.Handlers.Joystick;
using osu.Framework.Input.Handlers.Midi;
using osu.Framework.Input.Handlers.Mouse;
+using osu.Framework.Input.Handlers.Tablet;
using osu.Framework.Platform;
using osu.Game.Overlays.Settings.Sections.Input;
@@ -55,6 +56,11 @@ namespace osu.Game.Overlays.Settings.Sections
switch (handler)
{
+ // ReSharper disable once SuspiciousTypeConversion.Global (net standard fuckery)
+ case ITabletHandler th:
+ section = new TabletSettings(th);
+ break;
+
case MouseHandler mh:
section = new MouseSettings(mh);
break;
diff --git a/osu.Game/Overlays/Settings/SettingsSubsection.cs b/osu.Game/Overlays/Settings/SettingsSubsection.cs
index 1b82d973e9..6abf6283b9 100644
--- a/osu.Game/Overlays/Settings/SettingsSubsection.cs
+++ b/osu.Game/Overlays/Settings/SettingsSubsection.cs
@@ -8,10 +8,12 @@ using osu.Game.Graphics.Sprites;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
+using osu.Framework.Testing;
using osu.Game.Graphics;
namespace osu.Game.Overlays.Settings
{
+ [ExcludeFromDynamicCompile]
public abstract class SettingsSubsection : FillFlowContainer, IHasFilterableChildren
{
protected override Container Content => FlowContent;
diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs
index f1270f750e..8f3274b2b5 100644
--- a/osu.Game/Overlays/SettingsPanel.cs
+++ b/osu.Game/Overlays/SettingsPanel.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Overlays
private const float sidebar_width = Sidebar.DEFAULT_WIDTH;
- protected const float WIDTH = 400;
+ public const float WIDTH = 400;
protected Container ContentContainer;
diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
index 14aa3fe99a..e66a8c016c 100644
--- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
+++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs
@@ -117,6 +117,10 @@ namespace osu.Game.Rulesets.UI
public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotSupportedException();
+ public void BindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException();
+
+ public void UnbindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException();
+
public BindableNumber Volume => throw new NotSupportedException();
public BindableNumber Balance => throw new NotSupportedException();
@@ -125,8 +129,6 @@ namespace osu.Game.Rulesets.UI
public BindableNumber Tempo => throw new NotSupportedException();
- public IBindable GetAggregate(AdjustableProperty type) => throw new NotSupportedException();
-
public IBindable AggregateVolume => throw new NotSupportedException();
public IBindable AggregateBalance => throw new NotSupportedException();
@@ -135,10 +137,6 @@ namespace osu.Game.Rulesets.UI
public IBindable AggregateTempo => throw new NotSupportedException();
- public void BindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException();
-
- public void UnbindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException();
-
public int PlaybackConcurrency
{
get => throw new NotSupportedException();
diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs
index 78101991f6..f5192f3a40 100644
--- a/osu.Game/Scoring/ScoreInfo.cs
+++ b/osu.Game/Scoring/ScoreInfo.cs
@@ -73,7 +73,7 @@ namespace osu.Game.Scoring
}
set
{
- modsJson = JsonConvert.SerializeObject(value.Select(m => new DeserializedMod { Acronym = m.Acronym }));
+ modsJson = null;
mods = value;
}
}
@@ -86,7 +86,16 @@ namespace osu.Game.Scoring
[Column("Mods")]
public string ModsJson
{
- get => modsJson;
+ get
+ {
+ if (modsJson != null)
+ return modsJson;
+
+ if (mods == null)
+ return null;
+
+ return modsJson = JsonConvert.SerializeObject(mods.Select(m => new DeserializedMod { Acronym = m.Acronym }));
+ }
set
{
modsJson = value;
diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs
index 7d0abc5996..c7ee26c248 100644
--- a/osu.Game/Scoring/ScoreManager.cs
+++ b/osu.Game/Scoring/ScoreManager.cs
@@ -53,11 +53,6 @@ namespace osu.Game.Scoring
this.configManager = configManager;
}
- protected override void PreImport(ScoreInfo model)
- {
- model.Requery(ContextFactory);
- }
-
protected override ScoreInfo CreateModel(ArchiveReader archive)
{
if (archive == null)
diff --git a/osu.Game/Screens/Edit/BindableBeatDivisor.cs b/osu.Game/Screens/Edit/BindableBeatDivisor.cs
index d9477dd4bc..ff33f0c70d 100644
--- a/osu.Game/Screens/Edit/BindableBeatDivisor.cs
+++ b/osu.Game/Screens/Edit/BindableBeatDivisor.cs
@@ -4,7 +4,6 @@
using System;
using System.Linq;
using osu.Framework.Bindables;
-using osu.Framework.Graphics.Colour;
using osu.Game.Graphics;
using osuTK.Graphics;
@@ -48,7 +47,7 @@ namespace osu.Game.Screens.Edit
/// The beat divisor.
/// The set of colours.
/// The applicable colour from for .
- public static ColourInfo GetColourFor(int beatDivisor, OsuColour colours)
+ public static Color4 GetColourFor(int beatDivisor, OsuColour colours)
{
switch (beatDivisor)
{
diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs
index 9739f2876a..bdc6e238c8 100644
--- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs
+++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs
@@ -27,7 +27,7 @@ namespace osu.Game.Screens.Edit.Components
[Resolved]
private EditorClock editorClock { get; set; }
- private readonly BindableNumber tempo = new BindableDouble(1);
+ private readonly BindableNumber freqAdjust = new BindableDouble(1);
[BackgroundDependencyLoader]
private void load()
@@ -58,16 +58,16 @@ namespace osu.Game.Screens.Edit.Components
RelativeSizeAxes = Axes.Both,
Height = 0.5f,
Padding = new MarginPadding { Left = 45 },
- Child = new PlaybackTabControl { Current = tempo },
+ Child = new PlaybackTabControl { Current = freqAdjust },
}
};
- Track.BindValueChanged(tr => tr.NewValue?.AddAdjustment(AdjustableProperty.Tempo, tempo), true);
+ Track.BindValueChanged(tr => tr.NewValue?.AddAdjustment(AdjustableProperty.Frequency, freqAdjust), true);
}
protected override void Dispose(bool isDisposing)
{
- Track.Value?.RemoveAdjustment(AdjustableProperty.Tempo, tempo);
+ Track.Value?.RemoveAdjustment(AdjustableProperty.Frequency, freqAdjust);
base.Dispose(isDisposing);
}
@@ -101,7 +101,7 @@ namespace osu.Game.Screens.Edit.Components
private class PlaybackTabControl : OsuTabControl
{
- private static readonly double[] tempo_values = { 0.5, 0.75, 1 };
+ private static readonly double[] tempo_values = { 0.25, 0.5, 0.75, 1 };
protected override TabItem CreateTabItem(double value) => new PlaybackTabItem(value);
diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
index 051d0766bf..7def7e1d16 100644
--- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs
@@ -338,7 +338,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
private bool beginClickSelection(MouseButtonEvent e)
{
// Iterate from the top of the input stack (blueprints closest to the front of the screen first).
- foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse())
+ // Priority is given to already-selected blueprints.
+ foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse().OrderByDescending(b => b.IsSelected))
{
if (!blueprint.IsHovered) continue;
diff --git a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
index 8a92a2011d..59f88ac641 100644
--- a/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/DistanceSnapGrid.cs
@@ -132,7 +132,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
var colour = BindableBeatDivisor.GetColourFor(BindableBeatDivisor.GetDivisorForBeatIndex(beatIndex + placementIndex + 1, beatDivisor.Value), Colours);
int repeatIndex = placementIndex / beatDivisor.Value;
- return colour.MultiplyAlpha(0.5f / (repeatIndex + 1));
+ return ColourInfo.SingleColour(colour).MultiplyAlpha(0.5f / (repeatIndex + 1));
}
}
}
diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs
index fb11b859a7..c070c833f8 100644
--- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs
+++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs
@@ -6,7 +6,9 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
+using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
+using osu.Framework.Graphics.Colour;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts;
@@ -124,25 +126,28 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
if (beat == 0 && i == 0)
nextMinTick = float.MinValue;
- var indexInBar = beat % ((int)point.TimeSignature * beatDivisor.Value);
+ int indexInBar = beat % ((int)point.TimeSignature * beatDivisor.Value);
var divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value);
var colour = BindableBeatDivisor.GetColourFor(divisor, colours);
+ bool isMainBeat = indexInBar == 0;
+
// even though "bar lines" take up the full vertical space, we render them in two pieces because it allows for less anchor/origin churn.
- var height = indexInBar == 0 ? 0.5f : 0.1f - (float)divisor / highestDivisor * 0.08f;
+ float height = isMainBeat ? 0.5f : 0.4f - (float)divisor / highestDivisor * 0.2f;
+ float gradientOpacity = isMainBeat ? 1 : 0;
var topPoint = getNextUsablePoint();
topPoint.X = xPos;
- topPoint.Colour = colour;
topPoint.Height = height;
+ topPoint.Colour = ColourInfo.GradientVertical(colour, colour.Opacity(gradientOpacity));
topPoint.Anchor = Anchor.TopLeft;
topPoint.Origin = Anchor.TopCentre;
var bottomPoint = getNextUsablePoint();
bottomPoint.X = xPos;
- bottomPoint.Colour = colour;
bottomPoint.Anchor = Anchor.BottomLeft;
+ bottomPoint.Colour = ColourInfo.GradientVertical(colour.Opacity(gradientOpacity), colour);
bottomPoint.Origin = Anchor.BottomCentre;
bottomPoint.Height = height;
}
diff --git a/osu.Game/Screens/Edit/Timing/DifficultySection.cs b/osu.Game/Screens/Edit/Timing/DifficultySection.cs
index b87b8961f8..9d80ca0b14 100644
--- a/osu.Game/Screens/Edit/Timing/DifficultySection.cs
+++ b/osu.Game/Screens/Edit/Timing/DifficultySection.cs
@@ -18,7 +18,8 @@ namespace osu.Game.Screens.Edit.Timing
{
multiplierSlider = new SliderWithTextBoxInput("Speed Multiplier")
{
- Current = new DifficultyControlPoint().SpeedMultiplierBindable
+ Current = new DifficultyControlPoint().SpeedMultiplierBindable,
+ KeyboardStep = 0.1f
}
});
}
diff --git a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs
index f2f9f76143..10a5771520 100644
--- a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs
+++ b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs
@@ -69,6 +69,15 @@ namespace osu.Game.Screens.Edit.Timing
}, true);
}
+ ///
+ /// A custom step value for each key press which actuates a change on this control.
+ ///
+ public float KeyboardStep
+ {
+ get => slider.KeyboardStep;
+ set => slider.KeyboardStep = value;
+ }
+
public Bindable Current
{
get => slider.Current;
diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
index b3cd44d55a..aaacf891bb 100644
--- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs
@@ -11,7 +11,6 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Scoring;
-using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Ranking;
@@ -19,8 +18,7 @@ using osuTK;
namespace osu.Game.Screens.OnlinePlay.Multiplayer
{
- // Todo: The "room" part of PlaylistsPlayer should be split out into an abstract player class to be inherited instead.
- public class MultiplayerPlayer : PlaylistsPlayer
+ public class MultiplayerPlayer : RoomSubmittingPlayer
{
protected override bool PauseOnFocusLost => false;
@@ -63,9 +61,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(ScoreProcessor, userIds), HUDOverlay.Add);
HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue });
+ }
- if (Token == null)
- return; // Todo: Somehow handle token retrieval failure.
+ protected override void LoadAsyncComplete()
+ {
+ base.LoadAsyncComplete();
+
+ if (!ValidForResume)
+ return; // token retrieval may have failed.
client.MatchStarted += onMatchStarted;
client.ResultsReady += onResultsReady;
@@ -135,9 +138,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
private void onResultsReady() => resultsReady.SetResult(true);
- protected override async Task SubmitScore(Score score)
+ protected override async Task PrepareScoreForResultsAsync(Score score)
{
- await base.SubmitScore(score).ConfigureAwait(false);
+ await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
await client.ChangeState(MultiplayerUserState.FinishedPlay).ConfigureAwait(false);
diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
index a75e4bdc07..260d4961ff 100644
--- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
+++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs
@@ -4,13 +4,9 @@
using System;
using System.Diagnostics;
using System.Linq;
-using System.Threading;
-using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
-using osu.Framework.Logging;
using osu.Framework.Screens;
-using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Scoring;
@@ -19,36 +15,18 @@ using osu.Game.Screens.Ranking;
namespace osu.Game.Screens.OnlinePlay.Playlists
{
- public class PlaylistsPlayer : Player
+ public class PlaylistsPlayer : RoomSubmittingPlayer
{
public Action Exited;
- [Resolved(typeof(Room), nameof(Room.RoomID))]
- protected Bindable RoomId { get; private set; }
-
- protected readonly PlaylistItem PlaylistItem;
-
- protected int? Token { get; private set; }
-
- [Resolved]
- private IAPIProvider api { get; set; }
-
- [Resolved]
- private IBindable ruleset { get; set; }
-
public PlaylistsPlayer(PlaylistItem playlistItem, PlayerConfiguration configuration = null)
- : base(configuration)
+ : base(playlistItem, configuration)
{
- PlaylistItem = playlistItem;
}
[BackgroundDependencyLoader]
- private void load()
+ private void load(IBindable ruleset)
{
- Token = null;
-
- bool failed = false;
-
// Sanity checks to ensure that PlaylistsPlayer matches the settings for the current PlaylistItem
if (Beatmap.Value.BeatmapInfo.OnlineBeatmapID != PlaylistItem.Beatmap.Value.OnlineBeatmapID)
throw new InvalidOperationException("Current Beatmap does not match PlaylistItem's Beatmap");
@@ -58,29 +36,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
if (!PlaylistItem.RequiredMods.All(m => Mods.Value.Any(m.Equals)))
throw new InvalidOperationException("Current Mods do not match PlaylistItem's RequiredMods");
-
- var req = new CreateRoomScoreRequest(RoomId.Value ?? 0, PlaylistItem.ID, Game.VersionHash);
- req.Success += r => Token = r.ID;
- req.Failure += e =>
- {
- failed = true;
-
- if (string.IsNullOrEmpty(e.Message))
- Logger.Error(e, "Failed to retrieve a score submission token.");
- else
- Logger.Log($"You are not able to submit a score: {e.Message}", level: LogLevel.Important);
-
- Schedule(() =>
- {
- ValidForResume = false;
- this.Exit();
- });
- };
-
- api.Queue(req);
-
- while (!failed && !Token.HasValue)
- Thread.Sleep(1000);
}
public override bool OnExiting(IScreen next)
@@ -106,31 +61,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
return score;
}
- protected override async Task SubmitScore(Score score)
- {
- await base.SubmitScore(score).ConfigureAwait(false);
-
- Debug.Assert(Token != null);
-
- var tcs = new TaskCompletionSource();
- var request = new SubmitRoomScoreRequest(Token.Value, RoomId.Value ?? 0, PlaylistItem.ID, score.ScoreInfo);
-
- request.Success += s =>
- {
- score.ScoreInfo.OnlineScoreID = s.ID;
- tcs.SetResult(true);
- };
-
- request.Failure += e =>
- {
- Logger.Error(e, "Failed to submit score");
- tcs.SetResult(false);
- };
-
- api.Queue(request);
- await tcs.Task.ConfigureAwait(false);
- }
-
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs
index 0e221351aa..efe5d26409 100644
--- a/osu.Game/Screens/Play/Player.cs
+++ b/osu.Game/Screens/Play/Player.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Screens.Play
{
[Cached]
[Cached(typeof(ISamplePlaybackDisabler))]
- public class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisabler
+ public abstract class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisabler
{
///
/// The delay upon completion of the beatmap before displaying the results screen.
@@ -135,7 +135,7 @@ namespace osu.Game.Screens.Play
///
/// Create a new player instance.
///
- public Player(PlayerConfiguration configuration = null)
+ protected Player(PlayerConfiguration configuration = null)
{
Configuration = configuration ?? new PlayerConfiguration();
}
@@ -559,7 +559,7 @@ namespace osu.Game.Screens.Play
}
private ScheduledDelegate completionProgressDelegate;
- private Task scoreSubmissionTask;
+ private Task prepareScoreForDisplayTask;
private void updateCompletionState(ValueChangedEvent completionState)
{
@@ -586,17 +586,17 @@ namespace osu.Game.Screens.Play
if (!Configuration.ShowResults) return;
- scoreSubmissionTask ??= Task.Run(async () =>
+ prepareScoreForDisplayTask ??= Task.Run(async () =>
{
var score = CreateScore();
try
{
- await SubmitScore(score).ConfigureAwait(false);
+ await PrepareScoreForResultsAsync(score).ConfigureAwait(false);
}
catch (Exception ex)
{
- Logger.Error(ex, "Score submission failed!");
+ Logger.Error(ex, "Score preparation failed!");
}
try
@@ -617,7 +617,7 @@ namespace osu.Game.Screens.Play
private void scheduleCompletion() => completionProgressDelegate = Schedule(() =>
{
- if (!scoreSubmissionTask.IsCompleted)
+ if (!prepareScoreForDisplayTask.IsCompleted)
{
scheduleCompletion();
return;
@@ -625,7 +625,7 @@ namespace osu.Game.Screens.Play
// screen may be in the exiting transition phase.
if (this.IsCurrentScreen())
- this.Push(CreateResults(scoreSubmissionTask.Result));
+ this.Push(CreateResults(prepareScoreForDisplayTask.Result));
});
protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value;
@@ -895,11 +895,11 @@ namespace osu.Game.Screens.Play
}
///
- /// Submits the player's .
+ /// Prepare the for display at results.
///
- /// The to submit.
- /// The submitted score.
- protected virtual Task SubmitScore(Score score) => Task.CompletedTask;
+ /// The to prepare.
+ /// A task that prepares the provided score. On completion, the score is assumed to be ready for display.
+ protected virtual Task PrepareScoreForResultsAsync(Score score) => Task.CompletedTask;
///
/// Creates the for a .
diff --git a/osu.Game/Screens/Play/RoomSubmittingPlayer.cs b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs
new file mode 100644
index 0000000000..7ba12f5db6
--- /dev/null
+++ b/osu.Game/Screens/Play/RoomSubmittingPlayer.cs
@@ -0,0 +1,38 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using osu.Framework.Allocation;
+using osu.Framework.Bindables;
+using osu.Game.Online.API;
+using osu.Game.Online.Rooms;
+using osu.Game.Scoring;
+
+namespace osu.Game.Screens.Play
+{
+ ///
+ /// A player instance which submits to a room backing. This is generally used by playlists and multiplayer.
+ ///
+ public abstract class RoomSubmittingPlayer : SubmittingPlayer
+ {
+ [Resolved(typeof(Room), nameof(Room.RoomID))]
+ protected Bindable RoomId { get; private set; }
+
+ protected readonly PlaylistItem PlaylistItem;
+
+ protected RoomSubmittingPlayer(PlaylistItem playlistItem, PlayerConfiguration configuration = null)
+ : base(configuration)
+ {
+ PlaylistItem = playlistItem;
+ }
+
+ protected override APIRequest CreateTokenRequest()
+ {
+ if (!(RoomId.Value is long roomId))
+ return null;
+
+ return new CreateRoomScoreRequest(roomId, PlaylistItem.ID, Game.VersionHash);
+ }
+
+ protected override APIRequest CreateSubmissionRequest(Score score, long token) => new SubmitRoomScoreRequest(token, RoomId.Value ?? 0, PlaylistItem.ID, score.ScoreInfo);
+ }
+}
diff --git a/osu.Game/Screens/Play/SoloPlayer.cs b/osu.Game/Screens/Play/SoloPlayer.cs
new file mode 100644
index 0000000000..ee1ccdc5b3
--- /dev/null
+++ b/osu.Game/Screens/Play/SoloPlayer.cs
@@ -0,0 +1,34 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Diagnostics;
+using osu.Game.Online.API;
+using osu.Game.Online.Rooms;
+using osu.Game.Online.Solo;
+using osu.Game.Scoring;
+
+namespace osu.Game.Screens.Play
+{
+ public class SoloPlayer : SubmittingPlayer
+ {
+ protected override APIRequest CreateTokenRequest()
+ {
+ if (!(Beatmap.Value.BeatmapInfo.OnlineBeatmapID is int beatmapId))
+ return null;
+
+ return new CreateSoloScoreRequest(beatmapId, Game.VersionHash);
+ }
+
+ protected override bool HandleTokenRetrievalFailure(Exception exception) => false;
+
+ protected override APIRequest CreateSubmissionRequest(Score score, long token)
+ {
+ Debug.Assert(Beatmap.Value.BeatmapInfo.OnlineBeatmapID != null);
+
+ int beatmapId = Beatmap.Value.BeatmapInfo.OnlineBeatmapID.Value;
+
+ return new SubmitSoloScoreRequest(beatmapId, token, score.ScoreInfo);
+ }
+ }
+}
diff --git a/osu.Game/Screens/Play/SubmittingPlayer.cs b/osu.Game/Screens/Play/SubmittingPlayer.cs
new file mode 100644
index 0000000000..d22199447d
--- /dev/null
+++ b/osu.Game/Screens/Play/SubmittingPlayer.cs
@@ -0,0 +1,141 @@
+// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence.
+// See the LICENCE file in the repository root for full licence text.
+
+using System;
+using System.Threading.Tasks;
+using JetBrains.Annotations;
+using osu.Framework.Allocation;
+using osu.Framework.Logging;
+using osu.Framework.Screens;
+using osu.Game.Online.API;
+using osu.Game.Online.Rooms;
+using osu.Game.Scoring;
+
+namespace osu.Game.Screens.Play
+{
+ ///
+ /// A player instance which supports submitting scores to an online store.
+ ///
+ public abstract class SubmittingPlayer : Player
+ {
+ ///
+ /// The token to be used for the current submission. This is fetched via a request created by .
+ ///
+ private long? token;
+
+ [Resolved]
+ private IAPIProvider api { get; set; }
+
+ protected SubmittingPlayer(PlayerConfiguration configuration = null)
+ : base(configuration)
+ {
+ }
+
+ protected override void LoadAsyncComplete()
+ {
+ if (!handleTokenRetrieval()) return;
+
+ base.LoadAsyncComplete();
+ }
+
+ private bool handleTokenRetrieval()
+ {
+ // Token request construction should happen post-load to allow derived classes to potentially prepare DI backings that are used to create the request.
+ var tcs = new TaskCompletionSource();
+
+ if (!api.IsLoggedIn)
+ {
+ handleTokenFailure(new InvalidOperationException("API is not online."));
+ return false;
+ }
+
+ var req = CreateTokenRequest();
+
+ if (req == null)
+ {
+ handleTokenFailure(new InvalidOperationException("Request could not be constructed."));
+ return false;
+ }
+
+ req.Success += r =>
+ {
+ token = r.ID;
+ tcs.SetResult(true);
+ };
+ req.Failure += handleTokenFailure;
+
+ api.Queue(req);
+
+ tcs.Task.Wait();
+ return true;
+
+ void handleTokenFailure(Exception exception)
+ {
+ if (HandleTokenRetrievalFailure(exception))
+ {
+ if (string.IsNullOrEmpty(exception.Message))
+ Logger.Error(exception, "Failed to retrieve a score submission token.");
+ else
+ Logger.Log($"You are not able to submit a score: {exception.Message}", level: LogLevel.Important);
+
+ Schedule(() =>
+ {
+ ValidForResume = false;
+ this.Exit();
+ });
+ }
+
+ tcs.SetResult(false);
+ }
+ }
+
+ ///
+ /// Called when a token could not be retrieved for submission.
+ ///
+ /// The error causing the failure.
+ /// Whether gameplay should be immediately exited as a result. Returning false allows the gameplay session to continue. Defaults to true.
+ protected virtual bool HandleTokenRetrievalFailure(Exception exception) => true;
+
+ protected override async Task PrepareScoreForResultsAsync(Score score)
+ {
+ await base.PrepareScoreForResultsAsync(score).ConfigureAwait(false);
+
+ // token may be null if the request failed but gameplay was still allowed (see HandleTokenRetrievalFailure).
+ if (token == null)
+ return;
+
+ var tcs = new TaskCompletionSource();
+ var request = CreateSubmissionRequest(score, token.Value);
+
+ request.Success += s =>
+ {
+ score.ScoreInfo.OnlineScoreID = s.ID;
+ tcs.SetResult(true);
+ };
+
+ request.Failure += e =>
+ {
+ Logger.Error(e, "Failed to submit score");
+ tcs.SetResult(false);
+ };
+
+ api.Queue(request);
+ await tcs.Task.ConfigureAwait(false);
+ }
+
+ ///
+ /// Construct a request to be used for retrieval of the score token.
+ /// Can return null, at which point will be fired.
+ ///
+ [CanBeNull]
+ protected abstract APIRequest CreateTokenRequest();
+
+ ///
+ /// Construct a request to submit the score.
+ /// Will only be invoked if the request constructed via was successful.
+ ///
+ /// The score to be submitted.
+ /// The submission token.
+ protected abstract APIRequest CreateSubmissionRequest(Score score, long token);
+ }
+}
diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs
index e61d5cce85..dfb4b59060 100644
--- a/osu.Game/Screens/Select/PlaySongSelect.cs
+++ b/osu.Game/Screens/Select/PlaySongSelect.cs
@@ -106,7 +106,7 @@ namespace osu.Game.Screens.Select
SampleConfirm?.Play();
- this.Push(player = new PlayerLoader(() => new Player()));
+ this.Push(player = new PlayerLoader(() => new SoloPlayer()));
return true;
}
diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs
index 346c7b3c65..0b3f5f3cde 100644
--- a/osu.Game/Skinning/DefaultSkin.cs
+++ b/osu.Game/Skinning/DefaultSkin.cs
@@ -24,7 +24,7 @@ namespace osu.Game.Skinning
public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null;
- public override Sample GetSample(ISampleInfo sampleInfo) => null;
+ public override ISample GetSample(ISampleInfo sampleInfo) => null;
public override IBindable GetConfig(TLookup lookup)
{
diff --git a/osu.Game/Skinning/ISkin.cs b/osu.Game/Skinning/ISkin.cs
index ef8de01042..73f7cf6d39 100644
--- a/osu.Game/Skinning/ISkin.cs
+++ b/osu.Game/Skinning/ISkin.cs
@@ -48,7 +48,7 @@ namespace osu.Game.Skinning
/// The requested sample.
/// A matching sample channel, or null if unavailable.
[CanBeNull]
- Sample GetSample(ISampleInfo sampleInfo);
+ ISample GetSample(ISampleInfo sampleInfo);
///
/// Retrieve a configuration value.
diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs
index fb4207b647..3ec205e897 100644
--- a/osu.Game/Skinning/LegacyBeatmapSkin.cs
+++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs
@@ -39,7 +39,7 @@ namespace osu.Game.Skinning
return base.GetConfig(lookup);
}
- public override Sample GetSample(ISampleInfo sampleInfo)
+ public override ISample GetSample(ISampleInfo sampleInfo)
{
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy && legacy.CustomSampleBank == 0)
{
diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs
index e5d0217671..12abc4d867 100644
--- a/osu.Game/Skinning/LegacySkin.cs
+++ b/osu.Game/Skinning/LegacySkin.cs
@@ -100,13 +100,6 @@ namespace osu.Game.Skinning
true) != null);
}
- protected override void Dispose(bool isDisposing)
- {
- base.Dispose(isDisposing);
- Textures?.Dispose();
- Samples?.Dispose();
- }
-
public override IBindable GetConfig(TLookup lookup)
{
switch (lookup)
@@ -452,7 +445,7 @@ namespace osu.Game.Skinning
return null;
}
- public override Sample GetSample(ISampleInfo sampleInfo)
+ public override ISample GetSample(ISampleInfo sampleInfo)
{
IEnumerable lookupNames;
@@ -504,5 +497,12 @@ namespace osu.Game.Skinning
string lastPiece = componentName.Split('/').Last();
yield return componentName.StartsWith("Gameplay/taiko/", StringComparison.Ordinal) ? "taiko-" + lastPiece : lastPiece;
}
+
+ protected override void Dispose(bool isDisposing)
+ {
+ base.Dispose(isDisposing);
+ Textures?.Dispose();
+ Samples?.Dispose();
+ }
}
}
diff --git a/osu.Game/Skinning/LegacySkinDecoder.cs b/osu.Game/Skinning/LegacySkinDecoder.cs
index 75b7ba28b9..2700f84815 100644
--- a/osu.Game/Skinning/LegacySkinDecoder.cs
+++ b/osu.Game/Skinning/LegacySkinDecoder.cs
@@ -17,8 +17,6 @@ namespace osu.Game.Skinning
{
if (section != Section.Colours)
{
- line = StripComments(line);
-
var pair = SplitKeyVal(line);
switch (section)
diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs
index e2f4a82a54..ae8faf1a3b 100644
--- a/osu.Game/Skinning/LegacySkinTransformer.cs
+++ b/osu.Game/Skinning/LegacySkinTransformer.cs
@@ -34,7 +34,7 @@ namespace osu.Game.Skinning
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT)
=> Source.GetTexture(componentName, wrapModeS, wrapModeT);
- public virtual Sample GetSample(ISampleInfo sampleInfo)
+ public virtual ISample GetSample(ISampleInfo sampleInfo)
{
if (!(sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample))
return Source.GetSample(sampleInfo);
diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs
index 9103a6a960..b04158a58f 100644
--- a/osu.Game/Skinning/PoolableSkinnableSample.cs
+++ b/osu.Game/Skinning/PoolableSkinnableSample.cs
@@ -86,21 +86,21 @@ namespace osu.Game.Skinning
sampleContainer.Clear();
Sample = null;
- var ch = CurrentSkin.GetSample(sampleInfo);
+ var sample = CurrentSkin.GetSample(sampleInfo);
- if (ch == null && AllowDefaultFallback)
+ if (sample == null && AllowDefaultFallback)
{
foreach (var lookup in sampleInfo.LookupNames)
{
- if ((ch = sampleStore.Get(lookup)) != null)
+ if ((sample = sampleStore.Get(lookup)) != null)
break;
}
}
- if (ch == null)
+ if (sample == null)
return;
- sampleContainer.Add(Sample = new DrawableSample(ch));
+ sampleContainer.Add(Sample = new DrawableSample(sample));
// Start playback internally for the new sample if the previous one was playing beforehand.
if (wasPlaying && Looping)
diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs
index 6b435cff0f..6d1bce2cb1 100644
--- a/osu.Game/Skinning/Skin.cs
+++ b/osu.Game/Skinning/Skin.cs
@@ -19,7 +19,7 @@ namespace osu.Game.Skinning
public abstract Drawable GetDrawableComponent(ISkinComponent componentName);
- public abstract Sample GetSample(ISampleInfo sampleInfo);
+ public abstract ISample GetSample(ISampleInfo sampleInfo);
public Texture GetTexture(string componentName) => GetTexture(componentName, default, default);
diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs
index 601b77e782..ac4d63159a 100644
--- a/osu.Game/Skinning/SkinManager.cs
+++ b/osu.Game/Skinning/SkinManager.cs
@@ -86,7 +86,7 @@ namespace osu.Game.Skinning
public void SelectRandomSkin()
{
// choose from only user skins, removing the current selection to ensure a new one is chosen.
- var randomChoices = GetAllUsableSkins().Where(s => s.ID > 0 && s.ID != CurrentSkinInfo.Value.ID).ToArray();
+ var randomChoices = GetAllUsableSkins().Where(s => s.ID != CurrentSkinInfo.Value.ID).ToArray();
if (randomChoices.Length == 0)
{
@@ -104,7 +104,7 @@ namespace osu.Game.Skinning
protected override string ComputeHash(SkinInfo item, ArchiveReader reader = null)
{
// we need to populate early to create a hash based off skin.ini contents
- if (item.Name?.Contains(".osk") == true)
+ if (item.Name?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true)
populateMetadata(item);
if (item.Creator != null && item.Creator != unknown_creator_string)
@@ -122,7 +122,7 @@ namespace osu.Game.Skinning
{
await base.Populate(model, archive, cancellationToken).ConfigureAwait(false);
- if (model.Name?.Contains(".osk") == true)
+ if (model.Name?.Contains(".osk", StringComparison.OrdinalIgnoreCase) == true)
populateMetadata(model);
}
@@ -137,16 +137,11 @@ namespace osu.Game.Skinning
}
else
{
- item.Name = item.Name.Replace(".osk", "");
+ item.Name = item.Name.Replace(".osk", "", StringComparison.OrdinalIgnoreCase);
item.Creator ??= unknown_creator_string;
}
}
- protected override void PreImport(SkinInfo model)
- {
- model.Requery(ContextFactory);
- }
-
///
/// Retrieve a instance for the provided
///
@@ -176,7 +171,7 @@ namespace osu.Game.Skinning
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => CurrentSkin.Value.GetTexture(componentName, wrapModeS, wrapModeT);
- public Sample GetSample(ISampleInfo sampleInfo) => CurrentSkin.Value.GetSample(sampleInfo);
+ public ISample GetSample(ISampleInfo sampleInfo) => CurrentSkin.Value.GetSample(sampleInfo);
public IBindable GetConfig(TLookup lookup) => CurrentSkin.Value.GetConfig(lookup);
diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs
index ba67d0a678..cf22b2e820 100644
--- a/osu.Game/Skinning/SkinProvidingContainer.cs
+++ b/osu.Game/Skinning/SkinProvidingContainer.cs
@@ -59,9 +59,9 @@ namespace osu.Game.Skinning
return fallbackSource?.GetTexture(componentName, wrapModeS, wrapModeT);
}
- public Sample GetSample(ISampleInfo sampleInfo)
+ public ISample GetSample(ISampleInfo sampleInfo)
{
- Sample sourceChannel;
+ ISample sourceChannel;
if (AllowSampleLookup(sampleInfo) && (sourceChannel = skin?.GetSample(sampleInfo)) != null)
return sourceChannel;
diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs
index 57e20a8d31..f935adf7a5 100644
--- a/osu.Game/Skinning/SkinnableSound.cs
+++ b/osu.Game/Skinning/SkinnableSound.cs
@@ -131,6 +131,15 @@ namespace osu.Game.Skinning
});
}
+ protected override void LoadAsyncComplete()
+ {
+ // ensure samples are constructed before SkinChanged() is called via base.LoadAsyncComplete().
+ if (!samplesContainer.Any())
+ updateSamples();
+
+ base.LoadAsyncComplete();
+ }
+
///
/// Stops the samples.
///
@@ -139,12 +148,6 @@ namespace osu.Game.Skinning
samplesContainer.ForEach(c => c.Stop());
}
- protected override void SkinChanged(ISkinSource skin, bool allowFallback)
- {
- base.SkinChanged(skin, allowFallback);
- updateSamples();
- }
-
private void updateSamples()
{
bool wasPlaying = IsPlaying;
@@ -176,24 +179,15 @@ namespace osu.Game.Skinning
public BindableNumber Tempo => samplesContainer.Tempo;
- public void BindAdjustments(IAggregateAudioAdjustment component)
- {
- samplesContainer.BindAdjustments(component);
- }
+ public void BindAdjustments(IAggregateAudioAdjustment component) => samplesContainer.BindAdjustments(component);
- public void UnbindAdjustments(IAggregateAudioAdjustment component)
- {
- samplesContainer.UnbindAdjustments(component);
- }
+ public void UnbindAdjustments(IAggregateAudioAdjustment component) => samplesContainer.UnbindAdjustments(component);
- public void AddAdjustment(AdjustableProperty type, IBindable adjustBindable)
- => samplesContainer.AddAdjustment(type, adjustBindable);
+ public void AddAdjustment(AdjustableProperty type, IBindable adjustBindable) => samplesContainer.AddAdjustment(type, adjustBindable);
- public void RemoveAdjustment(AdjustableProperty type, IBindable adjustBindable)
- => samplesContainer.RemoveAdjustment(type, adjustBindable);
+ public void RemoveAdjustment(AdjustableProperty type, IBindable adjustBindable) => samplesContainer.RemoveAdjustment(type, adjustBindable);
- public void RemoveAllAdjustments(AdjustableProperty type)
- => samplesContainer.RemoveAllAdjustments(type);
+ public void RemoveAllAdjustments(AdjustableProperty type) => samplesContainer.RemoveAllAdjustments(type);
///
/// Whether any samples are currently playing.
diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs
index 7e824c4d7c..315be510a3 100644
--- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs
+++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs
@@ -52,15 +52,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
Rooms.Add(createdRoom);
createRoomRequest.TriggerSuccess(createdRoom);
- break;
+ return true;
case JoinRoomRequest joinRoomRequest:
joinRoomRequest.TriggerSuccess();
- break;
+ return true;
case PartRoomRequest partRoomRequest:
partRoomRequest.TriggerSuccess();
- break;
+ return true;
case GetRoomsRequest getRoomsRequest:
var roomsWithoutParticipants = new List();
@@ -76,11 +76,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
getRoomsRequest.TriggerSuccess(roomsWithoutParticipants);
- break;
+ return true;
case GetRoomRequest getRoomRequest:
getRoomRequest.TriggerSuccess(Rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId));
- break;
+ return true;
case GetBeatmapSetRequest getBeatmapSetRequest:
var onlineReq = new GetBeatmapSetRequest(getBeatmapSetRequest.ID, getBeatmapSetRequest.Type);
@@ -89,11 +89,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
// Get the online API from the game's dependencies.
game.Dependencies.Get().Queue(onlineReq);
- break;
+ return true;
case CreateRoomScoreRequest createRoomScoreRequest:
createRoomScoreRequest.TriggerSuccess(new APIScoreToken { ID = 1 });
- break;
+ return true;
case SubmitRoomScoreRequest submitRoomScoreRequest:
submitRoomScoreRequest.TriggerSuccess(new MultiplayerScore
@@ -108,8 +108,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
User = api.LocalUser.Value,
Statistics = new Dictionary()
});
- break;
+ return true;
}
+
+ return false;
};
}
diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs
index 6c45417db0..74ffb7c457 100644
--- a/osu.Game/Users/User.cs
+++ b/osu.Game/Users/User.cs
@@ -120,6 +120,9 @@ namespace osu.Game.Users
[JsonProperty(@"post_count")]
public int PostCount;
+ [JsonProperty(@"comments_count")]
+ public int CommentsCount;
+
[JsonProperty(@"follower_count")]
public int FollowerCount;
diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj
index 360c522193..b90c938a8b 100644
--- a/osu.Game/osu.Game.csproj
+++ b/osu.Game/osu.Game.csproj
@@ -24,12 +24,12 @@
-
-
+
+
-
+
-
+
diff --git a/osu.iOS.props b/osu.iOS.props
index b763a91dfb..ce182a3054 100644
--- a/osu.iOS.props
+++ b/osu.iOS.props
@@ -70,7 +70,7 @@
-
+
@@ -90,8 +90,10 @@
+
+
-
+