1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 19:03:08 +08:00

Merge branch 'master' into android

This commit is contained in:
Shane Woolcock 2019-06-18 20:05:14 +09:30
commit 17d103181c
151 changed files with 1646 additions and 759 deletions

View File

@ -32,7 +32,7 @@ If you are not interested in developing the game, you can consume our [binary re
| ------------- | ------------- | | ------------- | ------------- |
- **Linux** users are recommended to self-compile until we have official deployment in place. - **Linux** users are recommended to self-compile until we have official deployment in place.
- **iOS** users can join the [TestFlight beta program](https://t.co/xQJmHkfC18) (note that due to high demand this is regularly full). - **iOS** users can join the [TestFlight beta program](https://t.co/PasE1zrHhw) (note that due to high demand this is regularly full).
- **Android** users can self-compile, and expect a public beta soon. - **Android** users can self-compile, and expect a public beta soon.
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below. If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.

View File

@ -7,7 +7,6 @@ using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Desktop.Overlays; using osu.Desktop.Overlays;
using osu.Framework.Graphics.Containers;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game; using osu.Game;
using osuTK.Input; using osuTK.Input;
@ -56,7 +55,7 @@ namespace osu.Desktop
LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, v => LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, v =>
{ {
Add(v); Add(v);
v.State = Visibility.Visible; v.Show();
}); });
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
@ -74,13 +73,11 @@ namespace osu.Desktop
{ {
case Intro _: case Intro _:
case MainMenu _: case MainMenu _:
if (versionManager != null) versionManager?.Show();
versionManager.State = Visibility.Visible;
break; break;
default: default:
if (versionManager != null) versionManager?.Hide();
versionManager.State = Visibility.Hidden;
break; break;
} }
} }

View File

@ -120,7 +120,7 @@ namespace osu.Desktop.Overlays
Activated = delegate Activated = delegate
{ {
changelog.ShowBuild("lazer", version); changelog.ShowBuild(OsuGameBase.CLIENT_STREAM_NAME, version);
return true; return true;
}; };
} }

View File

@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
{ {
if (beatmap.HitObjects.Count == 0) if (beatmap.HitObjects.Count == 0)
return new CatchDifficultyAttributes { Mods = mods }; return new CatchDifficultyAttributes { Mods = mods, Skills = skills };
// this is the same as osu!, so there's potential to share the implementation... maybe // this is the same as osu!, so there's potential to share the implementation... maybe
double preempt = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate; double preempt = BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / clockRate;
@ -41,7 +41,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * star_scaling_factor, StarRating = Math.Sqrt(skills[0].DifficultyValue()) * star_scaling_factor,
Mods = mods, Mods = mods,
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0, ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType<JuiceStream>().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)) MaxCombo = beatmap.HitObjects.Count(h => h is Fruit) + beatmap.HitObjects.OfType<JuiceStream>().SelectMany(j => j.NestedHitObjects).Count(h => !(h is TinyDroplet)),
Skills = skills
}; };
} }

View File

@ -43,10 +43,13 @@ namespace osu.Game.Rulesets.Catch.Replays
float positionChange = Math.Abs(lastPosition - h.X); float positionChange = Math.Abs(lastPosition - h.X);
double timeAvailable = h.StartTime - lastTime; double timeAvailable = h.StartTime - lastTime;
//So we can either make it there without a dash or not. // So we can either make it there without a dash or not.
double speedRequired = positionChange / timeAvailable; // If positionChange is 0, we don't need to move, so speedRequired should also be 0 (could be NaN if timeAvailable is 0 too)
// The case where positionChange > 0 and timeAvailable == 0 results in PositiveInfinity which provides expected beheaviour.
double speedRequired = positionChange == 0 ? 0 : positionChange / timeAvailable;
bool dashRequired = speedRequired > movement_speed && h.StartTime != 0; bool dashRequired = speedRequired > movement_speed;
bool impossibleJump = speedRequired > movement_speed * 2;
// todo: get correct catcher size, based on difficulty CS. // todo: get correct catcher size, based on difficulty CS.
const float catcher_width_half = CatcherArea.CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * 0.3f * 0.5f; const float catcher_width_half = CatcherArea.CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * 0.3f * 0.5f;
@ -59,9 +62,8 @@ namespace osu.Game.Rulesets.Catch.Replays
return; return;
} }
if (h is Banana) if (impossibleJump)
{ {
// auto bananas unrealistically warp to catch 100% combo.
Replay.Frames.Add(new CatchReplayFrame(h.StartTime, h.X)); Replay.Frames.Add(new CatchReplayFrame(h.StartTime, h.X));
} }
else if (h.HyperDash) else if (h.HyperDash)

View File

@ -379,8 +379,8 @@ namespace osu.Game.Rulesets.Catch.UI
X = (float)MathHelper.Clamp(X + direction * Clock.ElapsedFrameTime * speed, 0, 1); X = (float)MathHelper.Clamp(X + direction * Clock.ElapsedFrameTime * speed, 0, 1);
// Correct overshooting. // Correct overshooting.
if (hyperDashDirection > 0 && hyperDashTargetPosition < X || if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
hyperDashDirection < 0 && hyperDashTargetPosition > X) (hyperDashDirection < 0 && hyperDashTargetPosition > X))
{ {
X = hyperDashTargetPosition; X = hyperDashTargetPosition;
SetHyperDashState(); SetHyperDashState();

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
{ {
if (beatmap.HitObjects.Count == 0) if (beatmap.HitObjects.Count == 0)
return new ManiaDifficultyAttributes { Mods = mods }; return new ManiaDifficultyAttributes { Mods = mods, Skills = skills };
return new ManiaDifficultyAttributes return new ManiaDifficultyAttributes
{ {
@ -38,6 +38,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
Mods = mods, Mods = mods,
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / clockRate, GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / clockRate,
Skills = skills
}; };
} }

View File

@ -46,11 +46,11 @@ namespace osu.Game.Rulesets.Osu.Tests
AddStep("move mouse away", () => InputManager.MoveMouseTo(ScreenSpaceDrawQuad.TopLeft)); AddStep("move mouse away", () => InputManager.MoveMouseTo(ScreenSpaceDrawQuad.TopLeft));
AddStep("click", () => osuInputManager.GameClick()); AddStep("click", () => osuInputManager.GameClick());
AddAssert("not dismissed", () => !resumeFired && resume.State == Visibility.Visible); AddAssert("not dismissed", () => !resumeFired && resume.State.Value == Visibility.Visible);
AddStep("move mouse back", () => InputManager.MoveMouseTo(ScreenSpaceDrawQuad.Centre)); AddStep("move mouse back", () => InputManager.MoveMouseTo(ScreenSpaceDrawQuad.Centre));
AddStep("click", () => osuInputManager.GameClick()); AddStep("click", () => osuInputManager.GameClick());
AddAssert("dismissed", () => resumeFired && resume.State == Visibility.Hidden); AddAssert("dismissed", () => resumeFired && resume.State.Value == Visibility.Hidden);
} }
private class ManualOsuInputManager : OsuInputManager private class ManualOsuInputManager : OsuInputManager

View File

@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
break; break;
if (Vector2Extensions.Distance(stackBaseObject.Position, objectN.Position) < stack_distance if (Vector2Extensions.Distance(stackBaseObject.Position, objectN.Position) < stack_distance
|| stackBaseObject is Slider && Vector2Extensions.Distance(stackBaseObject.EndPosition, objectN.Position) < stack_distance) || (stackBaseObject is Slider && Vector2Extensions.Distance(stackBaseObject.EndPosition, objectN.Position) < stack_distance))
{ {
stackBaseIndex = n; stackBaseIndex = n;

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
{ {
if (beatmap.HitObjects.Count == 0) if (beatmap.HitObjects.Count == 0)
return new OsuDifficultyAttributes { Mods = mods }; return new OsuDifficultyAttributes { Mods = mods, Skills = skills };
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier; double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier; double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
@ -50,7 +50,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
SpeedStrain = speedRating, SpeedStrain = speedRating,
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5, ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
OverallDifficulty = (80 - hitWindowGreat) / 6, OverallDifficulty = (80 - hitWindowGreat) / 6,
MaxCombo = maxCombo MaxCombo = maxCombo,
Skills = skills
}; };
} }

View File

@ -2,8 +2,11 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -11,7 +14,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
internal class OsuModGrow : Mod, IApplicableToDrawableHitObjects internal class OsuModGrow : Mod, IReadFromConfig, IApplicableToDrawableHitObjects
{ {
public override string Name => "Grow"; public override string Name => "Grow";
@ -25,9 +28,16 @@ namespace osu.Game.Rulesets.Osu.Mods
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
private Bindable<bool> increaseFirstObjectVisibility = new Bindable<bool>();
public void ReadFromConfig(OsuConfigManager config)
{
increaseFirstObjectVisibility = config.GetBindable<bool>(OsuSetting.IncreaseFirstObjectVisibility);
}
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables) public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
{ {
foreach (var drawable in drawables) foreach (var drawable in drawables.Skip(increaseFirstObjectVisibility.Value ? 1 : 0))
{ {
switch (drawable) switch (drawable)
{ {

View File

@ -37,11 +37,11 @@ namespace osu.Game.Rulesets.Osu.Mods
if (time < osuHit.HitObject.StartTime - relax_leniency) continue; if (time < osuHit.HitObject.StartTime - relax_leniency) continue;
if (osuHit.HitObject is IHasEndTime hasEnd && time > hasEnd.EndTime || osuHit.IsHit) if ((osuHit.HitObject is IHasEndTime hasEnd && time > hasEnd.EndTime) || osuHit.IsHit)
continue; continue;
requiresHit |= osuHit is DrawableHitCircle && osuHit.IsHovered && osuHit.HitObject.HitWindows.CanBeHit(relativetime); requiresHit |= osuHit is DrawableHitCircle && osuHit.IsHovered && osuHit.HitObject.HitWindows.CanBeHit(relativetime);
requiresHold |= osuHit is DrawableSlider slider && (slider.Ball.IsHovered || osuHit.IsHovered) || osuHit is DrawableSpinner; requiresHold |= (osuHit is DrawableSlider slider && (slider.Ball.IsHovered || osuHit.IsHovered)) || osuHit is DrawableSpinner;
} }
if (requiresHit) if (requiresHit)

View File

@ -6,7 +6,7 @@ using System.Diagnostics;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.OpenGL.Buffers; using osu.Framework.Graphics.Batches;
using osu.Framework.Graphics.OpenGL.Vertices; using osu.Framework.Graphics.OpenGL.Vertices;
using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Shaders;
@ -57,7 +57,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
// InvalidationID 1 forces an update of each part of the cursor trail the first time ApplyState is run on the draw node // InvalidationID 1 forces an update of each part of the cursor trail the first time ApplyState is run on the draw node
// This is to prevent garbage data from being sent to the vertex shader, resulting in visual issues on some platforms // This is to prevent garbage data from being sent to the vertex shader, resulting in visual issues on some platforms
parts[i].InvalidationID = 1; parts[i].InvalidationID = 1;
parts[i].WasUpdated = true;
} }
} }
@ -149,7 +148,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
public Vector2 Position; public Vector2 Position;
public float Time; public float Time;
public long InvalidationID; public long InvalidationID;
public bool WasUpdated;
} }
private class TrailDrawNode : DrawNode private class TrailDrawNode : DrawNode
@ -164,16 +162,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private readonly TrailPart[] parts = new TrailPart[max_sprites]; private readonly TrailPart[] parts = new TrailPart[max_sprites];
private Vector2 size; private Vector2 size;
private readonly VertexBuffer<TexturedTrailVertex> vertexBuffer = new QuadVertexBuffer<TexturedTrailVertex>(max_sprites, BufferUsageHint.DynamicDraw); private readonly VertexBatch<TexturedTrailVertex> vertexBatch = new QuadBatch<TexturedTrailVertex>(max_sprites, 1);
public TrailDrawNode(CursorTrail source) public TrailDrawNode(CursorTrail source)
: base(source) : base(source)
{ {
for (int i = 0; i < max_sprites; i++) for (int i = 0; i < max_sprites; i++)
{
parts[i].InvalidationID = 0; parts[i].InvalidationID = 0;
parts[i].WasUpdated = false;
}
} }
public override void ApplyState() public override void ApplyState()
@ -194,56 +189,29 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
public override void Draw(Action<TexturedVertex2D> vertexAction) public override void Draw(Action<TexturedVertex2D> vertexAction)
{ {
shader.GetUniform<float>("g_FadeClock").UpdateValue(ref time);
int updateStart = -1, updateEnd = 0;
for (int i = 0; i < parts.Length; ++i)
{
if (parts[i].WasUpdated)
{
if (updateStart == -1)
updateStart = i;
updateEnd = i + 1;
int start = i * 4;
int end = start;
Vector2 pos = parts[i].Position;
float localTime = parts[i].Time;
DrawQuad(
texture,
new Quad(pos.X - size.X / 2, pos.Y - size.Y / 2, size.X, size.Y),
DrawColourInfo.Colour,
null,
v => vertexBuffer.Vertices[end++] = new TexturedTrailVertex
{
Position = v.Position,
TexturePosition = v.TexturePosition,
Time = localTime + 1,
Colour = v.Colour,
});
parts[i].WasUpdated = false;
}
else if (updateStart != -1)
{
vertexBuffer.UpdateRange(updateStart * 4, updateEnd * 4);
updateStart = -1;
}
}
// Update all remaining vertices that have been changed.
if (updateStart != -1)
vertexBuffer.UpdateRange(updateStart * 4, updateEnd * 4);
base.Draw(vertexAction); base.Draw(vertexAction);
shader.Bind(); shader.Bind();
shader.GetUniform<float>("g_FadeClock").UpdateValue(ref time);
texture.TextureGL.Bind(); for (int i = 0; i < parts.Length; ++i)
vertexBuffer.Draw(); {
Vector2 pos = parts[i].Position;
float localTime = parts[i].Time;
DrawQuad(
texture,
new Quad(pos.X - size.X / 2, pos.Y - size.Y / 2, size.X, size.Y),
DrawColourInfo.Colour,
null,
v => vertexBatch.Add(new TexturedTrailVertex
{
Position = v.Position,
TexturePosition = v.TexturePosition,
Time = localTime + 1,
Colour = v.Colour,
}));
}
shader.Unbind(); shader.Unbind();
} }
@ -252,7 +220,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
vertexBuffer.Dispose(); vertexBatch.Dispose();
} }
} }

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.UI
private GameplayCursorContainer localCursorContainer; private GameplayCursorContainer localCursorContainer;
public override CursorContainer LocalCursor => State == Visibility.Visible ? localCursorContainer : null; public override CursorContainer LocalCursor => State.Value == Visibility.Visible ? localCursorContainer : null;
protected override string Message => "Click the orange cursor to resume"; protected override string Message => "Click the orange cursor to resume";

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
{ {
if (beatmap.HitObjects.Count == 0) if (beatmap.HitObjects.Count == 0)
return new TaikoDifficultyAttributes { Mods = mods }; return new TaikoDifficultyAttributes { Mods = mods, Skills = skills };
return new TaikoDifficultyAttributes return new TaikoDifficultyAttributes
{ {
@ -36,6 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / clockRate, GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / clockRate,
MaxCombo = beatmap.HitObjects.Count(h => h is Hit), MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
Skills = skills
}; };
} }

View File

@ -11,7 +11,9 @@ using NUnit.Framework;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.IPC; using osu.Game.IPC;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.IO;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using SharpCompress.Archives.Zip; using SharpCompress.Archives.Zip;
@ -21,14 +23,14 @@ namespace osu.Game.Tests.Beatmaps.IO
public class ImportBeatmapTest public class ImportBeatmapTest
{ {
[Test] [Test]
public void TestImportWhenClosed() public async Task TestImportWhenClosed()
{ {
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWhenClosed")) using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWhenClosed"))
{ {
try try
{ {
LoadOszIntoOsu(loadOsu(host)); await LoadOszIntoOsu(loadOsu(host));
} }
finally finally
{ {
@ -38,7 +40,7 @@ namespace osu.Game.Tests.Beatmaps.IO
} }
[Test] [Test]
public void TestImportThenDelete() public async Task TestImportThenDelete()
{ {
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenDelete")) using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenDelete"))
@ -47,7 +49,7 @@ namespace osu.Game.Tests.Beatmaps.IO
{ {
var osu = loadOsu(host); var osu = loadOsu(host);
var imported = LoadOszIntoOsu(osu); var imported = await LoadOszIntoOsu(osu);
deleteBeatmapSet(imported, osu); deleteBeatmapSet(imported, osu);
} }
@ -59,7 +61,7 @@ namespace osu.Game.Tests.Beatmaps.IO
} }
[Test] [Test]
public void TestImportThenImport() public async Task TestImportThenImport()
{ {
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenImport")) using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenImport"))
@ -68,17 +70,15 @@ namespace osu.Game.Tests.Beatmaps.IO
{ {
var osu = loadOsu(host); var osu = loadOsu(host);
var imported = LoadOszIntoOsu(osu); var imported = await LoadOszIntoOsu(osu);
var importedSecondTime = LoadOszIntoOsu(osu); var importedSecondTime = await LoadOszIntoOsu(osu);
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
Assert.IsTrue(imported.ID == importedSecondTime.ID); Assert.IsTrue(imported.ID == importedSecondTime.ID);
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID); Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
var manager = osu.Dependencies.Get<BeatmapManager>(); checkBeatmapSetCount(osu, 1);
checkSingleReferencedFileCount(osu, 18);
Assert.AreEqual(1, manager.GetAllUsableBeatmapSets().Count);
Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count);
} }
finally finally
{ {
@ -88,30 +88,41 @@ namespace osu.Game.Tests.Beatmaps.IO
} }
[Test] [Test]
public void TestRollbackOnFailure() public async Task TestRollbackOnFailure()
{ {
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestRollbackOnFailure")) using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestRollbackOnFailure"))
{ {
try try
{ {
int itemAddRemoveFireCount = 0;
int loggedExceptionCount = 0;
Logger.NewEntry += l =>
{
if (l.Target == LoggingTarget.Database && l.Exception != null)
Interlocked.Increment(ref loggedExceptionCount);
};
var osu = loadOsu(host); var osu = loadOsu(host);
var manager = osu.Dependencies.Get<BeatmapManager>(); var manager = osu.Dependencies.Get<BeatmapManager>();
int fireCount = 0;
// ReSharper disable once AccessToModifiedClosure // ReSharper disable once AccessToModifiedClosure
manager.ItemAdded += (_, __) => fireCount++; manager.ItemAdded += (_, __) => Interlocked.Increment(ref itemAddRemoveFireCount);
manager.ItemRemoved += _ => fireCount++; manager.ItemRemoved += _ => Interlocked.Increment(ref itemAddRemoveFireCount);
var imported = LoadOszIntoOsu(osu); var imported = await LoadOszIntoOsu(osu);
Assert.AreEqual(0, fireCount -= 1); Assert.AreEqual(0, itemAddRemoveFireCount -= 1);
imported.Hash += "-changed"; imported.Hash += "-changed";
manager.Update(imported); manager.Update(imported);
Assert.AreEqual(0, fireCount -= 2); Assert.AreEqual(0, itemAddRemoveFireCount -= 2);
checkBeatmapSetCount(osu, 1);
checkBeatmapCount(osu, 12);
checkSingleReferencedFileCount(osu, 18);
var breakTemp = TestResources.GetTestBeatmapForImport(); var breakTemp = TestResources.GetTestBeatmapForImport();
@ -127,19 +138,24 @@ namespace osu.Game.Tests.Beatmaps.IO
zip.SaveTo(outStream, SharpCompress.Common.CompressionType.Deflate); zip.SaveTo(outStream, SharpCompress.Common.CompressionType.Deflate);
} }
Assert.AreEqual(1, manager.GetAllUsableBeatmapSets().Count);
Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count);
Assert.AreEqual(12, manager.QueryBeatmaps(_ => true).ToList().Count);
// this will trigger purging of the existing beatmap (online set id match) but should rollback due to broken osu. // this will trigger purging of the existing beatmap (online set id match) but should rollback due to broken osu.
manager.Import(breakTemp); try
{
await manager.Import(breakTemp);
}
catch
{
}
// no events should be fired in the case of a rollback. // no events should be fired in the case of a rollback.
Assert.AreEqual(0, fireCount); Assert.AreEqual(0, itemAddRemoveFireCount);
Assert.AreEqual(1, manager.GetAllUsableBeatmapSets().Count); checkBeatmapSetCount(osu, 1);
Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count); checkBeatmapCount(osu, 12);
Assert.AreEqual(12, manager.QueryBeatmaps(_ => true).ToList().Count);
checkSingleReferencedFileCount(osu, 18);
Assert.AreEqual(1, loggedExceptionCount);
} }
finally finally
{ {
@ -149,7 +165,7 @@ namespace osu.Game.Tests.Beatmaps.IO
} }
[Test] [Test]
public void TestImportThenImportDifferentHash() public async Task TestImportThenImportDifferentHash()
{ {
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenImportDifferentHash")) using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenImportDifferentHash"))
@ -159,19 +175,18 @@ namespace osu.Game.Tests.Beatmaps.IO
var osu = loadOsu(host); var osu = loadOsu(host);
var manager = osu.Dependencies.Get<BeatmapManager>(); var manager = osu.Dependencies.Get<BeatmapManager>();
var imported = LoadOszIntoOsu(osu); var imported = await LoadOszIntoOsu(osu);
imported.Hash += "-changed"; imported.Hash += "-changed";
manager.Update(imported); manager.Update(imported);
var importedSecondTime = LoadOszIntoOsu(osu); var importedSecondTime = await LoadOszIntoOsu(osu);
Assert.IsTrue(imported.ID != importedSecondTime.ID); Assert.IsTrue(imported.ID != importedSecondTime.ID);
Assert.IsTrue(imported.Beatmaps.First().ID < importedSecondTime.Beatmaps.First().ID); Assert.IsTrue(imported.Beatmaps.First().ID < importedSecondTime.Beatmaps.First().ID);
// only one beatmap will exist as the online set ID matched, causing purging of the first import. // only one beatmap will exist as the online set ID matched, causing purging of the first import.
Assert.AreEqual(1, manager.GetAllUsableBeatmapSets().Count); checkBeatmapSetCount(osu, 1);
Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count);
} }
finally finally
{ {
@ -181,7 +196,7 @@ namespace osu.Game.Tests.Beatmaps.IO
} }
[Test] [Test]
public void TestImportThenDeleteThenImport() public async Task TestImportThenDeleteThenImport()
{ {
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenDeleteThenImport")) using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenDeleteThenImport"))
@ -190,11 +205,11 @@ namespace osu.Game.Tests.Beatmaps.IO
{ {
var osu = loadOsu(host); var osu = loadOsu(host);
var imported = LoadOszIntoOsu(osu); var imported = await LoadOszIntoOsu(osu);
deleteBeatmapSet(imported, osu); deleteBeatmapSet(imported, osu);
var importedSecondTime = LoadOszIntoOsu(osu); var importedSecondTime = await LoadOszIntoOsu(osu);
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash. // check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
Assert.IsTrue(imported.ID == importedSecondTime.ID); Assert.IsTrue(imported.ID == importedSecondTime.ID);
@ -209,7 +224,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[TestCase(true)] [TestCase(true)]
[TestCase(false)] [TestCase(false)]
public void TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set) public async Task TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set)
{ {
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"TestImportThenDeleteThenImport-{set}")) using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"TestImportThenDeleteThenImport-{set}"))
@ -218,7 +233,7 @@ namespace osu.Game.Tests.Beatmaps.IO
{ {
var osu = loadOsu(host); var osu = loadOsu(host);
var imported = LoadOszIntoOsu(osu); var imported = await LoadOszIntoOsu(osu);
if (set) if (set)
imported.OnlineBeatmapSetID = 1234; imported.OnlineBeatmapSetID = 1234;
@ -229,7 +244,7 @@ namespace osu.Game.Tests.Beatmaps.IO
deleteBeatmapSet(imported, osu); deleteBeatmapSet(imported, osu);
var importedSecondTime = LoadOszIntoOsu(osu); var importedSecondTime = await LoadOszIntoOsu(osu);
// check the newly "imported" beatmap has been reimported due to mismatch (even though hashes matched) // check the newly "imported" beatmap has been reimported due to mismatch (even though hashes matched)
Assert.IsTrue(imported.ID != importedSecondTime.ID); Assert.IsTrue(imported.ID != importedSecondTime.ID);
@ -243,7 +258,7 @@ namespace osu.Game.Tests.Beatmaps.IO
} }
[Test] [Test]
public void TestImportWithDuplicateBeatmapIDs() public async Task TestImportWithDuplicateBeatmapIDs()
{ {
//unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. //unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWithDuplicateBeatmapID")) using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWithDuplicateBeatmapID"))
@ -284,7 +299,7 @@ namespace osu.Game.Tests.Beatmaps.IO
var manager = osu.Dependencies.Get<BeatmapManager>(); var manager = osu.Dependencies.Get<BeatmapManager>();
var imported = manager.Import(toImport); var imported = await manager.Import(toImport);
Assert.NotNull(imported); Assert.NotNull(imported);
Assert.AreEqual(null, imported.Beatmaps[0].OnlineBeatmapID); Assert.AreEqual(null, imported.Beatmaps[0].OnlineBeatmapID);
@ -330,7 +345,7 @@ namespace osu.Game.Tests.Beatmaps.IO
} }
[Test] [Test]
public void TestImportWhenFileOpen() public async Task TestImportWhenFileOpen()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWhenFileOpen")) using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWhenFileOpen"))
{ {
@ -339,7 +354,7 @@ namespace osu.Game.Tests.Beatmaps.IO
var osu = loadOsu(host); var osu = loadOsu(host);
var temp = TestResources.GetTestBeatmapForImport(); var temp = TestResources.GetTestBeatmapForImport();
using (File.OpenRead(temp)) using (File.OpenRead(temp))
osu.Dependencies.Get<BeatmapManager>().Import(temp); await osu.Dependencies.Get<BeatmapManager>().Import(temp);
ensureLoaded(osu); ensureLoaded(osu);
File.Delete(temp); File.Delete(temp);
Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't"); Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't");
@ -351,13 +366,13 @@ namespace osu.Game.Tests.Beatmaps.IO
} }
} }
public static BeatmapSetInfo LoadOszIntoOsu(OsuGameBase osu, string path = null) public static async Task<BeatmapSetInfo> LoadOszIntoOsu(OsuGameBase osu, string path = null)
{ {
var temp = path ?? TestResources.GetTestBeatmapForImport(); var temp = path ?? TestResources.GetTestBeatmapForImport();
var manager = osu.Dependencies.Get<BeatmapManager>(); var manager = osu.Dependencies.Get<BeatmapManager>();
manager.Import(temp); await manager.Import(temp);
var imported = manager.GetAllUsableBeatmapSets(); var imported = manager.GetAllUsableBeatmapSets();
@ -373,11 +388,32 @@ namespace osu.Game.Tests.Beatmaps.IO
var manager = osu.Dependencies.Get<BeatmapManager>(); var manager = osu.Dependencies.Get<BeatmapManager>();
manager.Delete(imported); manager.Delete(imported);
Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 0); checkBeatmapSetCount(osu, 0);
Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count); checkBeatmapSetCount(osu, 1, true);
checkSingleReferencedFileCount(osu, 0);
Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending); Assert.IsTrue(manager.QueryBeatmapSets(_ => true).First().DeletePending);
} }
private void checkBeatmapSetCount(OsuGameBase osu, int expected, bool includeDeletePending = false)
{
var manager = osu.Dependencies.Get<BeatmapManager>();
Assert.AreEqual(expected, includeDeletePending
? manager.QueryBeatmapSets(_ => true).ToList().Count
: manager.GetAllUsableBeatmapSets().Count);
}
private void checkBeatmapCount(OsuGameBase osu, int expected)
{
Assert.AreEqual(expected, osu.Dependencies.Get<BeatmapManager>().QueryBeatmaps(_ => true).ToList().Count);
}
private void checkSingleReferencedFileCount(OsuGameBase osu, int expected)
{
Assert.AreEqual(expected, osu.Dependencies.Get<FileStore>().QueryFiles(f => f.ReferenceCount == 1).Count());
}
private OsuGameBase loadOsu(GameHost host) private OsuGameBase loadOsu(GameHost host)
{ {
var osu = new OsuGameBase(); var osu = new OsuGameBase();

View File

@ -23,13 +23,13 @@ namespace osu.Game.Tests.Scores.IO
public class ImportScoreTest public class ImportScoreTest
{ {
[Test] [Test]
public void TestBasicImport() public async Task TestBasicImport()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestBasicImport")) using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestBasicImport"))
{ {
try try
{ {
var osu = loadOsu(host); var osu = await loadOsu(host);
var toImport = new ScoreInfo var toImport = new ScoreInfo
{ {
@ -43,7 +43,7 @@ namespace osu.Game.Tests.Scores.IO
OnlineScoreID = 12345, OnlineScoreID = 12345,
}; };
var imported = loadIntoOsu(osu, toImport); var imported = await loadIntoOsu(osu, toImport);
Assert.AreEqual(toImport.Rank, imported.Rank); Assert.AreEqual(toImport.Rank, imported.Rank);
Assert.AreEqual(toImport.TotalScore, imported.TotalScore); Assert.AreEqual(toImport.TotalScore, imported.TotalScore);
@ -62,20 +62,20 @@ namespace osu.Game.Tests.Scores.IO
} }
[Test] [Test]
public void TestImportMods() public async Task TestImportMods()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportMods")) using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportMods"))
{ {
try try
{ {
var osu = loadOsu(host); var osu = await loadOsu(host);
var toImport = new ScoreInfo var toImport = new ScoreInfo
{ {
Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() },
}; };
var imported = loadIntoOsu(osu, toImport); var imported = await loadIntoOsu(osu, toImport);
Assert.IsTrue(imported.Mods.Any(m => m is OsuModHardRock)); Assert.IsTrue(imported.Mods.Any(m => m is OsuModHardRock));
Assert.IsTrue(imported.Mods.Any(m => m is OsuModDoubleTime)); Assert.IsTrue(imported.Mods.Any(m => m is OsuModDoubleTime));
@ -88,13 +88,13 @@ namespace osu.Game.Tests.Scores.IO
} }
[Test] [Test]
public void TestImportStatistics() public async Task TestImportStatistics()
{ {
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportStatistics")) using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportStatistics"))
{ {
try try
{ {
var osu = loadOsu(host); var osu = await loadOsu(host);
var toImport = new ScoreInfo var toImport = new ScoreInfo
{ {
@ -105,7 +105,7 @@ namespace osu.Game.Tests.Scores.IO
} }
}; };
var imported = loadIntoOsu(osu, toImport); var imported = await loadIntoOsu(osu, toImport);
Assert.AreEqual(toImport.Statistics[HitResult.Perfect], imported.Statistics[HitResult.Perfect]); Assert.AreEqual(toImport.Statistics[HitResult.Perfect], imported.Statistics[HitResult.Perfect]);
Assert.AreEqual(toImport.Statistics[HitResult.Miss], imported.Statistics[HitResult.Miss]); Assert.AreEqual(toImport.Statistics[HitResult.Miss], imported.Statistics[HitResult.Miss]);
@ -117,7 +117,7 @@ namespace osu.Game.Tests.Scores.IO
} }
} }
private ScoreInfo loadIntoOsu(OsuGameBase osu, ScoreInfo score) private async Task<ScoreInfo> loadIntoOsu(OsuGameBase osu, ScoreInfo score)
{ {
var beatmapManager = osu.Dependencies.Get<BeatmapManager>(); var beatmapManager = osu.Dependencies.Get<BeatmapManager>();
@ -125,20 +125,24 @@ namespace osu.Game.Tests.Scores.IO
score.Ruleset = new OsuRuleset().RulesetInfo; score.Ruleset = new OsuRuleset().RulesetInfo;
var scoreManager = osu.Dependencies.Get<ScoreManager>(); var scoreManager = osu.Dependencies.Get<ScoreManager>();
scoreManager.Import(score); await scoreManager.Import(score);
return scoreManager.GetAllUsableScores().First(); return scoreManager.GetAllUsableScores().First();
} }
private OsuGameBase loadOsu(GameHost host) private async Task<OsuGameBase> loadOsu(GameHost host)
{ {
var osu = new OsuGameBase(); var osu = new OsuGameBase();
#pragma warning disable 4014
Task.Run(() => host.Run(osu)); Task.Run(() => host.Run(osu));
#pragma warning restore 4014
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
var beatmapFile = TestResources.GetTestBeatmapForImport(); var beatmapFile = TestResources.GetTestBeatmapForImport();
var beatmapManager = osu.Dependencies.Get<BeatmapManager>(); var beatmapManager = osu.Dependencies.Get<BeatmapManager>();
beatmapManager.Import(beatmapFile); await beatmapManager.Import(beatmapFile);
return osu; return osu;
} }

View File

@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.Background
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, factory, rulesets, null, audio, host, Beatmap.Default)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, factory, rulesets, null, audio, host, Beatmap.Default));
Dependencies.Cache(new OsuConfigManager(LocalStorage)); Dependencies.Cache(new OsuConfigManager(LocalStorage));
manager.Import(TestResources.GetTestBeatmapForImport()); manager.Import(TestResources.GetTestBeatmapForImport()).Wait();
Beatmap.SetDefault(); Beatmap.SetDefault();
} }

View File

@ -28,7 +28,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override void AddCheckSteps() protected override void AddCheckSteps()
{ {
AddUntilStep("wait for fail", () => Player.HasFailed); AddUntilStep("wait for fail", () => Player.HasFailed);
AddUntilStep("wait for fail overlay", () => ((FailPlayer)Player).FailOverlay.State == Visibility.Visible); AddUntilStep("wait for fail overlay", () => ((FailPlayer)Player).FailOverlay.State.Value == Visibility.Visible);
} }
private class FailPlayer : TestPlayer private class FailPlayer : TestPlayer

View File

@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Show overlay", () => pauseOverlay.Show()); AddStep("Show overlay", () => pauseOverlay.Show());
AddStep("Press select", () => press(GlobalAction.Select)); AddStep("Press select", () => press(GlobalAction.Select));
AddAssert("Overlay still open", () => pauseOverlay.State == Visibility.Visible); AddAssert("Overlay still open", () => pauseOverlay.State.Value == Visibility.Visible);
AddStep("Hide overlay", () => pauseOverlay.Hide()); AddStep("Hide overlay", () => pauseOverlay.Hide());
} }
@ -237,7 +237,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}); });
AddAssert("Action was triggered", () => triggered); AddAssert("Action was triggered", () => triggered);
AddAssert("Overlay is closed", () => pauseOverlay.State == Visibility.Hidden); AddAssert("Overlay is closed", () => pauseOverlay.State.Value == Visibility.Hidden);
} }
/// <summary> /// <summary>
@ -272,7 +272,7 @@ namespace osu.Game.Tests.Visual.Gameplay
return triggered; return triggered;
}); });
AddAssert("Overlay is closed", () => pauseOverlay.State == Visibility.Hidden); AddAssert("Overlay is closed", () => pauseOverlay.State.Value == Visibility.Hidden);
} }
private void press(Key key) private void press(Key key)

View File

@ -0,0 +1,109 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.MathUtils;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osuTK;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneGameplayRewinding : PlayerTestScene
{
private RulesetExposingPlayer player => (RulesetExposingPlayer)Player;
[Resolved]
private AudioManager audioManager { get; set; }
public TestSceneGameplayRewinding()
: base(new OsuRuleset())
{
}
private Track track;
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap)
{
var working = new ClockBackedTestWorkingBeatmap(beatmap, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
track = working.Track;
return working;
}
[Test]
public void TestNoJudgementsOnRewind()
{
AddUntilStep("wait for track to start running", () => track.IsRunning);
addSeekStep(3000);
AddAssert("all judged", () => player.DrawableRuleset.Playfield.AllHitObjects.All(h => h.Judged));
AddStep("clear results", () => player.AppliedResults.Clear());
addSeekStep(0);
AddAssert("none judged", () => player.DrawableRuleset.Playfield.AllHitObjects.All(h => !h.Judged));
AddAssert("no results triggered", () => player.AppliedResults.Count == 0);
}
private void addSeekStep(double time)
{
AddStep($"seek to {time}", () => track.Seek(time));
// Allow a few frames of lenience
AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, player.DrawableRuleset.FrameStableClock.CurrentTime, 100));
}
protected override Player CreatePlayer(Ruleset ruleset)
{
Mods.Value = Mods.Value.Concat(new[] { ruleset.GetAutoplayMod() }).ToArray();
return new RulesetExposingPlayer();
}
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset)
{
var beatmap = new Beatmap
{
BeatmapInfo = { BaseDifficulty = { ApproachRate = 9 } },
};
for (int i = 0; i < 15; i++)
{
beatmap.HitObjects.Add(new HitCircle
{
Position = new Vector2(256, 192),
StartTime = 1000 + 30 * i
});
}
return beatmap;
}
private class RulesetExposingPlayer : Player
{
public readonly List<JudgementResult> AppliedResults = new List<JudgementResult>();
public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer;
public new DrawableRuleset DrawableRuleset => base.DrawableRuleset;
public RulesetExposingPlayer()
: base(false, false)
{
}
[BackgroundDependencyLoader]
private void load()
{
ScoreProcessor.NewJudgement += r => AppliedResults.Add(r);
}
}
}
}

View File

@ -189,7 +189,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("pause overlay " + (isShown ? "shown" : "hidden"), () => Player.PauseOverlayVisible == isShown); AddAssert("pause overlay " + (isShown ? "shown" : "hidden"), () => Player.PauseOverlayVisible == isShown);
private void confirmClockRunning(bool isRunning) => private void confirmClockRunning(bool isRunning) =>
AddAssert("clock " + (isRunning ? "running" : "stopped"), () => Player.GameplayClockContainer.GameplayClock.IsRunning == isRunning); AddUntilStep("clock " + (isRunning ? "running" : "stopped"), () => Player.GameplayClockContainer.GameplayClock.IsRunning == isRunning);
protected override bool AllowFail => true; protected override bool AllowFail => true;
@ -203,9 +203,9 @@ namespace osu.Game.Tests.Visual.Gameplay
public new HUDOverlay HUDOverlay => base.HUDOverlay; public new HUDOverlay HUDOverlay => base.HUDOverlay;
public bool FailOverlayVisible => FailOverlay.State == Visibility.Visible; public bool FailOverlayVisible => FailOverlay.State.Value == Visibility.Visible;
public bool PauseOverlayVisible => PauseOverlay.State == Visibility.Visible; public bool PauseOverlayVisible => PauseOverlay.State.Value == Visibility.Visible;
public override void OnEntering(IScreen last) public override void OnEntering(IScreen last)
{ {

View File

@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
State = Visibility.Visible, State = { Value = Visibility.Visible },
}); });
AddStep("Restart", restart); AddStep("Restart", restart);

View File

@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Menus
public TestSceneToolbar() public TestSceneToolbar()
{ {
var toolbar = new Toolbar { State = Visibility.Visible }; var toolbar = new Toolbar { State = { Value = Visibility.Visible } };
ToolbarNotificationButton notificationButton = null; ToolbarNotificationButton notificationButton = null;
AddStep("create toolbar", () => AddStep("create toolbar", () =>

View File

@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
settings = new TestRoomSettings settings = new TestRoomSettings
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
State = Visibility.Visible State = { Value = Visibility.Visible }
}; };
Child = settings; Child = settings;

View File

@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Online
api.Logout(); api.Logout();
api.LocalUser.BindValueChanged(user => { userPanelArea.Child = new UserPanel(user.NewValue) { Width = 200 }; }, true); api.LocalUser.BindValueChanged(user => { userPanelArea.Child = new UserPanel(user.NewValue) { Width = 200 }; }, true);
AddStep("show", () => accountCreation.State = Visibility.Visible); AddStep("show", () => accountCreation.Show());
AddStep("logout", () => api.Logout()); AddStep("logout", () => api.Logout());
} }
} }

View File

@ -41,6 +41,9 @@ namespace osu.Game.Tests.Visual.Online
typeof(SuccessRate), typeof(SuccessRate),
}; };
private RulesetInfo maniaRuleset;
private RulesetInfo taikoRuleset;
public TestSceneBeatmapSetOverlay() public TestSceneBeatmapSetOverlay()
{ {
Add(overlay = new BeatmapSetOverlay()); Add(overlay = new BeatmapSetOverlay());
@ -49,13 +52,25 @@ namespace osu.Game.Tests.Visual.Online
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(RulesetStore rulesets) private void load(RulesetStore rulesets)
{ {
var mania = rulesets.GetRuleset(3); maniaRuleset = rulesets.GetRuleset(3);
var taiko = rulesets.GetRuleset(1); taikoRuleset = rulesets.GetRuleset(1);
}
[Test]
public void TestLoading()
{
AddStep(@"show loading", () => overlay.ShowBeatmapSet(null)); AddStep(@"show loading", () => overlay.ShowBeatmapSet(null));
}
[Test]
public void TestOnline()
{
AddStep(@"show online", () => overlay.FetchAndShowBeatmapSet(55)); AddStep(@"show online", () => overlay.FetchAndShowBeatmapSet(55));
}
[Test]
public void TestLocalBeatmaps()
{
AddStep(@"show first", () => AddStep(@"show first", () =>
{ {
overlay.ShowBeatmapSet(new BeatmapSetInfo overlay.ShowBeatmapSet(new BeatmapSetInfo
@ -87,13 +102,14 @@ namespace osu.Game.Tests.Visual.Online
Cover = @"https://assets.ppy.sh/beatmaps/415886/covers/cover.jpg?1465651778", Cover = @"https://assets.ppy.sh/beatmaps/415886/covers/cover.jpg?1465651778",
}, },
}, },
Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() },
Beatmaps = new List<BeatmapInfo> Beatmaps = new List<BeatmapInfo>
{ {
new BeatmapInfo new BeatmapInfo
{ {
StarDifficulty = 1.36, StarDifficulty = 1.36,
Version = @"BASIC", Version = @"BASIC",
Ruleset = mania, Ruleset = maniaRuleset,
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {
CircleSize = 4, CircleSize = 4,
@ -111,16 +127,15 @@ namespace osu.Game.Tests.Visual.Online
}, },
Metrics = new BeatmapMetrics Metrics = new BeatmapMetrics
{ {
Ratings = Enumerable.Range(0, 11), Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6), Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6),
}, },
}, },
new BeatmapInfo new BeatmapInfo
{ {
StarDifficulty = 2.22, StarDifficulty = 2.22,
Version = @"NOVICE", Version = @"NOVICE",
Ruleset = mania, Ruleset = maniaRuleset,
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {
CircleSize = 4, CircleSize = 4,
@ -138,16 +153,15 @@ namespace osu.Game.Tests.Visual.Online
}, },
Metrics = new BeatmapMetrics Metrics = new BeatmapMetrics
{ {
Ratings = Enumerable.Range(0, 11), Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6), Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6),
}, },
}, },
new BeatmapInfo new BeatmapInfo
{ {
StarDifficulty = 3.49, StarDifficulty = 3.49,
Version = @"ADVANCED", Version = @"ADVANCED",
Ruleset = mania, Ruleset = maniaRuleset,
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {
CircleSize = 4, CircleSize = 4,
@ -165,16 +179,15 @@ namespace osu.Game.Tests.Visual.Online
}, },
Metrics = new BeatmapMetrics Metrics = new BeatmapMetrics
{ {
Ratings = Enumerable.Range(0, 11), Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6), Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6),
}, },
}, },
new BeatmapInfo new BeatmapInfo
{ {
StarDifficulty = 4.24, StarDifficulty = 4.24,
Version = @"EXHAUST", Version = @"EXHAUST",
Ruleset = mania, Ruleset = maniaRuleset,
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {
CircleSize = 4, CircleSize = 4,
@ -192,16 +205,15 @@ namespace osu.Game.Tests.Visual.Online
}, },
Metrics = new BeatmapMetrics Metrics = new BeatmapMetrics
{ {
Ratings = Enumerable.Range(0, 11), Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6), Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6),
}, },
}, },
new BeatmapInfo new BeatmapInfo
{ {
StarDifficulty = 5.26, StarDifficulty = 5.26,
Version = @"GRAVITY", Version = @"GRAVITY",
Ruleset = mania, Ruleset = maniaRuleset,
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {
CircleSize = 4, CircleSize = 4,
@ -219,9 +231,8 @@ namespace osu.Game.Tests.Visual.Online
}, },
Metrics = new BeatmapMetrics Metrics = new BeatmapMetrics
{ {
Ratings = Enumerable.Range(0, 11), Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6), Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6),
}, },
}, },
}, },
@ -258,13 +269,14 @@ namespace osu.Game.Tests.Visual.Online
Cover = @"https://assets.ppy.sh/beatmaps/625493/covers/cover.jpg?1499167472", Cover = @"https://assets.ppy.sh/beatmaps/625493/covers/cover.jpg?1499167472",
}, },
}, },
Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() },
Beatmaps = new List<BeatmapInfo> Beatmaps = new List<BeatmapInfo>
{ {
new BeatmapInfo new BeatmapInfo
{ {
StarDifficulty = 1.40, StarDifficulty = 1.40,
Version = @"yzrin's Kantan", Version = @"yzrin's Kantan",
Ruleset = taiko, Ruleset = taikoRuleset,
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {
CircleSize = 2, CircleSize = 2,
@ -282,16 +294,15 @@ namespace osu.Game.Tests.Visual.Online
}, },
Metrics = new BeatmapMetrics Metrics = new BeatmapMetrics
{ {
Ratings = Enumerable.Range(0, 11), Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6), Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6),
}, },
}, },
new BeatmapInfo new BeatmapInfo
{ {
StarDifficulty = 2.23, StarDifficulty = 2.23,
Version = @"Futsuu", Version = @"Futsuu",
Ruleset = taiko, Ruleset = taikoRuleset,
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {
CircleSize = 2, CircleSize = 2,
@ -309,16 +320,15 @@ namespace osu.Game.Tests.Visual.Online
}, },
Metrics = new BeatmapMetrics Metrics = new BeatmapMetrics
{ {
Ratings = Enumerable.Range(0, 11), Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6), Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6),
}, },
}, },
new BeatmapInfo new BeatmapInfo
{ {
StarDifficulty = 3.19, StarDifficulty = 3.19,
Version = @"Muzukashii", Version = @"Muzukashii",
Ruleset = taiko, Ruleset = taikoRuleset,
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {
CircleSize = 2, CircleSize = 2,
@ -336,16 +346,15 @@ namespace osu.Game.Tests.Visual.Online
}, },
Metrics = new BeatmapMetrics Metrics = new BeatmapMetrics
{ {
Ratings = Enumerable.Range(0, 11), Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6), Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6),
}, },
}, },
new BeatmapInfo new BeatmapInfo
{ {
StarDifficulty = 3.97, StarDifficulty = 3.97,
Version = @"Charlotte's Oni", Version = @"Charlotte's Oni",
Ruleset = taiko, Ruleset = taikoRuleset,
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {
CircleSize = 5, CircleSize = 5,
@ -363,16 +372,15 @@ namespace osu.Game.Tests.Visual.Online
}, },
Metrics = new BeatmapMetrics Metrics = new BeatmapMetrics
{ {
Ratings = Enumerable.Range(0, 11), Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6), Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6),
}, },
}, },
new BeatmapInfo new BeatmapInfo
{ {
StarDifficulty = 5.08, StarDifficulty = 5.08,
Version = @"Labyrinth Oni", Version = @"Labyrinth Oni",
Ruleset = taiko, Ruleset = taikoRuleset,
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {
CircleSize = 5, CircleSize = 5,
@ -390,16 +398,24 @@ namespace osu.Game.Tests.Visual.Online
}, },
Metrics = new BeatmapMetrics Metrics = new BeatmapMetrics
{ {
Ratings = Enumerable.Range(0, 11), Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6), Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6),
}, },
}, },
}, },
}); });
}); });
}
[Test]
public void TestHide()
{
AddStep(@"hide", overlay.Hide); AddStep(@"hide", overlay.Hide);
}
[Test]
public void TestShowWithNoReload()
{
AddStep(@"show without reload", overlay.Show); AddStep(@"show without reload", overlay.Show);
} }
} }

View File

@ -0,0 +1,69 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.MathUtils;
using osu.Game.Beatmaps;
using osu.Game.Overlays.BeatmapSet;
using osu.Game.Screens.Select.Details;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneBeatmapSetOverlayDetails : OsuTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(Details)
};
private RatingsExposingDetails details;
[SetUp]
public void Setup() => Schedule(() =>
{
Child = details = new RatingsExposingDetails
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
};
});
[Test]
public void TestMetrics()
{
var firstSet = createSet();
var secondSet = createSet();
AddStep("set first set", () => details.BeatmapSet = firstSet);
AddAssert("ratings set", () => details.Ratings.Metrics == firstSet.Metrics);
AddStep("set second set", () => details.BeatmapSet = secondSet);
AddAssert("ratings set", () => details.Ratings.Metrics == secondSet.Metrics);
BeatmapSetInfo createSet() => new BeatmapSetInfo
{
Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).Select(_ => RNG.Next(10)).ToArray() },
Beatmaps = new List<BeatmapInfo>
{
new BeatmapInfo
{
Metrics = new BeatmapMetrics
{
Fails = Enumerable.Range(1, 100).Select(_ => RNG.Next(10)).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(_ => RNG.Next(10)).ToArray(),
}
}
}
};
}
private class RatingsExposingDetails : Details
{
public new UserRatings Ratings => base.Ratings;
}
}
}

View File

@ -0,0 +1,82 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.MathUtils;
using osu.Game.Beatmaps;
using osu.Game.Overlays.BeatmapSet;
using osu.Game.Screens.Select.Details;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Online
{
public class TestSceneBeatmapSetOverlaySuccessRate : OsuTestScene
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(Details)
};
private GraphExposingSuccessRate successRate;
[SetUp]
public void Setup() => Schedule(() =>
{
Child = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(275, 220),
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Gray,
},
successRate = new GraphExposingSuccessRate
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(275, 220),
Padding = new MarginPadding(20)
}
}
};
});
[Test]
public void TestMetrics()
{
var firstBeatmap = createBeatmap();
var secondBeatmap = createBeatmap();
AddStep("set first set", () => successRate.Beatmap = firstBeatmap);
AddAssert("ratings set", () => successRate.Graph.Metrics == firstBeatmap.Metrics);
AddStep("set second set", () => successRate.Beatmap = secondBeatmap);
AddAssert("ratings set", () => successRate.Graph.Metrics == secondBeatmap.Metrics);
BeatmapInfo createBeatmap() => new BeatmapInfo
{
Metrics = new BeatmapMetrics
{
Fails = Enumerable.Range(1, 100).Select(_ => RNG.Next(10)).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(_ => RNG.Next(10)).ToArray(),
}
};
}
private class GraphExposingSuccessRate : SuccessRate
{
public new FailRetryGraph Graph => base.Graph;
}
}
}

View File

@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
Version = "2018.712.0", Version = "2018.712.0",
DisplayVersion = "2018.712.0", DisplayVersion = "2018.712.0",
UpdateStream = new APIUpdateStream { Name = "lazer" }, UpdateStream = new APIUpdateStream { Name = OsuGameBase.CLIENT_STREAM_NAME },
ChangelogEntries = new List<APIChangelogEntry> ChangelogEntries = new List<APIChangelogEntry>
{ {
new APIChangelogEntry new APIChangelogEntry

View File

@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Online
Children = new Drawable[] Children = new Drawable[]
{ {
channelManager, channelManager,
new ChatOverlay { State = Visibility.Visible } new ChatOverlay { State = { Value = Visibility.Visible } }
}; };
} }
} }

View File

@ -18,8 +18,24 @@ namespace osu.Game.Tests.Visual.Online
{ {
base.LoadComplete(); base.LoadComplete();
int fireCount = 0;
Add(overlay = new TestFullscreenOverlay()); Add(overlay = new TestFullscreenOverlay());
AddStep(@"toggle", overlay.ToggleVisibility);
overlay.State.ValueChanged += _ => fireCount++;
AddStep(@"show", overlay.Show);
AddAssert("fire count 1", () => fireCount == 1);
AddStep(@"show again", overlay.Show);
// this logic is specific to FullscreenOverlay
AddAssert("fire count 2", () => fireCount == 2);
AddStep(@"hide", overlay.Hide);
AddAssert("fire count 3", () => fireCount == 3);
} }
private class TestFullscreenOverlay : FullscreenOverlay private class TestFullscreenOverlay : FullscreenOverlay

View File

@ -5,9 +5,9 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays.Profile.Sections; using osu.Game.Overlays.Profile.Sections;
using osu.Game.Overlays.Profile.Sections.Historical; using osu.Game.Overlays.Profile.Sections.Historical;
using osu.Game.Users; using osu.Game.Users;
@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Online
Colour = OsuColour.Gray(0.2f) Colour = OsuColour.Gray(0.2f)
}); });
Add(new ScrollContainer Add(new OsuScrollContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = section = new HistoricalSection(), Child = section = new HistoricalSection(),

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Users; using osu.Game.Users;
@ -12,10 +13,12 @@ namespace osu.Game.Tests.Visual.Online
[TestFixture] [TestFixture]
public class TestSceneUserPanel : OsuTestScene public class TestSceneUserPanel : OsuTestScene
{ {
private readonly UserPanel peppy;
public TestSceneUserPanel() public TestSceneUserPanel()
{ {
UserPanel flyte; UserPanel flyte;
UserPanel peppy;
Add(new FillFlowContainer Add(new FillFlowContainer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
@ -44,13 +47,31 @@ namespace osu.Game.Tests.Visual.Online
}); });
flyte.Status.Value = new UserStatusOnline(); flyte.Status.Value = new UserStatusOnline();
peppy.Status.Value = new UserStatusSoloGame(); peppy.Status.Value = null;
}
AddStep(@"spectating", () => { flyte.Status.Value = new UserStatusSpectating(); }); [Test]
AddStep(@"multiplaying", () => { flyte.Status.Value = new UserStatusMultiplayerGame(); }); public void UserStatusesTests()
AddStep(@"modding", () => { flyte.Status.Value = new UserStatusModding(); }); {
AddStep(@"offline", () => { flyte.Status.Value = new UserStatusOffline(); }); AddStep("online", () => { peppy.Status.Value = new UserStatusOnline(); });
AddStep(@"null status", () => { flyte.Status.Value = null; }); AddStep(@"do not disturb", () => { peppy.Status.Value = new UserStatusDoNotDisturb(); });
AddStep(@"offline", () => { peppy.Status.Value = new UserStatusOffline(); });
AddStep(@"null status", () => { peppy.Status.Value = null; });
}
[Test]
public void UserActivitiesTests()
{
Bindable<UserActivity> activity = new Bindable<UserActivity>();
peppy.Activity.BindTo(activity);
AddStep("idle", () => { activity.Value = null; });
AddStep("spectating", () => { activity.Value = new UserActivity.Spectating(); });
AddStep("solo", () => { activity.Value = new UserActivity.SoloGame(null, null); });
AddStep("choosing", () => { activity.Value = new UserActivity.ChoosingBeatmap(); });
AddStep("editing", () => { activity.Value = new UserActivity.Editing(null); });
AddStep("modding", () => { activity.Value = new UserActivity.Modding(); });
} }
} }
} }

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Profile.Sections; using osu.Game.Overlays.Profile.Sections;
@ -36,7 +37,7 @@ namespace osu.Game.Tests.Visual.Online
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(0.2f) Colour = OsuColour.Gray(0.2f)
}, },
new ScrollContainer new OsuScrollContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer<DrawableRecentActivity> Child = new FillFlowContainer<DrawableRecentActivity>

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays.Profile.Sections; using osu.Game.Overlays.Profile.Sections;
using osu.Game.Overlays.Profile.Sections.Ranks; using osu.Game.Overlays.Profile.Sections.Ranks;
using osu.Game.Users; using osu.Game.Users;
@ -33,7 +34,7 @@ namespace osu.Game.Tests.Visual.Online
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(0.2f) Colour = OsuColour.Gray(0.2f)
}, },
new ScrollContainer new OsuScrollContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = ranks = new RanksSection(), Child = ranks = new RanksSection(),

View File

@ -1,24 +1,33 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
namespace osu.Game.Tests.Visual.Settings namespace osu.Game.Tests.Visual.Settings
{ {
[TestFixture] [TestFixture]
public class TestSceneSettings : OsuTestScene public class TestSceneSettingsPanel : OsuTestScene
{ {
private readonly SettingsPanel settings; private readonly SettingsPanel settings;
private readonly DialogOverlay dialogOverlay; private readonly DialogOverlay dialogOverlay;
public TestSceneSettings() public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(SettingsFooter),
typeof(SettingsOverlay),
};
public TestSceneSettingsPanel()
{ {
settings = new SettingsOverlay settings = new SettingsOverlay
{ {
State = Visibility.Visible State = { Value = Visibility.Visible }
}; };
Add(dialogOverlay = new DialogOverlay Add(dialogOverlay = new DialogOverlay
{ {

View File

@ -32,6 +32,10 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("all metrics", () => detailsArea.Beatmap = new DummyWorkingBeatmap(null, null) AddStep("all metrics", () => detailsArea.Beatmap = new DummyWorkingBeatmap(null, null)
{ {
BeatmapSetInfo =
{
Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() }
},
BeatmapInfo = BeatmapInfo =
{ {
Version = "All Metrics", Version = "All Metrics",
@ -50,9 +54,8 @@ namespace osu.Game.Tests.Visual.SongSelect
StarDifficulty = 5.3f, StarDifficulty = 5.3f,
Metrics = new BeatmapMetrics Metrics = new BeatmapMetrics
{ {
Ratings = Enumerable.Range(0, 11), Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6), Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6),
}, },
} }
} }
@ -60,6 +63,10 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("all except source", () => detailsArea.Beatmap = new DummyWorkingBeatmap(null, null) AddStep("all except source", () => detailsArea.Beatmap = new DummyWorkingBeatmap(null, null)
{ {
BeatmapSetInfo =
{
Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() }
},
BeatmapInfo = BeatmapInfo =
{ {
Version = "All Metrics", Version = "All Metrics",
@ -77,15 +84,18 @@ namespace osu.Game.Tests.Visual.SongSelect
StarDifficulty = 5.3f, StarDifficulty = 5.3f,
Metrics = new BeatmapMetrics Metrics = new BeatmapMetrics
{ {
Ratings = Enumerable.Range(0, 11), Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6), Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6),
}, },
} }
}); });
AddStep("ratings", () => detailsArea.Beatmap = new DummyWorkingBeatmap(null, null) AddStep("ratings", () => detailsArea.Beatmap = new DummyWorkingBeatmap(null, null)
{ {
BeatmapSetInfo =
{
Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() }
},
BeatmapInfo = BeatmapInfo =
{ {
Version = "Only Ratings", Version = "Only Ratings",
@ -101,11 +111,7 @@ namespace osu.Game.Tests.Visual.SongSelect
OverallDifficulty = 6, OverallDifficulty = 6,
ApproachRate = 6, ApproachRate = 6,
}, },
StarDifficulty = 4.8f, StarDifficulty = 4.8f
Metrics = new BeatmapMetrics
{
Ratings = Enumerable.Range(0, 11),
},
} }
}); });
@ -129,8 +135,8 @@ namespace osu.Game.Tests.Visual.SongSelect
StarDifficulty = 2.91f, StarDifficulty = 2.91f,
Metrics = new BeatmapMetrics Metrics = new BeatmapMetrics
{ {
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6), Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6), Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
}, },
} }
}); });

View File

@ -1,28 +1,38 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.ComponentModel;
using System.Linq; using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
namespace osu.Game.Tests.Visual.SongSelect namespace osu.Game.Tests.Visual.SongSelect
{ {
[Description("PlaySongSelect beatmap details")] [System.ComponentModel.Description("PlaySongSelect beatmap details")]
public class TestSceneBeatmapDetails : OsuTestScene public class TestSceneBeatmapDetails : OsuTestScene
{ {
public TestSceneBeatmapDetails() private BeatmapDetails details;
[SetUp]
public void Setup() => Schedule(() =>
{ {
BeatmapDetails details; Child = details = new BeatmapDetails
Add(details = new BeatmapDetails
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(150), Padding = new MarginPadding(150),
}); };
});
[Test]
public void TestAllMetrics()
{
AddStep("all metrics", () => details.Beatmap = new BeatmapInfo AddStep("all metrics", () => details.Beatmap = new BeatmapInfo
{ {
BeatmapSet = new BeatmapSetInfo
{
Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() }
},
Version = "All Metrics", Version = "All Metrics",
Metadata = new BeatmapMetadata Metadata = new BeatmapMetadata
{ {
@ -39,14 +49,21 @@ namespace osu.Game.Tests.Visual.SongSelect
StarDifficulty = 5.3f, StarDifficulty = 5.3f,
Metrics = new BeatmapMetrics Metrics = new BeatmapMetrics
{ {
Ratings = Enumerable.Range(0, 11), Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6), Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6),
}, },
}); });
}
[Test]
public void TestAllMetricsExceptSource()
{
AddStep("all except source", () => details.Beatmap = new BeatmapInfo AddStep("all except source", () => details.Beatmap = new BeatmapInfo
{ {
BeatmapSet = new BeatmapSetInfo
{
Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() }
},
Version = "All Metrics", Version = "All Metrics",
Metadata = new BeatmapMetadata Metadata = new BeatmapMetadata
{ {
@ -62,14 +79,21 @@ namespace osu.Game.Tests.Visual.SongSelect
StarDifficulty = 5.3f, StarDifficulty = 5.3f,
Metrics = new BeatmapMetrics Metrics = new BeatmapMetrics
{ {
Ratings = Enumerable.Range(0, 11), Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6), Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6),
}, },
}); });
}
[Test]
public void TestOnlyRatings()
{
AddStep("ratings", () => details.Beatmap = new BeatmapInfo AddStep("ratings", () => details.Beatmap = new BeatmapInfo
{ {
BeatmapSet = new BeatmapSetInfo
{
Metrics = new BeatmapSetMetrics { Ratings = Enumerable.Range(0, 11).ToArray() }
},
Version = "Only Ratings", Version = "Only Ratings",
Metadata = new BeatmapMetadata Metadata = new BeatmapMetadata
{ {
@ -84,12 +108,12 @@ namespace osu.Game.Tests.Visual.SongSelect
ApproachRate = 6, ApproachRate = 6,
}, },
StarDifficulty = 4.8f, StarDifficulty = 4.8f,
Metrics = new BeatmapMetrics
{
Ratings = Enumerable.Range(0, 11),
},
}); });
}
[Test]
public void TestOnlyFailsAndRetries()
{
AddStep("fails retries", () => details.Beatmap = new BeatmapInfo AddStep("fails retries", () => details.Beatmap = new BeatmapInfo
{ {
Version = "Only Retries and Fails", Version = "Only Retries and Fails",
@ -108,11 +132,15 @@ namespace osu.Game.Tests.Visual.SongSelect
StarDifficulty = 2.91f, StarDifficulty = 2.91f,
Metrics = new BeatmapMetrics Metrics = new BeatmapMetrics
{ {
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6), Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6), Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
}, },
}); });
}
[Test]
public void TestNoMetrics()
{
AddStep("no metrics", () => details.Beatmap = new BeatmapInfo AddStep("no metrics", () => details.Beatmap = new BeatmapInfo
{ {
Version = "No Metrics", Version = "No Metrics",
@ -129,10 +157,22 @@ namespace osu.Game.Tests.Visual.SongSelect
ApproachRate = 6.5f, ApproachRate = 6.5f,
}, },
StarDifficulty = 1.97f, StarDifficulty = 1.97f,
Metrics = new BeatmapMetrics(),
}); });
}
[Test]
public void TestNullBeatmap()
{
AddStep("null beatmap", () => details.Beatmap = null); AddStep("null beatmap", () => details.Beatmap = null);
} }
[Test]
public void TestOnlineMetrics()
{
AddStep("online ratings/retries/fails", () => details.Beatmap = new BeatmapInfo
{
OnlineBeatmapID = 162,
});
}
} }
} }

View File

@ -1,4 +1,4 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Collections.Generic;
@ -7,7 +7,6 @@ using JetBrains.Annotations;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -48,7 +47,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("show", () => AddStep("show", () =>
{ {
infoWedge.State = Visibility.Visible; infoWedge.Show();
infoWedge.Beatmap = Beatmap.Value; infoWedge.Beatmap = Beatmap.Value;
}); });
@ -57,11 +56,11 @@ namespace osu.Game.Tests.Visual.SongSelect
AddWaitStep("wait for select", 3); AddWaitStep("wait for select", 3);
AddStep("hide", () => { infoWedge.State = Visibility.Hidden; }); AddStep("hide", () => { infoWedge.Hide(); });
AddWaitStep("wait for hide", 3); AddWaitStep("wait for hide", 3);
AddStep("show", () => { infoWedge.State = Visibility.Visible; }); AddStep("show", () => { infoWedge.Show(); });
foreach (var rulesetInfo in rulesets.AvailableRulesets) foreach (var rulesetInfo in rulesets.AvailableRulesets)
{ {

View File

@ -270,9 +270,8 @@ namespace osu.Game.Tests.Visual.SongSelect
}, },
Metrics = new BeatmapMetrics Metrics = new BeatmapMetrics
{ {
Ratings = Enumerable.Range(0, 11), Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6), Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6),
}, },
}; };
} }

View File

@ -18,6 +18,7 @@ using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.Taiko;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
@ -100,8 +101,11 @@ namespace osu.Game.Tests.Visual.SongSelect
} }
[SetUp] [SetUp]
public virtual void SetUp() => public virtual void SetUp() => Schedule(() =>
Schedule(() => { manager?.Delete(manager.GetAllUsableBeatmapSets()); }); {
Ruleset.Value = new OsuRuleset().RulesetInfo;
manager?.Delete(manager.GetAllUsableBeatmapSets());
});
[Test] [Test]
public void TestDummy() public void TestDummy()
@ -138,7 +142,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
createSongSelect(); createSongSelect();
changeRuleset(2); changeRuleset(2);
importForRuleset(0); addRulesetImportStep(0);
AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmap == null); AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmap == null);
} }
@ -147,8 +151,8 @@ namespace osu.Game.Tests.Visual.SongSelect
{ {
createSongSelect(); createSongSelect();
changeRuleset(2); changeRuleset(2);
importForRuleset(2); addRulesetImportStep(2);
importForRuleset(1); addRulesetImportStep(1);
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2); AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2);
changeRuleset(1); changeRuleset(1);
@ -185,7 +189,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("empty mods", () => !Mods.Value.Any()); AddAssert("empty mods", () => !Mods.Value.Any());
void onModChange(ValueChangedEvent<IReadOnlyList<Mod>> e) => modChangeIndex = actionIndex++; void onModChange(ValueChangedEvent<IReadOnlyList<Mod>> e) => modChangeIndex = actionIndex++;
void onRulesetChange(ValueChangedEvent<RulesetInfo> e) => rulesetChangeIndex = actionIndex--; void onRulesetChange(ValueChangedEvent<RulesetInfo> e) => rulesetChangeIndex = actionIndex++;
} }
[Test] [Test]
@ -210,7 +214,52 @@ namespace osu.Game.Tests.Visual.SongSelect
AddAssert("start not requested", () => !startRequested); AddAssert("start not requested", () => !startRequested);
} }
private void importForRuleset(int id) => AddStep($"import test map for ruleset {id}", () => manager.Import(createTestBeatmapSet(getImportId(), rulesets.AvailableRulesets.Where(r => r.ID == id).ToArray()))); [Test]
public void TestAddNewBeatmapWhileSelectingRandom()
{
const int test_count = 10;
int beatmapChangedCount = 0;
int debounceCount = 0;
createSongSelect();
AddStep("Setup counters", () =>
{
beatmapChangedCount = 0;
debounceCount = 0;
songSelect.Carousel.SelectionChanged += _ => beatmapChangedCount++;
});
AddRepeatStep($"Create beatmaps {test_count} times", () =>
{
importForRuleset(0);
Scheduler.AddDelayed(() =>
{
// Wait for debounce
songSelect.Carousel.SelectNextRandom();
++debounceCount;
}, 400);
}, test_count);
AddUntilStep("Debounce limit reached", () => debounceCount == test_count);
// The selected beatmap should have changed an additional 2 times since both initially loading songselect and the first import also triggers selectionChanged
AddAssert($"Beatmap changed {test_count + 2} times", () => beatmapChangedCount == test_count + 2);
}
[Test]
public void TestHideSetSelectsCorrectBeatmap()
{
int? previousID = null;
createSongSelect();
addRulesetImportStep(0);
AddStep("Move to last difficulty", () => songSelect.Carousel.SelectBeatmap(songSelect.Carousel.BeatmapSets.First().Beatmaps.Last()));
AddStep("Store current ID", () => previousID = songSelect.Carousel.SelectedBeatmap.ID);
AddStep("Hide first beatmap", () => manager.Hide(songSelect.Carousel.SelectedBeatmapSet.Beatmaps.First()));
AddAssert("Selected beatmap has not changed", () => songSelect.Carousel.SelectedBeatmap.ID == previousID);
}
private void addRulesetImportStep(int id) => AddStep($"import test map for ruleset {id}", () => importForRuleset(id));
private void importForRuleset(int id) => manager.Import(createTestBeatmapSet(getImportId(), rulesets.AvailableRulesets.Where(r => r.ID == id).ToArray())).Wait();
private static int importId; private static int importId;
private int getImportId() => ++importId; private int getImportId() => ++importId;
@ -232,7 +281,7 @@ namespace osu.Game.Tests.Visual.SongSelect
var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray(); var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray();
for (int i = 0; i < 100; i += 10) for (int i = 0; i < 100; i += 10)
manager.Import(createTestBeatmapSet(i, usableRulesets)); manager.Import(createTestBeatmapSet(i, usableRulesets)).Wait();
}); });
} }

View File

@ -176,7 +176,7 @@ namespace osu.Game.Tests.Visual.UserInterface
/// Checks if a cursor is visible. /// Checks if a cursor is visible.
/// </summary> /// </summary>
/// <param name="cursorContainer">The cursor to check.</param> /// <param name="cursorContainer">The cursor to check.</param>
private bool checkVisible(CursorContainer cursorContainer) => cursorContainer.State == Visibility.Visible; private bool checkVisible(CursorContainer cursorContainer) => cursorContainer.State.Value == Visibility.Visible;
/// <summary> /// <summary>
/// Checks if a cursor is at the current inputmanager screen position. /// Checks if a cursor is at the current inputmanager screen position.
@ -192,7 +192,7 @@ namespace osu.Game.Tests.Visual.UserInterface
public CursorContainer Cursor { get; } public CursorContainer Cursor { get; }
public bool ProvidingUserCursor { get; } public bool ProvidingUserCursor { get; }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || SmoothTransition && !ProvidingUserCursor; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || (SmoothTransition && !ProvidingUserCursor);
private readonly Box background; private readonly Box background;
@ -218,7 +218,7 @@ namespace osu.Game.Tests.Visual.UserInterface
}, },
Cursor = new TestCursorContainer Cursor = new TestCursorContainer
{ {
State = providesUserCursor ? Visibility.Hidden : Visibility.Visible, State = { Value = providesUserCursor ? Visibility.Hidden : Visibility.Visible },
} }
}; };
} }

View File

@ -26,8 +26,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
new NamedIconButton("No change", new IconButton()), new NamedIconButton("No change", new IconButton()),
new NamedIconButton("Background colours", new ColouredIconButton()), new NamedIconButton("Background colours", new ColouredIconButton()),
new NamedIconButton("Full-width", new IconButton { ButtonSize = new Vector2(200, 30) }), new NamedIconButton("Full-width", new IconButton { Size = new Vector2(200, 30) }),
new NamedIconButton("Unchanging size", new IconButton(), false),
new NamedIconButton("Icon colours", new IconButton new NamedIconButton("Icon colours", new IconButton
{ {
IconColour = Color4.Green, IconColour = Color4.Green,
@ -48,7 +47,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private class NamedIconButton : Container private class NamedIconButton : Container
{ {
public NamedIconButton(string name, IconButton button, bool allowSizeChange = true) public NamedIconButton(string name, IconButton button)
{ {
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
Width = 200; Width = 200;
@ -101,13 +100,7 @@ namespace osu.Game.Tests.Visual.UserInterface
} }
}; };
if (allowSizeChange) iconContainer.AutoSizeAxes = Axes.Both;
iconContainer.AutoSizeAxes = Axes.Both;
else
{
iconContainer.RelativeSizeAxes = Axes.X;
iconContainer.Height = 30;
}
button.Anchor = Anchor.Centre; button.Anchor = Anchor.Centre;
button.Origin = Anchor.Centre; button.Origin = Anchor.Centre;

View File

@ -3,7 +3,6 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -23,9 +22,9 @@ namespace osu.Game.Tests.Visual.UserInterface
}; };
Add(mc); Add(mc);
AddToggleStep(@"toggle visibility", state => mc.State = state ? Visibility.Visible : Visibility.Hidden); AddStep(@"show", () => mc.Show());
AddStep(@"show", () => mc.State = Visibility.Visible);
AddToggleStep(@"toggle beatmap lock", state => Beatmap.Disabled = state); AddToggleStep(@"toggle beatmap lock", state => Beatmap.Disabled = state);
AddStep(@"show", () => mc.Hide());
} }
} }
} }

View File

@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Content.Add(displayedCount); Content.Add(displayedCount);
void setState(Visibility state) => AddStep(state.ToString(), () => manager.State = state); void setState(Visibility state) => AddStep(state.ToString(), () => manager.State.Value = state);
void checkProgressingCount(int expected) => AddAssert($"progressing count is {expected}", () => progressingNotifications.Count == expected); void checkProgressingCount(int expected) => AddAssert($"progressing count is {expected}", () => progressingNotifications.Count == expected);
manager.UnreadCount.ValueChanged += count => { displayedCount.Text = $"displayed count: {count.NewValue}"; }; manager.UnreadCount.ValueChanged += count => { displayedCount.Text = $"displayed count: {count.NewValue}"; };

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -29,7 +30,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Colour = Color4.Teal, Colour = Color4.Teal,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
new ScrollContainer new OsuScrollContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = flow = new FillFlowContainer<Icon> Child = flow = new FillFlowContainer<Icon>

View File

@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.UserInterface
var popup = new PopupDialog var popup = new PopupDialog
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
State = Framework.Graphics.Containers.Visibility.Visible, State = { Value = Framework.Graphics.Containers.Visibility.Visible },
Icon = FontAwesome.Solid.AssistiveListeningSystems, Icon = FontAwesome.Solid.AssistiveListeningSystems,
HeaderText = @"This is a test popup", HeaderText = @"This is a test popup",
BodyText = "I can say lots of stuff and even wrap my words!", BodyText = "I can say lots of stuff and even wrap my words!",

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -32,7 +33,7 @@ namespace osu.Game.Tests.Visual.UserInterface
this.api = api; this.api = api;
this.rulesets = rulesets; this.rulesets = rulesets;
testBeatmap = ImportBeatmapTest.LoadOszIntoOsu(osu); testBeatmap = ImportBeatmapTest.LoadOszIntoOsu(osu).Result;
} }
[Test] [Test]
@ -92,13 +93,13 @@ namespace osu.Game.Tests.Visual.UserInterface
public void TestUnloadAndReload() public void TestUnloadAndReload()
{ {
var backgrounds = new List<TestUpdateableBeatmapBackgroundSprite>(); var backgrounds = new List<TestUpdateableBeatmapBackgroundSprite>();
ScrollContainer scrollContainer = null; OsuScrollContainer scrollContainer = null;
AddStep("create backgrounds hierarchy", () => AddStep("create backgrounds hierarchy", () =>
{ {
FillFlowContainer backgroundFlow; FillFlowContainer backgroundFlow;
Child = scrollContainer = new ScrollContainer Child = scrollContainer = new OsuScrollContainer
{ {
Size = new Vector2(500), Size = new Vector2(500),
Child = backgroundFlow = new FillFlowContainer Child = backgroundFlow = new FillFlowContainer

View File

@ -119,7 +119,7 @@ namespace osu.Game.Beatmaps
/// </summary> /// </summary>
public List<ScoreInfo> Scores { get; set; } public List<ScoreInfo> Scores { get; set; }
public override string ToString() => $"{Metadata} [{Version}]"; public override string ToString() => $"{Metadata} [{Version}]".Trim();
public bool Equals(BeatmapInfo other) public bool Equals(BeatmapInfo other)
{ {

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using osu.Framework.Audio; using osu.Framework.Audio;
@ -14,6 +15,7 @@ using osu.Framework.Extensions;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.IO.Archives; using osu.Game.IO.Archives;
@ -72,6 +74,8 @@ namespace osu.Game.Beatmaps
private readonly List<DownloadBeatmapSetRequest> currentDownloads = new List<DownloadBeatmapSetRequest>(); private readonly List<DownloadBeatmapSetRequest> currentDownloads = new List<DownloadBeatmapSetRequest>();
private readonly BeatmapUpdateQueue updateQueue;
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, AudioManager audioManager, GameHost host = null, public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, AudioManager audioManager, GameHost host = null,
WorkingBeatmap defaultBeatmap = null) WorkingBeatmap defaultBeatmap = null)
: base(storage, contextFactory, new BeatmapStore(contextFactory), host) : base(storage, contextFactory, new BeatmapStore(contextFactory), host)
@ -86,9 +90,11 @@ namespace osu.Game.Beatmaps
beatmaps = (BeatmapStore)ModelStore; beatmaps = (BeatmapStore)ModelStore;
beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b); beatmaps.BeatmapHidden += b => BeatmapHidden?.Invoke(b);
beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b); beatmaps.BeatmapRestored += b => BeatmapRestored?.Invoke(b);
updateQueue = new BeatmapUpdateQueue(api);
} }
protected override void Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive) protected override Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default)
{ {
if (archive != null) if (archive != null)
beatmapSet.Beatmaps = createBeatmapDifficulties(archive); beatmapSet.Beatmaps = createBeatmapDifficulties(archive);
@ -104,8 +110,7 @@ namespace osu.Game.Beatmaps
validateOnlineIds(beatmapSet); validateOnlineIds(beatmapSet);
foreach (BeatmapInfo b in beatmapSet.Beatmaps) return updateQueue.UpdateAsync(beatmapSet, cancellationToken);
fetchAndPopulateOnlineValues(b);
} }
protected override void PreImport(BeatmapSetInfo beatmapSet) protected override void PreImport(BeatmapSetInfo beatmapSet)
@ -122,7 +127,7 @@ namespace osu.Game.Beatmaps
{ {
Delete(existingOnlineId); Delete(existingOnlineId);
beatmaps.PurgeDeletable(s => s.ID == existingOnlineId.ID); beatmaps.PurgeDeletable(s => s.ID == existingOnlineId.ID);
Logger.Log($"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been purged.", LoggingTarget.Database); LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been purged.");
} }
} }
} }
@ -181,10 +186,10 @@ namespace osu.Game.Beatmaps
request.Success += filename => request.Success += filename =>
{ {
Task.Factory.StartNew(() => Task.Factory.StartNew(async () =>
{ {
// This gets scheduled back to the update thread, but we want the import to run in the background. // This gets scheduled back to the update thread, but we want the import to run in the background.
Import(downloadNotification, filename); await Import(downloadNotification, filename);
currentDownloads.Remove(request); currentDownloads.Remove(request);
}, TaskCreationOptions.LongRunning); }, TaskCreationOptions.LongRunning);
}; };
@ -322,6 +327,8 @@ namespace osu.Game.Beatmaps
/// <returns>Results from the provided query.</returns> /// <returns>Results from the provided query.</returns>
public IQueryable<BeatmapInfo> QueryBeatmaps(Expression<Func<BeatmapInfo, bool>> query) => beatmaps.Beatmaps.AsNoTracking().Where(query); public IQueryable<BeatmapInfo> QueryBeatmaps(Expression<Func<BeatmapInfo, bool>> query) => beatmaps.Beatmaps.AsNoTracking().Where(query);
protected override string HumanisedModelName => "beatmap";
protected override BeatmapSetInfo CreateModel(ArchiveReader reader) protected override BeatmapSetInfo CreateModel(ArchiveReader reader)
{ {
// let's make sure there are actually .osu files to import. // let's make sure there are actually .osu files to import.
@ -381,47 +388,6 @@ namespace osu.Game.Beatmaps
return beatmapInfos; return beatmapInfos;
} }
/// <summary>
/// Query the API to populate missing values like OnlineBeatmapID / OnlineBeatmapSetID or (Rank-)Status.
/// </summary>
/// <param name="beatmap">The beatmap to populate.</param>
/// <param name="force">Whether to re-query if the provided beatmap already has populated values.</param>
/// <returns>True if population was successful.</returns>
private bool fetchAndPopulateOnlineValues(BeatmapInfo beatmap, bool force = false)
{
if (api?.State != APIState.Online)
return false;
if (!force && beatmap.OnlineBeatmapID != null && beatmap.BeatmapSet.OnlineBeatmapSetID != null
&& beatmap.Status != BeatmapSetOnlineStatus.None && beatmap.BeatmapSet.Status != BeatmapSetOnlineStatus.None)
return true;
Logger.Log("Attempting online lookup for the missing values...", LoggingTarget.Database);
try
{
var req = new GetBeatmapRequest(beatmap);
req.Perform(api);
var res = req.Result;
Logger.Log($"Successfully mapped to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}.", LoggingTarget.Database);
beatmap.Status = res.Status;
beatmap.BeatmapSet.Status = res.BeatmapSet.Status;
beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID;
beatmap.OnlineBeatmapID = res.OnlineBeatmapID;
return true;
}
catch (Exception e)
{
Logger.Log($"Failed ({e})", LoggingTarget.Database);
return false;
}
}
/// <summary> /// <summary>
/// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation.
/// </summary> /// </summary>
@ -455,5 +421,55 @@ namespace osu.Game.Beatmaps
public override bool IsImportant => false; public override bool IsImportant => false;
} }
} }
private class BeatmapUpdateQueue
{
private readonly IAPIProvider api;
private const int update_queue_request_concurrency = 4;
private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(update_queue_request_concurrency, nameof(BeatmapUpdateQueue));
public BeatmapUpdateQueue(IAPIProvider api)
{
this.api = api;
}
public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken)
{
if (api?.State != APIState.Online)
return Task.CompletedTask;
LogForModel(beatmapSet, "Performing online lookups...");
return Task.WhenAll(beatmapSet.Beatmaps.Select(b => UpdateAsync(beatmapSet, b, cancellationToken)).ToArray());
}
// todo: expose this when we need to do individual difficulty lookups.
protected Task UpdateAsync(BeatmapSetInfo beatmapSet, BeatmapInfo beatmap, CancellationToken cancellationToken)
=> Task.Factory.StartNew(() => update(beatmapSet, beatmap), cancellationToken, TaskCreationOptions.HideScheduler, updateScheduler);
private void update(BeatmapSetInfo set, BeatmapInfo beatmap)
{
if (api?.State != APIState.Online)
return;
var req = new GetBeatmapRequest(beatmap);
req.Success += res =>
{
LogForModel(set, $"Online retrieval mapped {beatmap} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}.");
beatmap.Status = res.Status;
beatmap.BeatmapSet.Status = res.BeatmapSet.Status;
beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID;
beatmap.OnlineBeatmapID = res.OnlineBeatmapID;
};
req.Failure += e => { LogForModel(set, $"Online retrieval failed for {beatmap}", e); };
// intentionally blocking to limit web request concurrency
req.Perform(api);
}
}
} }
} }

View File

@ -1,31 +1,26 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
/// <summary> /// <summary>
/// Beatmap metrics based on acculumated online data from community plays. /// Beatmap metrics based on accumulated online data from community plays.
/// </summary> /// </summary>
public class BeatmapMetrics public class BeatmapMetrics
{ {
/// <summary>
/// Total vote counts of user ratings on a scale of 0..10 where 0 is unused (probably will be fixed at API?).
/// </summary>
public IEnumerable<int> Ratings { get; set; }
/// <summary> /// <summary>
/// Points of failure on a relative time scale (usually 0..100). /// Points of failure on a relative time scale (usually 0..100).
/// </summary> /// </summary>
[JsonProperty(@"fail")] [JsonProperty(@"fail")]
public IEnumerable<int> Fails { get; set; } public int[] Fails { get; set; } = Array.Empty<int>();
/// <summary> /// <summary>
/// Points of retry on a relative time scale (usually 0..100). /// Points of retry on a relative time scale (usually 0..100).
/// </summary> /// </summary>
[JsonProperty(@"exit")] [JsonProperty(@"exit")]
public IEnumerable<int> Retries { get; set; } public int[] Retries { get; set; } = Array.Empty<int>();
} }
} }

View File

@ -32,6 +32,9 @@ namespace osu.Game.Beatmaps
[NotMapped] [NotMapped]
public BeatmapSetOnlineInfo OnlineInfo { get; set; } public BeatmapSetOnlineInfo OnlineInfo { get; set; }
[NotMapped]
public BeatmapSetMetrics Metrics { get; set; }
public double MaxStarDifficulty => Beatmaps?.Max(b => b.StarDifficulty) ?? 0; public double MaxStarDifficulty => Beatmaps?.Max(b => b.StarDifficulty) ?? 0;
[NotMapped] [NotMapped]

View File

@ -0,0 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using Newtonsoft.Json;
namespace osu.Game.Beatmaps
{
public class BeatmapSetMetrics
{
/// <summary>
/// Total vote counts of user ratings on a scale of 0..10 where 0 is unused (probably will be fixed at API?).
/// </summary>
[JsonProperty("ratings")]
public int[] Ratings { get; set; } = Array.Empty<int>();
}
}

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations; using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@ -13,6 +14,7 @@ using osu.Framework.Extensions;
using osu.Framework.IO.File; using osu.Framework.IO.File;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.IO; using osu.Game.IO;
using osu.Game.IO.Archives; using osu.Game.IO.Archives;
using osu.Game.IPC; using osu.Game.IPC;
@ -29,7 +31,7 @@ namespace osu.Game.Database
/// </summary> /// </summary>
/// <typeparam name="TModel">The model type.</typeparam> /// <typeparam name="TModel">The model type.</typeparam>
/// <typeparam name="TFileModel">The associated file join type.</typeparam> /// <typeparam name="TFileModel">The associated file join type.</typeparam>
public abstract class ArchiveModelManager<TModel, TFileModel> : ICanAcceptFiles public abstract class ArchiveModelManager<TModel, TFileModel> : ArchiveModelManager, ICanAcceptFiles
where TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete where TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete
where TFileModel : INamedFileInfo, new() where TFileModel : INamedFileInfo, new()
{ {
@ -130,56 +132,50 @@ namespace osu.Game.Database
/// This will post notifications tracking progress. /// This will post notifications tracking progress.
/// </summary> /// </summary>
/// <param name="paths">One or more archive locations on disk.</param> /// <param name="paths">One or more archive locations on disk.</param>
public void Import(params string[] paths) public Task Import(params string[] paths)
{ {
var notification = new ProgressNotification { State = ProgressNotificationState.Active }; var notification = new ProgressNotification { State = ProgressNotificationState.Active };
PostNotification?.Invoke(notification); PostNotification?.Invoke(notification);
Import(notification, paths);
return Import(notification, paths);
} }
protected void Import(ProgressNotification notification, params string[] paths) protected async Task Import(ProgressNotification notification, params string[] paths)
{ {
notification.Progress = 0; notification.Progress = 0;
notification.Text = "Import is initialising..."; notification.Text = "Import is initialising...";
var term = $"{typeof(TModel).Name.Replace("Info", "").ToLower()}";
List<TModel> imported = new List<TModel>();
int current = 0; int current = 0;
foreach (string path in paths) var imported = new List<TModel>();
await Task.WhenAll(paths.Select(async path =>
{ {
if (notification.State == ProgressNotificationState.Cancelled) notification.CancellationToken.ThrowIfCancellationRequested();
// user requested abort
return;
try try
{ {
var text = "Importing "; var model = await Import(path, notification.CancellationToken);
if (path.Length > 1) lock (imported)
text += $"{++current} of {paths.Length} {term}s.."; {
else imported.Add(model);
text += $"{term}.."; current++;
// only show the filename if it isn't a temporary one (as those look ugly). notification.Text = $"Imported {current} of {paths.Length} {HumanisedModelName}s";
if (!path.Contains(Path.GetTempPath())) notification.Progress = (float)current / paths.Length;
text += $"\n{Path.GetFileName(path)}"; }
}
notification.Text = text; catch (TaskCanceledException)
{
imported.Add(Import(path)); throw;
notification.Progress = (float)current / paths.Length;
} }
catch (Exception e) catch (Exception e)
{ {
e = e.InnerException ?? e; Logger.Error(e, $@"Could not import ({Path.GetFileName(path)})", LoggingTarget.Database);
Logger.Error(e, $@"Could not import ({Path.GetFileName(path)})");
} }
} }));
if (imported.Count == 0) if (imported.Count == 0)
{ {
@ -190,7 +186,7 @@ namespace osu.Game.Database
{ {
notification.CompletionText = imported.Count == 1 notification.CompletionText = imported.Count == 1
? $"Imported {imported.First()}!" ? $"Imported {imported.First()}!"
: $"Imported {current} {term}s!"; : $"Imported {current} {HumanisedModelName}s!";
if (imported.Count > 0 && PresentImport != null) if (imported.Count > 0 && PresentImport != null)
{ {
@ -210,12 +206,15 @@ namespace osu.Game.Database
/// Import one <see cref="TModel"/> from the filesystem and delete the file on success. /// Import one <see cref="TModel"/> from the filesystem and delete the file on success.
/// </summary> /// </summary>
/// <param name="path">The archive location on disk.</param> /// <param name="path">The archive location on disk.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <returns>The imported model, if successful.</returns> /// <returns>The imported model, if successful.</returns>
public TModel Import(string path) public async Task<TModel> Import(string path, CancellationToken cancellationToken = default)
{ {
cancellationToken.ThrowIfCancellationRequested();
TModel import; TModel import;
using (ArchiveReader reader = getReaderFrom(path)) using (ArchiveReader reader = getReaderFrom(path))
import = Import(reader); import = await Import(reader, cancellationToken);
// We may or may not want to delete the file depending on where it is stored. // We may or may not want to delete the file depending on where it is stored.
// e.g. reconstructing/repairing database with items from default storage. // e.g. reconstructing/repairing database with items from default storage.
@ -228,7 +227,7 @@ namespace osu.Game.Database
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Error(e, $@"Could not delete original file after import ({Path.GetFileName(path)})"); LogForModel(import, $@"Could not delete original file after import ({Path.GetFileName(path)})", e);
} }
return import; return import;
@ -243,23 +242,32 @@ namespace osu.Game.Database
/// Import an item from an <see cref="ArchiveReader"/>. /// Import an item from an <see cref="ArchiveReader"/>.
/// </summary> /// </summary>
/// <param name="archive">The archive to be imported.</param> /// <param name="archive">The archive to be imported.</param>
public TModel Import(ArchiveReader archive) /// <param name="cancellationToken">An optional cancellation token.</param>
public Task<TModel> Import(ArchiveReader archive, CancellationToken cancellationToken = default)
{ {
cancellationToken.ThrowIfCancellationRequested();
TModel model = null;
try try
{ {
var model = CreateModel(archive); model = CreateModel(archive);
if (model == null) return null; if (model == null) return null;
model.Hash = computeHash(archive); model.Hash = computeHash(archive);
}
return Import(model, archive); catch (TaskCanceledException)
{
throw;
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Error(e, $"Model creation of {archive.Name} failed.", LoggingTarget.Database); LogForModel(model, $"Model creation of {archive.Name} failed.", e);
return null; return null;
} }
return Import(model, archive, cancellationToken);
} }
/// <summary> /// <summary>
@ -269,6 +277,16 @@ namespace osu.Game.Database
/// </summary> /// </summary>
protected abstract string[] HashableFileTypes { get; } protected abstract string[] HashableFileTypes { get; }
protected static void LogForModel(TModel model, string message, Exception e = null)
{
string prefix = $"[{(model?.Hash ?? "?????").Substring(0, 5)}]";
if (e != null)
Logger.Error(e, $"{prefix} {message}", LoggingTarget.Database);
else
Logger.Log($"{prefix} {message}", LoggingTarget.Database);
}
/// <summary> /// <summary>
/// Create a SHA-2 hash from the provided archive based on file content of all files matching <see cref="HashableFileTypes"/>. /// Create a SHA-2 hash from the provided archive based on file content of all files matching <see cref="HashableFileTypes"/>.
/// </summary> /// </summary>
@ -288,13 +306,30 @@ namespace osu.Game.Database
/// </summary> /// </summary>
/// <param name="item">The model to be imported.</param> /// <param name="item">The model to be imported.</param>
/// <param name="archive">An optional archive to use for model population.</param> /// <param name="archive">An optional archive to use for model population.</param>
public TModel Import(TModel item, ArchiveReader archive = null) /// <param name="cancellationToken">An optional cancellation token.</param>
public async Task<TModel> Import(TModel item, ArchiveReader archive = null, CancellationToken cancellationToken = default) => await Task.Factory.StartNew(async () =>
{ {
cancellationToken.ThrowIfCancellationRequested();
delayEvents(); delayEvents();
void rollback()
{
if (!Delete(item))
{
// We may have not yet added the model to the underlying table, but should still clean up files.
LogForModel(item, "Dereferencing files for incomplete import.");
Files.Dereference(item.Files.Select(f => f.FileInfo).ToArray());
}
}
try try
{ {
Logger.Log($"Importing {item}...", LoggingTarget.Database); LogForModel(item, "Beginning import...");
item.Files = archive != null ? createFileInfos(archive, Files) : new List<TFileModel>();
await Populate(item, archive, cancellationToken);
using (var write = ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes. using (var write = ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes.
{ {
@ -302,11 +337,6 @@ namespace osu.Game.Database
{ {
if (!write.IsTransactionLeader) throw new InvalidOperationException($"Ensure there is no parent transaction so errors can correctly be handled by {this}"); if (!write.IsTransactionLeader) throw new InvalidOperationException($"Ensure there is no parent transaction so errors can correctly be handled by {this}");
if (archive != null)
item.Files = createFileInfos(archive, Files);
Populate(item, archive);
var existing = CheckForExisting(item); var existing = CheckForExisting(item);
if (existing != null) if (existing != null)
@ -314,15 +344,17 @@ namespace osu.Game.Database
if (CanUndelete(existing, item)) if (CanUndelete(existing, item))
{ {
Undelete(existing); Undelete(existing);
Logger.Log($"Found existing {typeof(TModel)} for {item} (ID {existing.ID}). Skipping import.", LoggingTarget.Database); LogForModel(item, $"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) skipping import.");
handleEvent(() => ItemAdded?.Invoke(existing, true)); handleEvent(() => ItemAdded?.Invoke(existing, true));
// existing item will be used; rollback new import and exit early.
rollback();
flushEvents(true);
return existing; return existing;
} }
else
{ Delete(existing);
Delete(existing); ModelStore.PurgeDeletable(s => s.ID == existing.ID);
ModelStore.PurgeDeletable(s => s.ID == existing.ID);
}
} }
PreImport(item); PreImport(item);
@ -337,21 +369,21 @@ namespace osu.Game.Database
} }
} }
Logger.Log($"Import of {item} successfully completed!", LoggingTarget.Database); LogForModel(item, "Import successfully completed!");
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Error(e, $"Import of {item} failed and has been rolled back.", LoggingTarget.Database); if (!(e is TaskCanceledException))
item = null; LogForModel(item, "Database import or population failed and has been rolled back.", e);
}
finally rollback();
{ flushEvents(false);
// we only want to flush events after we've confirmed the write context didn't have any errors. throw;
flushEvents(item != null);
} }
flushEvents(true);
return item; return item;
} }, cancellationToken, TaskCreationOptions.HideScheduler, IMPORT_SCHEDULER).Unwrap();
/// <summary> /// <summary>
/// Perform an update of the specified item. /// Perform an update of the specified item.
@ -392,7 +424,8 @@ namespace osu.Game.Database
var notification = new ProgressNotification var notification = new ProgressNotification
{ {
Progress = 0, Progress = 0,
CompletionText = $"Deleted all {typeof(TModel).Name.Replace("Info", "").ToLower()}s!", Text = $"Preparing to delete all {HumanisedModelName}s...",
CompletionText = $"Deleted all {HumanisedModelName}s!",
State = ProgressNotificationState.Active, State = ProgressNotificationState.Active,
}; };
@ -409,7 +442,7 @@ namespace osu.Game.Database
// user requested abort // user requested abort
return; return;
notification.Text = $"Deleting ({++i} of {items.Count})"; notification.Text = $"Deleting {HumanisedModelName}s ({++i} of {items.Count})";
Delete(b); Delete(b);
@ -533,7 +566,7 @@ namespace osu.Game.Database
return Task.CompletedTask; return Task.CompletedTask;
} }
return Task.Factory.StartNew(() => Import(stable.GetDirectories(ImportFromStablePath).Select(f => stable.GetFullPath(f)).ToArray()), TaskCreationOptions.LongRunning); return Task.Run(async () => await Import(stable.GetDirectories(ImportFromStablePath).Select(f => stable.GetFullPath(f)).ToArray()));
} }
#endregion #endregion
@ -552,9 +585,8 @@ namespace osu.Game.Database
/// </summary> /// </summary>
/// <param name="model">The model to populate.</param> /// <param name="model">The model to populate.</param>
/// <param name="archive">The archive to use as a reference for population. May be null.</param> /// <param name="archive">The archive to use as a reference for population. May be null.</param>
protected virtual void Populate(TModel model, [CanBeNull] ArchiveReader archive) /// <param name="cancellationToken">An optional cancellation token.</param>
{ protected virtual Task Populate(TModel model, [CanBeNull] ArchiveReader archive, CancellationToken cancellationToken = default) => Task.CompletedTask;
}
/// <summary> /// <summary>
/// Perform any final actions before the import to database executes. /// Perform any final actions before the import to database executes.
@ -582,6 +614,8 @@ namespace osu.Game.Database
private DbSet<TModel> queryModel() => ContextFactory.Get().Set<TModel>(); private DbSet<TModel> queryModel() => ContextFactory.Get().Set<TModel>();
protected virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace("Info", "").ToLower()}";
/// <summary> /// <summary>
/// Creates an <see cref="ArchiveReader"/> from a valid storage path. /// Creates an <see cref="ArchiveReader"/> from a valid storage path.
/// </summary> /// </summary>
@ -599,4 +633,18 @@ namespace osu.Game.Database
throw new InvalidFormatException($"{path} is not a valid archive"); throw new InvalidFormatException($"{path} is not a valid archive");
} }
} }
public abstract class ArchiveModelManager
{
private const int import_queue_request_concurrency = 1;
/// <summary>
/// A singleton scheduler shared by all <see cref="ArchiveModelManager{TModel,TFileModel}"/>.
/// </summary>
/// <remarks>
/// This scheduler generally performs IO and CPU intensive work so concurrency is limited harshly.
/// It is mainly being used as a queue mechanism for large imports.
/// </remarks>
protected static readonly ThreadedTaskScheduler IMPORT_SCHEDULER = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(ArchiveModelManager));
}
} }

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Threading.Tasks;
namespace osu.Game.Database namespace osu.Game.Database
{ {
/// <summary> /// <summary>
@ -12,7 +14,7 @@ namespace osu.Game.Database
/// Import the specified paths. /// Import the specified paths.
/// </summary> /// </summary>
/// <param name="paths">The files which should be imported.</param> /// <param name="paths">The files which should be imported.</param>
void Import(params string[] paths); Task Import(params string[] paths);
/// <summary> /// <summary>
/// An array of accepted file extensions (in the standard format of ".abc"). /// An array of accepted file extensions (in the standard format of ".abc").

View File

@ -27,7 +27,7 @@ namespace osu.Game.Graphics.Containers
protected void BeginConfirm() protected void BeginConfirm()
{ {
if (confirming || !AllowMultipleFires && fired) return; if (confirming || (!AllowMultipleFires && fired)) return;
confirming = true; confirming = true;

View File

@ -54,7 +54,7 @@ namespace osu.Game.Graphics.Containers
samplePopIn = audio.Samples.Get(@"UI/overlay-pop-in"); samplePopIn = audio.Samples.Get(@"UI/overlay-pop-in");
samplePopOut = audio.Samples.Get(@"UI/overlay-pop-out"); samplePopOut = audio.Samples.Get(@"UI/overlay-pop-out");
StateChanged += onStateChanged; State.ValueChanged += onStateChanged;
} }
/// <summary> /// <summary>
@ -70,7 +70,7 @@ namespace osu.Game.Graphics.Containers
{ {
if (!base.ReceivePositionalInputAt(e.ScreenSpaceMousePosition)) if (!base.ReceivePositionalInputAt(e.ScreenSpaceMousePosition))
{ {
State = Visibility.Hidden; Hide();
return true; return true;
} }
@ -82,7 +82,7 @@ namespace osu.Game.Graphics.Containers
switch (action) switch (action)
{ {
case GlobalAction.Back: case GlobalAction.Back:
State = Visibility.Hidden; Hide();
return true; return true;
case GlobalAction.Select: case GlobalAction.Select:
@ -94,9 +94,9 @@ namespace osu.Game.Graphics.Containers
public bool OnReleased(GlobalAction action) => false; public bool OnReleased(GlobalAction action) => false;
private void onStateChanged(Visibility visibility) private void onStateChanged(ValueChangedEvent<Visibility> state)
{ {
switch (visibility) switch (state.NewValue)
{ {
case Visibility.Visible: case Visibility.Visible:
if (OverlayActivationMode.Value != OverlayActivation.Disabled) if (OverlayActivationMode.Value != OverlayActivation.Disabled)
@ -105,7 +105,7 @@ namespace osu.Game.Graphics.Containers
if (BlockScreenWideMouse && DimMainContent) osuGame?.AddBlockingOverlay(this); if (BlockScreenWideMouse && DimMainContent) osuGame?.AddBlockingOverlay(this);
} }
else else
State = Visibility.Hidden; Hide();
break; break;

View File

@ -1,13 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osuTK;
using osuTK.Graphics;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Graphics.Containers namespace osu.Game.Graphics.Containers
{ {
public class OsuScrollContainer : ScrollContainer public class OsuScrollContainer : ScrollContainer<Drawable>
{ {
/// <summary> /// <summary>
/// Allows controlling the scroll bar from any position in the container using the right mouse button. /// Allows controlling the scroll bar from any position in the container using the right mouse button.
@ -28,6 +33,11 @@ namespace osu.Game.Graphics.Containers
protected override bool IsDragging => base.IsDragging || mouseScrollBarDragging; protected override bool IsDragging => base.IsDragging || mouseScrollBarDragging;
public OsuScrollContainer(Direction scrollDirection = Direction.Vertical)
: base(scrollDirection)
{
}
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)
{ {
if (shouldPerformRightMouseScroll(e)) if (shouldPerformRightMouseScroll(e))
@ -71,5 +81,87 @@ namespace osu.Game.Graphics.Containers
return base.OnDragEnd(e); return base.OnDragEnd(e);
} }
protected override ScrollbarContainer CreateScrollbar(Direction direction) => new OsuScrollbar(direction);
protected class OsuScrollbar : ScrollbarContainer
{
private const float dim_size = 10;
private Color4 hoverColour;
private Color4 defaultColour;
private Color4 highlightColour;
private readonly Box box;
public OsuScrollbar(Direction scrollDir)
: base(scrollDir)
{
Blending = BlendingMode.Additive;
CornerRadius = 5;
const float margin = 3;
Margin = new MarginPadding
{
Left = scrollDir == Direction.Vertical ? margin : 0,
Right = scrollDir == Direction.Vertical ? margin : 0,
Top = scrollDir == Direction.Horizontal ? margin : 0,
Bottom = scrollDir == Direction.Horizontal ? margin : 0,
};
Masking = true;
Child = box = new Box { RelativeSizeAxes = Axes.Both };
ResizeTo(1);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Colour = defaultColour = colours.Gray8;
hoverColour = colours.GrayF;
highlightColour = colours.Green;
}
public override void ResizeTo(float val, int duration = 0, Easing easing = Easing.None)
{
Vector2 size = new Vector2(dim_size)
{
[(int)ScrollDirection] = val
};
this.ResizeTo(size, duration, easing);
}
protected override bool OnHover(HoverEvent e)
{
this.FadeColour(hoverColour, 100);
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
this.FadeColour(defaultColour, 100);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (!base.OnMouseDown(e)) return false;
//note that we are changing the colour of the box here as to not interfere with the hover effect.
box.FadeColour(highlightColour, 100);
return true;
}
protected override bool OnMouseUp(MouseUpEvent e)
{
if (e.Button != MouseButton.Left) return false;
box.FadeColour(Color4.White, 100);
return base.OnMouseUp(e);
}
}
} }
} }

View File

@ -16,7 +16,7 @@ namespace osu.Game.Graphics.Containers
where T : Drawable where T : Drawable
{ {
private Drawable expandableHeader, fixedHeader, footer, headerBackground; private Drawable expandableHeader, fixedHeader, footer, headerBackground;
private readonly ScrollContainer scrollContainer; private readonly OsuScrollContainer scrollContainer;
private readonly Container headerBackgroundContainer; private readonly Container headerBackgroundContainer;
private readonly FlowContainer<T> scrollContentContainer; private readonly FlowContainer<T> scrollContentContainer;
@ -124,7 +124,7 @@ namespace osu.Game.Graphics.Containers
public SectionsContainer() public SectionsContainer()
{ {
AddInternal(scrollContainer = new ScrollContainer AddInternal(scrollContainer = new OsuScrollContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Masking = true, Masking = true,

View File

@ -103,7 +103,7 @@ namespace osu.Game.Graphics.Containers
protected override void PopIn() protected override void PopIn()
{ {
foreach (var w in wavesContainer.Children) foreach (var w in wavesContainer.Children)
w.State = Visibility.Visible; w.Show();
this.FadeIn(100, Easing.OutQuint); this.FadeIn(100, Easing.OutQuint);
contentContainer.MoveToY(0, APPEAR_DURATION, Easing.OutQuint); contentContainer.MoveToY(0, APPEAR_DURATION, Easing.OutQuint);
@ -117,7 +117,7 @@ namespace osu.Game.Graphics.Containers
contentContainer.MoveToY(DrawHeight * 2f, DISAPPEAR_DURATION, Easing.In); contentContainer.MoveToY(DrawHeight * 2f, DISAPPEAR_DURATION, Easing.In);
foreach (var w in wavesContainer.Children) foreach (var w in wavesContainer.Children)
w.State = Visibility.Hidden; w.Hide();
this.FadeOut(DISAPPEAR_DURATION, Easing.InQuint); this.FadeOut(DISAPPEAR_DURATION, Easing.InQuint);
} }

View File

@ -29,7 +29,7 @@ namespace osu.Game.Graphics.Cursor
{ {
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
{ {
Cursor = new MenuCursor { State = Visibility.Hidden }, Cursor = new MenuCursor { State = { Value = Visibility.Hidden } },
content = new Container { RelativeSizeAxes = Axes.Both } content = new Container { RelativeSizeAxes = Axes.Both }
}); });
} }

View File

@ -82,6 +82,10 @@ namespace osu.Game.Graphics.UserInterface
} }
} }
public override void Hide() => State = Visibility.Hidden;
public override void Show() => State = Visibility.Visible;
public BreadcrumbTabItem(T value) public BreadcrumbTabItem(T value)
: base(value) : base(value)
{ {

View File

@ -11,7 +11,7 @@ namespace osu.Game.Graphics.UserInterface
{ {
public class IconButton : OsuAnimatedButton public class IconButton : OsuAnimatedButton
{ {
public const float BUTTON_SIZE = 30; public const float DEFAULT_BUTTON_SIZE = 30;
private Color4? iconColour; private Color4? iconColour;
@ -57,26 +57,11 @@ namespace osu.Game.Graphics.UserInterface
set => icon.Scale = value; set => icon.Scale = value;
} }
/// <summary>
/// The size of the <see cref="IconButton"/> while it is not being pressed.
/// </summary>
public Vector2 ButtonSize
{
get => Content.Size;
set
{
Content.RelativeSizeAxes = Axes.None;
Content.AutoSizeAxes = Axes.None;
Content.Size = value;
}
}
private readonly SpriteIcon icon; private readonly SpriteIcon icon;
public IconButton() public IconButton()
{ {
AutoSizeAxes = Axes.Both; Size = new Vector2(DEFAULT_BUTTON_SIZE);
ButtonSize = new Vector2(BUTTON_SIZE);
Add(icon = new SpriteIcon Add(icon = new SpriteIcon
{ {

View File

@ -19,14 +19,14 @@ namespace osu.Game.Graphics.UserInterface
public class OsuAnimatedButton : OsuClickableContainer public class OsuAnimatedButton : OsuClickableContainer
{ {
/// <summary> /// <summary>
/// The colour that should be flashed when the <see cref="IconButton"/> is clicked. /// The colour that should be flashed when the <see cref="OsuAnimatedButton"/> is clicked.
/// </summary> /// </summary>
protected Color4 FlashColour = Color4.White.Opacity(0.3f); protected Color4 FlashColour = Color4.White.Opacity(0.3f);
private Color4 hoverColour = Color4.White.Opacity(0.1f); private Color4 hoverColour = Color4.White.Opacity(0.1f);
/// <summary> /// <summary>
/// The background colour of the <see cref="IconButton"/> while it is hovered. /// The background colour of the <see cref="OsuAnimatedButton"/> while it is hovered.
/// </summary> /// </summary>
protected Color4 HoverColour protected Color4 HoverColour
{ {

View File

@ -17,11 +17,11 @@ namespace osu.Game.Graphics.UserInterface
/// <summary> /// <summary>
/// A button with added default sound effects. /// A button with added default sound effects.
/// </summary> /// </summary>
public abstract class OsuButton : Button public class OsuButton : Button
{ {
private Box hover; private Box hover;
protected OsuButton() public OsuButton()
{ {
Height = 40; Height = 40;

View File

@ -6,10 +6,9 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Containers;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
@ -33,27 +32,26 @@ namespace osu.Game.Graphics.UserInterface
public string LabelText public string LabelText
{ {
get => labelSpriteText?.Text;
set set
{ {
if (labelSpriteText != null) if (labelText != null)
labelSpriteText.Text = value; labelText.Text = value;
} }
} }
public MarginPadding LabelPadding public MarginPadding LabelPadding
{ {
get => labelSpriteText?.Padding ?? new MarginPadding(); get => labelText?.Padding ?? new MarginPadding();
set set
{ {
if (labelSpriteText != null) if (labelText != null)
labelSpriteText.Padding = value; labelText.Padding = value;
} }
} }
protected readonly Nub Nub; protected readonly Nub Nub;
private readonly SpriteText labelSpriteText; private readonly OsuTextFlowContainer labelText;
private SampleChannel sampleChecked; private SampleChannel sampleChecked;
private SampleChannel sampleUnchecked; private SampleChannel sampleUnchecked;
@ -62,24 +60,28 @@ namespace osu.Game.Graphics.UserInterface
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
const float nub_padding = 5;
Children = new Drawable[] Children = new Drawable[]
{ {
labelSpriteText = new OsuSpriteText(), labelText = new OsuTextFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Padding = new MarginPadding { Right = Nub.EXPANDED_SIZE + nub_padding }
},
Nub = new Nub Nub = new Nub
{ {
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
Margin = new MarginPadding { Right = 5 }, Margin = new MarginPadding { Right = nub_padding },
}, },
new HoverClickSounds() new HoverClickSounds()
}; };
Nub.Current.BindTo(Current); Nub.Current.BindTo(Current);
Current.DisabledChanged += disabled => Current.DisabledChanged += disabled => labelText.Alpha = Nub.Alpha = disabled ? 0.3f : 1;
{
labelSpriteText.Alpha = Nub.Alpha = disabled ? 0.3f : 1;
};
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -34,7 +34,7 @@ namespace osu.Game.Graphics.UserInterface
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Alpha = 0.9f, Alpha = 0.9f,
}, },
new LoadingAnimation { State = Visibility.Visible } new LoadingAnimation { State = { Value = Visibility.Visible } }
}; };
} }

View File

@ -2,8 +2,11 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Logging; using osu.Framework.Logging;
@ -27,6 +30,13 @@ namespace osu.Game.IO
Store = new StorageBackedResourceStore(Storage); Store = new StorageBackedResourceStore(Storage);
} }
/// <summary>
/// Perform a lookup query on available <see cref="FileInfo"/>s.
/// </summary>
/// <param name="query">The query.</param>
/// <returns>Results from the provided query.</returns>
public IEnumerable<FileInfo> QueryFiles(Expression<Func<FileInfo, bool>> query) => ContextFactory.Get().Set<FileInfo>().AsNoTracking().Where(f => f.ReferenceCount > 0).Where(query);
public FileInfo Add(Stream data, bool reference = true) public FileInfo Add(Stream data, bool reference = true)
{ {
using (var usage = ContextFactory.GetForWrite()) using (var usage = ContextFactory.GetForWrite())

View File

@ -38,7 +38,7 @@ namespace osu.Game.IPC
} }
if (importer.HandledExtensions.Contains(Path.GetExtension(path)?.ToLowerInvariant())) if (importer.HandledExtensions.Contains(Path.GetExtension(path)?.ToLowerInvariant()))
importer.Import(path); await importer.Import(path);
} }
} }

View File

@ -37,7 +37,9 @@ namespace osu.Game.Online.API
public Bindable<User> LocalUser { get; } = new Bindable<User>(createGuestUser()); public Bindable<User> LocalUser { get; } = new Bindable<User>(createGuestUser());
protected bool HasLogin => authentication.Token.Value != null || !string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password); public Bindable<UserActivity> Activity { get; } = new Bindable<UserActivity>();
protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password));
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource(); private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
@ -55,6 +57,12 @@ namespace osu.Game.Online.API
authentication.TokenString = config.Get<string>(OsuSetting.Token); authentication.TokenString = config.Get<string>(OsuSetting.Token);
authentication.Token.ValueChanged += onTokenChanged; authentication.Token.ValueChanged += onTokenChanged;
LocalUser.BindValueChanged(u =>
{
u.OldValue?.Activity.UnbindFrom(Activity);
u.NewValue.Activity.BindTo(Activity);
}, true);
var thread = new Thread(run) var thread = new Thread(run)
{ {
Name = "APIAccess", Name = "APIAccess",
@ -254,8 +262,9 @@ namespace osu.Game.Online.API
handleWebException(we); handleWebException(we);
return false; return false;
} }
catch catch (Exception ex)
{ {
Logger.Error(ex, "Error occurred while handling an API request.");
return false; return false;
} }
} }

View File

@ -10,9 +10,18 @@ namespace osu.Game.Online.API
{ {
private string filename; private string filename;
/// <summary>
/// Used to set the extension of the file returned by this request.
/// </summary>
protected virtual string FileExtension { get; } = @".tmp";
protected override WebRequest CreateWebRequest() protected override WebRequest CreateWebRequest()
{ {
var request = new FileWebRequest(filename = Path.GetTempFileName(), Uri); var file = Path.GetTempFileName();
File.Move(file, filename = Path.ChangeExtension(file, FileExtension));
var request = new FileWebRequest(filename, Uri);
request.DownloadProgress += request_Progress; request.DownloadProgress += request_Progress;
return request; return request;
} }

View File

@ -17,6 +17,8 @@ namespace osu.Game.Online.API
Id = 1001, Id = 1001,
}); });
public Bindable<UserActivity> Activity { get; } = new Bindable<UserActivity>();
public bool IsLoggedIn => true; public bool IsLoggedIn => true;
public string ProvidedUsername => LocalUser.Value.Username; public string ProvidedUsername => LocalUser.Value.Username;
@ -41,6 +43,15 @@ namespace osu.Game.Online.API
} }
} }
public DummyAPIAccess()
{
LocalUser.BindValueChanged(u =>
{
u.OldValue?.Activity.UnbindFrom(Activity);
u.NewValue.Activity.BindTo(Activity);
}, true);
}
public virtual void Queue(APIRequest request) public virtual void Queue(APIRequest request)
{ {
} }

View File

@ -13,6 +13,11 @@ namespace osu.Game.Online.API
/// </summary> /// </summary>
Bindable<User> LocalUser { get; } Bindable<User> LocalUser { get; }
/// <summary>
/// The current user's activity.
/// </summary>
Bindable<UserActivity> Activity { get; }
/// <summary> /// <summary>
/// Returns whether the local user is logged in. /// Returns whether the local user is logged in.
/// </summary> /// </summary>

View File

@ -1,20 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests
{
public class GetBeatmapDetailsRequest : APIRequest<APIBeatmapMetrics>
{
private readonly BeatmapInfo beatmap;
public GetBeatmapDetailsRequest(BeatmapInfo beatmap)
{
this.beatmap = beatmap;
}
protected override string Target => $@"beatmaps/{beatmap.OnlineBeatmapID}";
}
}

View File

@ -57,6 +57,9 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"version")] [JsonProperty(@"version")]
private string version { get; set; } private string version { get; set; }
[JsonProperty(@"failtimes")]
private BeatmapMetrics metrics { get; set; }
public BeatmapInfo ToBeatmap(RulesetStore rulesets) public BeatmapInfo ToBeatmap(RulesetStore rulesets)
{ {
var set = BeatmapSet?.ToBeatmapSet(rulesets); var set = BeatmapSet?.ToBeatmapSet(rulesets);
@ -70,6 +73,7 @@ namespace osu.Game.Online.API.Requests.Responses
Version = version, Version = version,
Status = Status, Status = Status,
BeatmapSet = set, BeatmapSet = set,
Metrics = metrics,
BaseDifficulty = new BeatmapDifficulty BaseDifficulty = new BeatmapDifficulty
{ {
DrainRate = drainRate, DrainRate = drainRate,

View File

@ -1,29 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Newtonsoft.Json;
using osu.Game.Beatmaps;
namespace osu.Game.Online.API.Requests.Responses
{
public class APIBeatmapMetrics : BeatmapMetrics
{
//the online API returns some metrics as a nested object.
[JsonProperty(@"failtimes")]
private BeatmapMetrics failTimes
{
set
{
Fails = value.Fails;
Retries = value.Retries;
}
}
//and other metrics in the beatmap set.
[JsonProperty(@"beatmapset")]
private BeatmapMetrics beatmapSet
{
set => Ratings = value.Ratings;
}
}
}

View File

@ -54,6 +54,9 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"last_updated")] [JsonProperty(@"last_updated")]
private DateTimeOffset lastUpdated { get; set; } private DateTimeOffset lastUpdated { get; set; }
[JsonProperty(@"ratings")]
private int[] ratings { get; set; }
[JsonProperty(@"user_id")] [JsonProperty(@"user_id")]
private long creatorId private long creatorId
{ {
@ -70,6 +73,7 @@ namespace osu.Game.Online.API.Requests.Responses
OnlineBeatmapSetID = OnlineBeatmapSetID, OnlineBeatmapSetID = OnlineBeatmapSetID,
Metadata = this, Metadata = this,
Status = Status, Status = Status,
Metrics = ratings == null ? null : new BeatmapSetMetrics { Ratings = ratings },
OnlineInfo = new BeatmapSetOnlineInfo OnlineInfo = new BeatmapSetOnlineInfo
{ {
Covers = covers, Covers = covers,

View File

@ -31,9 +31,9 @@ namespace osu.Game.Online.API.Requests.Responses
public List<APIChangelogEntry> ChangelogEntries { get; set; } public List<APIChangelogEntry> ChangelogEntries { get; set; }
[JsonProperty("versions")] [JsonProperty("versions")]
public VersionNatigation Versions { get; set; } public VersionNavigation Versions { get; set; }
public class VersionNatigation public class VersionNavigation
{ {
[JsonProperty("next")] [JsonProperty("next")]
public APIChangelogBuild Next { get; set; } public APIChangelogBuild Next { get; set; }

View File

@ -45,7 +45,7 @@ namespace osu.Game.Online.API.Requests.Responses
case "cuttingedge": case "cuttingedge":
return new Color4(238, 170, 0, 255); return new Color4(238, 170, 0, 255);
case "lazer": case OsuGameBase.CLIENT_STREAM_NAME:
return new Color4(237, 18, 33, 255); return new Color4(237, 18, 33, 255);
case "web": case "web":

View File

@ -30,7 +30,10 @@ namespace osu.Game.Online.API.Requests
req.ContentType = "application/json"; req.ContentType = "application/json";
req.Method = HttpMethod.Put; req.Method = HttpMethod.Put;
req.AddRaw(JsonConvert.SerializeObject(scoreInfo)); req.AddRaw(JsonConvert.SerializeObject(scoreInfo, new JsonSerializerSettings
{
ReferenceLoopHandling = ReferenceLoopHandling.Ignore
}));
return req; return req;
} }

View File

@ -69,7 +69,7 @@ namespace osu.Game.Online.Chat
if (displayText.Length == 0 || linkText.Length == 0) continue; if (displayText.Length == 0 || linkText.Length == 0) continue;
// Check for encapsulated links // Check for encapsulated links
if (result.Links.Find(l => l.Index <= index && l.Index + l.Length >= index + m.Length || index <= l.Index && index + m.Length >= l.Index + l.Length) == null) if (result.Links.Find(l => (l.Index <= index && l.Index + l.Length >= index + m.Length) || (index <= l.Index && index + m.Length >= l.Index + l.Length)) == null)
{ {
result.Text = result.Text.Remove(index, m.Length).Insert(index, displayText); result.Text = result.Text.Remove(index, m.Length).Insert(index, displayText);

View File

@ -23,7 +23,7 @@ namespace osu.Game.Online.Leaderboards
{ {
private const double fade_duration = 300; private const double fade_duration = 300;
private readonly ScrollContainer scrollContainer; private readonly OsuScrollContainer scrollContainer;
private readonly Container placeholderContainer; private readonly Container placeholderContainer;
private FillFlowContainer<LeaderboardScore> scrollFlow; private FillFlowContainer<LeaderboardScore> scrollFlow;

View File

@ -132,12 +132,12 @@ namespace osu.Game
public void CloseAllOverlays(bool hideToolbarElements = true) public void CloseAllOverlays(bool hideToolbarElements = true)
{ {
foreach (var overlay in overlays) foreach (var overlay in overlays)
overlay.State = Visibility.Hidden; overlay.Hide();
if (hideToolbarElements) if (hideToolbarElements)
{ {
foreach (var overlay in toolbarElements) foreach (var overlay in toolbarElements)
overlay.State = Visibility.Hidden; overlay.Hide();
} }
} }
@ -181,7 +181,7 @@ namespace osu.Game
configSkin.ValueChanged += skinId => SkinManager.CurrentSkinInfo.Value = SkinManager.Query(s => s.ID == skinId.NewValue) ?? SkinInfo.Default; configSkin.ValueChanged += skinId => SkinManager.CurrentSkinInfo.Value = SkinManager.Query(s => s.ID == skinId.NewValue) ?? SkinInfo.Default;
configSkin.TriggerChange(); configSkin.TriggerChange();
LocalConfig.BindWith(OsuSetting.VolumeInactive, inactiveVolumeAdjust); LocalConfig.BindWith(OsuSetting.VolumeInactive, userInactiveVolume);
IsActive.BindValueChanged(active => updateActiveState(active.NewValue), true); IsActive.BindValueChanged(active => updateActiveState(active.NewValue), true);
} }
@ -461,7 +461,7 @@ namespace osu.Game
loadComponentSingleFile(new DialogOverlay(), topMostOverlayContent.Add, true); loadComponentSingleFile(new DialogOverlay(), topMostOverlayContent.Add, true);
loadComponentSingleFile(externalLinkOpener = new ExternalLinkOpener(), topMostOverlayContent.Add); loadComponentSingleFile(externalLinkOpener = new ExternalLinkOpener(), topMostOverlayContent.Add);
chatOverlay.StateChanged += state => channelManager.HighPollRate.Value = state == Visibility.Visible; chatOverlay.State.ValueChanged += state => channelManager.HighPollRate.Value = state.NewValue == Visibility.Visible;
Add(externalLinkOpener = new ExternalLinkOpener()); Add(externalLinkOpener = new ExternalLinkOpener());
@ -470,9 +470,9 @@ namespace osu.Game
foreach (var overlay in singleDisplaySideOverlays) foreach (var overlay in singleDisplaySideOverlays)
{ {
overlay.StateChanged += state => overlay.State.ValueChanged += state =>
{ {
if (state == Visibility.Hidden) return; if (state.NewValue == Visibility.Hidden) return;
singleDisplaySideOverlays.Where(o => o != overlay).ForEach(o => o.Hide()); singleDisplaySideOverlays.Where(o => o != overlay).ForEach(o => o.Hide());
}; };
@ -484,9 +484,9 @@ namespace osu.Game
foreach (var overlay in informationalOverlays) foreach (var overlay in informationalOverlays)
{ {
overlay.StateChanged += state => overlay.State.ValueChanged += state =>
{ {
if (state == Visibility.Hidden) return; if (state.NewValue == Visibility.Hidden) return;
informationalOverlays.Where(o => o != overlay).ForEach(o => o.Hide()); informationalOverlays.Where(o => o != overlay).ForEach(o => o.Hide());
}; };
@ -498,12 +498,12 @@ namespace osu.Game
foreach (var overlay in singleDisplayOverlays) foreach (var overlay in singleDisplayOverlays)
{ {
overlay.StateChanged += state => overlay.State.ValueChanged += state =>
{ {
// informational overlays should be dismissed on a show or hide of a full overlay. // informational overlays should be dismissed on a show or hide of a full overlay.
informationalOverlays.ForEach(o => o.Hide()); informationalOverlays.ForEach(o => o.Hide());
if (state == Visibility.Hidden) return; if (state.NewValue == Visibility.Hidden) return;
singleDisplayOverlays.Where(o => o != overlay).ForEach(o => o.Hide()); singleDisplayOverlays.Where(o => o != overlay).ForEach(o => o.Hide());
}; };
@ -518,16 +518,16 @@ namespace osu.Game
{ {
float offset = 0; float offset = 0;
if (settings.State == Visibility.Visible) if (settings.State.Value == Visibility.Visible)
offset += ToolbarButton.WIDTH / 2; offset += ToolbarButton.WIDTH / 2;
if (notifications.State == Visibility.Visible) if (notifications.State.Value == Visibility.Visible)
offset -= ToolbarButton.WIDTH / 2; offset -= ToolbarButton.WIDTH / 2;
screenContainer.MoveToX(offset, SettingsPanel.TRANSITION_LENGTH, Easing.OutQuint); screenContainer.MoveToX(offset, SettingsPanel.TRANSITION_LENGTH, Easing.OutQuint);
} }
settings.StateChanged += _ => updateScreenOffset(); settings.State.ValueChanged += _ => updateScreenOffset();
notifications.StateChanged += _ => updateScreenOffset(); notifications.State.ValueChanged += _ => updateScreenOffset();
} }
public class GameIdleTracker : IdleTracker public class GameIdleTracker : IdleTracker
@ -686,16 +686,28 @@ namespace osu.Game
return false; return false;
} }
private readonly BindableDouble inactiveVolumeAdjust = new BindableDouble(); #region Inactive audio dimming
private readonly BindableDouble userInactiveVolume = new BindableDouble();
private readonly BindableDouble inactiveVolumeFade = new BindableDouble();
private void updateActiveState(bool isActive) private void updateActiveState(bool isActive)
{ {
if (isActive) if (isActive)
Audio.RemoveAdjustment(AdjustableProperty.Volume, inactiveVolumeAdjust); {
this.TransformBindableTo(inactiveVolumeFade, 1, 500, Easing.OutQuint)
.Finally(_ => Audio.RemoveAdjustment(AdjustableProperty.Volume, inactiveVolumeFade)); //wait for the transition to finish to remove the inactive audio adjustment
}
else else
Audio.AddAdjustment(AdjustableProperty.Volume, inactiveVolumeAdjust); {
Audio.AddAdjustment(AdjustableProperty.Volume, inactiveVolumeFade);
this.TransformBindableTo(inactiveVolumeFade, userInactiveVolume.Value, 1500, Easing.OutSine);
}
} }
#endregion
public bool OnReleased(GlobalAction action) => false; public bool OnReleased(GlobalAction action) => false;
private Container overlayContent; private Container overlayContent;
@ -768,7 +780,7 @@ namespace osu.Game
if (newOsuScreen.HideOverlaysOnEnter) if (newOsuScreen.HideOverlaysOnEnter)
CloseAllOverlays(); CloseAllOverlays();
else else
Toolbar.State = Visibility.Visible; Toolbar.Show();
} }
} }

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -44,6 +45,8 @@ namespace osu.Game
/// </summary> /// </summary>
public class OsuGameBase : Framework.Game, ICanAcceptFiles public class OsuGameBase : Framework.Game, ICanAcceptFiles
{ {
public const string CLIENT_STREAM_NAME = "lazer";
protected OsuConfigManager LocalConfig; protected OsuConfigManager LocalConfig;
protected BeatmapManager BeatmapManager; protected BeatmapManager BeatmapManager;
@ -268,13 +271,13 @@ namespace osu.Game
private readonly List<ICanAcceptFiles> fileImporters = new List<ICanAcceptFiles>(); private readonly List<ICanAcceptFiles> fileImporters = new List<ICanAcceptFiles>();
public void Import(params string[] paths) public async Task Import(params string[] paths)
{ {
var extension = Path.GetExtension(paths.First())?.ToLowerInvariant(); var extension = Path.GetExtension(paths.First())?.ToLowerInvariant();
foreach (var importer in fileImporters) foreach (var importer in fileImporters)
if (importer.HandledExtensions.Contains(extension)) if (importer.HandledExtensions.Contains(extension))
importer.Import(paths); await importer.Import(paths);
} }
public string[] HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions).ToArray(); public string[] HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions).ToArray();

View File

@ -109,7 +109,7 @@ namespace osu.Game.Overlays
break; break;
case APIState.Online: case APIState.Online:
State = Visibility.Hidden; Hide();
break; break;
} }
} }

View File

@ -50,7 +50,7 @@ namespace osu.Game.Overlays.BeatmapSet
private void updateDisplay() private void updateDisplay()
{ {
bpm.Value = BeatmapSet?.OnlineInfo.BPM.ToString(@"0.##") ?? "-"; bpm.Value = BeatmapSet?.OnlineInfo?.BPM.ToString(@"0.##") ?? "-";
if (beatmap == null) if (beatmap == null)
{ {

View File

@ -16,10 +16,11 @@ namespace osu.Game.Overlays.BeatmapSet
{ {
public class Details : FillFlowContainer public class Details : FillFlowContainer
{ {
protected readonly UserRatings Ratings;
private readonly PreviewButton preview; private readonly PreviewButton preview;
private readonly BasicStats basic; private readonly BasicStats basic;
private readonly AdvancedStats advanced; private readonly AdvancedStats advanced;
private readonly UserRatings ratings;
private BeatmapSetInfo beatmapSet; private BeatmapSetInfo beatmapSet;
@ -33,6 +34,7 @@ namespace osu.Game.Overlays.BeatmapSet
beatmapSet = value; beatmapSet = value;
basic.BeatmapSet = preview.BeatmapSet = BeatmapSet; basic.BeatmapSet = preview.BeatmapSet = BeatmapSet;
updateDisplay();
} }
} }
@ -46,13 +48,12 @@ namespace osu.Game.Overlays.BeatmapSet
if (value == beatmap) return; if (value == beatmap) return;
basic.Beatmap = advanced.Beatmap = beatmap = value; basic.Beatmap = advanced.Beatmap = beatmap = value;
updateDisplay();
} }
} }
private void updateDisplay() private void updateDisplay()
{ {
ratings.Metrics = Beatmap?.Metrics; Ratings.Metrics = BeatmapSet?.Metrics;
} }
public Details() public Details()
@ -87,7 +88,7 @@ namespace osu.Game.Overlays.BeatmapSet
}, },
new DetailBox new DetailBox
{ {
Child = ratings = new UserRatings Child = Ratings = new UserRatings
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = 95, Height = 95,

View File

@ -14,11 +14,12 @@ namespace osu.Game.Overlays.BeatmapSet
{ {
public class SuccessRate : Container public class SuccessRate : Container
{ {
protected readonly FailRetryGraph Graph;
private readonly FillFlowContainer header; private readonly FillFlowContainer header;
private readonly OsuSpriteText successRateLabel, successPercent, graphLabel; private readonly OsuSpriteText successRateLabel, successPercent, graphLabel;
private readonly Bar successRate; private readonly Bar successRate;
private readonly Container percentContainer; private readonly Container percentContainer;
private readonly FailRetryGraph graph;
private BeatmapInfo beatmap; private BeatmapInfo beatmap;
@ -37,15 +38,15 @@ namespace osu.Game.Overlays.BeatmapSet
private void updateDisplay() private void updateDisplay()
{ {
int passCount = beatmap?.OnlineInfo.PassCount ?? 0; int passCount = beatmap?.OnlineInfo?.PassCount ?? 0;
int playCount = beatmap?.OnlineInfo.PlayCount ?? 0; int playCount = beatmap?.OnlineInfo?.PlayCount ?? 0;
var rate = playCount != 0 ? (float)passCount / playCount : 0; var rate = playCount != 0 ? (float)passCount / playCount : 0;
successPercent.Text = rate.ToString("P0"); successPercent.Text = rate.ToString("P0");
successRate.Length = rate; successRate.Length = rate;
percentContainer.ResizeWidthTo(successRate.Length, 250, Easing.InOutCubic); percentContainer.ResizeWidthTo(successRate.Length, 250, Easing.InOutCubic);
graph.Metrics = beatmap?.Metrics; Graph.Metrics = beatmap?.Metrics;
} }
public SuccessRate() public SuccessRate()
@ -94,7 +95,7 @@ namespace osu.Game.Overlays.BeatmapSet
}, },
}, },
}, },
graph = new FailRetryGraph Graph = new FailRetryGraph
{ {
Anchor = Anchor.BottomLeft, Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft, Origin = Anchor.BottomLeft,
@ -117,7 +118,7 @@ namespace osu.Game.Overlays.BeatmapSet
{ {
base.UpdateAfterChildren(); base.UpdateAfterChildren();
graph.Padding = new MarginPadding { Top = header.DrawHeight }; Graph.Padding = new MarginPadding { Top = header.DrawHeight };
} }
} }
} }

View File

@ -30,7 +30,7 @@ namespace osu.Game.Overlays
private RulesetStore rulesets; private RulesetStore rulesets;
private readonly ScrollContainer scroll; private readonly OsuScrollContainer scroll;
private readonly Bindable<BeatmapSetInfo> beatmapSet = new Bindable<BeatmapSetInfo>(); private readonly Bindable<BeatmapSetInfo> beatmapSet = new Bindable<BeatmapSetInfo>();
@ -49,7 +49,7 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(0.2f) Colour = OsuColour.Gray(0.2f)
}, },
scroll = new ScrollContainer scroll = new OsuScrollContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false, ScrollbarVisible = false,
@ -92,7 +92,7 @@ namespace osu.Game.Overlays
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
{ {
State = Visibility.Hidden; Hide();
return true; return true;
} }

View File

@ -51,7 +51,7 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = colour.PurpleDarkAlternative, Colour = colour.PurpleDarkAlternative,
}, },
new ScrollContainer new OsuScrollContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false, ScrollbarVisible = false,
@ -92,7 +92,7 @@ namespace osu.Game.Overlays
public void ShowListing() public void ShowListing()
{ {
Current.Value = null; Current.Value = null;
State = Visibility.Visible; Show();
} }
/// <summary> /// <summary>
@ -106,7 +106,7 @@ namespace osu.Game.Overlays
if (build == null) throw new ArgumentNullException(nameof(build)); if (build == null) throw new ArgumentNullException(nameof(build));
Current.Value = build; Current.Value = build;
State = Visibility.Visible; Show();
} }
public void ShowBuild([NotNull] string updateStream, [NotNull] string version) public void ShowBuild([NotNull] string updateStream, [NotNull] string version)
@ -123,7 +123,7 @@ namespace osu.Game.Overlays
ShowBuild(build); ShowBuild(build);
}); });
State = Visibility.Visible; Show();
} }
public override bool OnPressed(GlobalAction action) public override bool OnPressed(GlobalAction action)
@ -133,7 +133,7 @@ namespace osu.Game.Overlays
case GlobalAction.Back: case GlobalAction.Back:
if (Current.Value == null) if (Current.Value == null)
{ {
State = Visibility.Hidden; Hide();
} }
else else
{ {

View File

@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Chat
{ {
public readonly Channel Channel; public readonly Channel Channel;
protected readonly ChatLineContainer ChatLineFlow; protected readonly ChatLineContainer ChatLineFlow;
private readonly ScrollContainer scroll; private readonly OsuScrollContainer scroll;
public DrawableChannel(Channel channel) public DrawableChannel(Channel channel)
{ {

View File

@ -56,7 +56,7 @@ namespace osu.Game.Overlays
private readonly Container channelSelectionContainer; private readonly Container channelSelectionContainer;
private readonly ChannelSelectionOverlay channelSelectionOverlay; private readonly ChannelSelectionOverlay channelSelectionOverlay;
public override bool Contains(Vector2 screenSpacePos) => chatContainer.ReceivePositionalInputAt(screenSpacePos) || channelSelectionOverlay.State == Visibility.Visible && channelSelectionOverlay.ReceivePositionalInputAt(screenSpacePos); public override bool Contains(Vector2 screenSpacePos) => chatContainer.ReceivePositionalInputAt(screenSpacePos) || (channelSelectionOverlay.State.Value == Visibility.Visible && channelSelectionOverlay.ReceivePositionalInputAt(screenSpacePos));
public ChatOverlay() public ChatOverlay()
{ {
@ -130,7 +130,7 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Height = 1, Height = 1,
PlaceholderText = "type your message", PlaceholderText = "type your message",
Exit = () => State = Visibility.Hidden, Exit = Hide,
OnCommit = postMessage, OnCommit = postMessage,
ReleaseFocusOnCommit = false, ReleaseFocusOnCommit = false,
HoldFocus = true, HoldFocus = true,
@ -163,19 +163,19 @@ namespace osu.Game.Overlays
}; };
channelTabControl.Current.ValueChanged += current => channelManager.CurrentChannel.Value = current.NewValue; channelTabControl.Current.ValueChanged += current => channelManager.CurrentChannel.Value = current.NewValue;
channelTabControl.ChannelSelectorActive.ValueChanged += active => channelSelectionOverlay.State = active.NewValue ? Visibility.Visible : Visibility.Hidden; channelTabControl.ChannelSelectorActive.ValueChanged += active => channelSelectionOverlay.State.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden;
channelSelectionOverlay.StateChanged += state => channelSelectionOverlay.State.ValueChanged += state =>
{ {
if (state == Visibility.Hidden && channelManager.CurrentChannel.Value == null) if (state.NewValue == Visibility.Hidden && channelManager.CurrentChannel.Value == null)
{ {
channelSelectionOverlay.State = Visibility.Visible; channelSelectionOverlay.Show();
State = Visibility.Hidden; Hide();
return; return;
} }
channelTabControl.ChannelSelectorActive.Value = state == Visibility.Visible; channelTabControl.ChannelSelectorActive.Value = state.NewValue == Visibility.Visible;
if (state == Visibility.Visible) if (state.NewValue == Visibility.Visible)
{ {
textbox.HoldFocus = false; textbox.HoldFocus = false;
if (1f - ChatHeight.Value < channel_selection_min_height) if (1f - ChatHeight.Value < channel_selection_min_height)
@ -195,7 +195,7 @@ namespace osu.Game.Overlays
{ {
textbox.Current.Disabled = true; textbox.Current.Disabled = true;
currentChannelContainer.Clear(false); currentChannelContainer.Clear(false);
channelSelectionOverlay.State = Visibility.Visible; channelSelectionOverlay.Show();
return; return;
} }
@ -253,7 +253,7 @@ namespace osu.Game.Overlays
double targetChatHeight = startDragChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y; double targetChatHeight = startDragChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y;
// If the channel selection screen is shown, mind its minimum height // If the channel selection screen is shown, mind its minimum height
if (channelSelectionOverlay.State == Visibility.Visible && targetChatHeight > 1f - channel_selection_min_height) if (channelSelectionOverlay.State.Value == Visibility.Visible && targetChatHeight > 1f - channel_selection_min_height)
targetChatHeight = 1f - channel_selection_min_height; targetChatHeight = 1f - channel_selection_min_height;
ChatHeight.Value = targetChatHeight; ChatHeight.Value = targetChatHeight;
@ -325,7 +325,7 @@ namespace osu.Game.Overlays
this.MoveToY(Height, transition_length, Easing.InSine); this.MoveToY(Height, transition_length, Easing.InSine);
this.FadeOut(transition_length, Easing.InSine); this.FadeOut(transition_length, Easing.InSine);
channelSelectionOverlay.State = Visibility.Hidden; channelSelectionOverlay.Hide();
textbox.HoldFocus = false; textbox.HoldFocus = false;
base.PopOut(); base.PopOut();

View File

@ -37,8 +37,8 @@ namespace osu.Game.Overlays
dialogContainer.Add(currentDialog); dialogContainer.Add(currentDialog);
currentDialog.Show(); currentDialog.Show();
currentDialog.StateChanged += state => onDialogOnStateChanged(dialog, state); currentDialog.State.ValueChanged += state => onDialogOnStateChanged(dialog, state.NewValue);
State = Visibility.Visible; Show();
} }
protected override bool PlaySamplesOnStateChange => false; protected override bool PlaySamplesOnStateChange => false;
@ -53,7 +53,7 @@ namespace osu.Game.Overlays
dialog.Delay(PopupDialog.EXIT_DURATION).Expire(); dialog.Delay(PopupDialog.EXIT_DURATION).Expire();
if (dialog == currentDialog) if (dialog == currentDialog)
State = Visibility.Hidden; Hide();
} }
protected override void PopIn() protected override void PopIn()
@ -66,7 +66,7 @@ namespace osu.Game.Overlays
{ {
base.PopOut(); base.PopOut();
if (currentDialog?.State == Visibility.Visible) if (currentDialog?.State.Value == Visibility.Visible)
{ {
currentDialog.Hide(); currentDialog.Hide();
return; return;

View File

@ -252,7 +252,7 @@ namespace osu.Game.Overlays
if (!IsLoaded) if (!IsLoaded)
return; return;
if (State == Visibility.Hidden) if (State.Value == Visibility.Hidden)
return; return;
if (API == null) if (API == null)

View File

@ -4,6 +4,7 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
@ -40,6 +41,19 @@ namespace osu.Game.Overlays
}; };
} }
public override void Show()
{
if (State.Value == Visibility.Visible)
{
// re-trigger the state changed so we can potentially surface to front
State.TriggerChange();
}
else
{
base.Show();
}
}
protected override void PopIn() protected override void PopIn()
{ {
base.PopIn(); base.PopIn();

View File

@ -413,12 +413,12 @@ namespace osu.Game.Overlays.Mods
{ {
if (selectedMod != null) if (selectedMod != null)
{ {
if (State == Visibility.Visible) sampleOn?.Play(); if (State.Value == Visibility.Visible) sampleOn?.Play();
DeselectTypes(selectedMod.IncompatibleMods, true); DeselectTypes(selectedMod.IncompatibleMods, true);
} }
else else
{ {
if (State == Visibility.Visible) sampleOff?.Play(); if (State.Value == Visibility.Visible) sampleOff?.Play();
} }
refreshSelectedMods(); refreshSelectedMods();

View File

@ -71,7 +71,7 @@ namespace osu.Game.Overlays.Music
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
ExitRequested = () => State = Visibility.Hidden, ExitRequested = Hide,
FilterChanged = search => list.Filter(search), FilterChanged = search => list.Filter(search),
Padding = new MarginPadding(10), Padding = new MarginPadding(10),
}, },

View File

@ -200,7 +200,7 @@ namespace osu.Game.Overlays
beatmaps.ItemAdded += handleBeatmapAdded; beatmaps.ItemAdded += handleBeatmapAdded;
beatmaps.ItemRemoved += handleBeatmapRemoved; beatmaps.ItemRemoved += handleBeatmapRemoved;
playlist.StateChanged += s => playlistButton.FadeColour(s == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint); playlist.State.ValueChanged += s => playlistButton.FadeColour(s.NewValue == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint);
} }
private ScheduledDelegate seekDelegate; private ScheduledDelegate seekDelegate;
@ -449,7 +449,7 @@ namespace osu.Game.Overlays
// This is here mostly as a performance fix. // This is here mostly as a performance fix.
// If the playlist is not hidden it will update children even when the music controller is hidden (due to AlwaysPresent). // If the playlist is not hidden it will update children even when the music controller is hidden (due to AlwaysPresent).
playlist.State = Visibility.Hidden; playlist.Hide();
this.FadeOut(transition_length, Easing.OutQuint); this.FadeOut(transition_length, Easing.OutQuint);
dragContainer.ScaleTo(0.9f, transition_length, Easing.OutQuint); dragContainer.ScaleTo(0.9f, transition_length, Easing.OutQuint);
@ -464,12 +464,26 @@ namespace osu.Game.Overlays
private class MusicIconButton : IconButton private class MusicIconButton : IconButton
{ {
public MusicIconButton()
{
AutoSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
HoverColour = colours.YellowDark.Opacity(0.6f); HoverColour = colours.YellowDark.Opacity(0.6f);
FlashColour = colours.Yellow; FlashColour = colours.Yellow;
} }
protected override void LoadComplete()
{
base.LoadComplete();
// works with AutoSizeAxes above to make buttons autosize with the scale animation.
Content.AutoSizeAxes = Axes.None;
Content.Size = new Vector2(DEFAULT_BUTTON_SIZE);
}
} }
private class Background : BufferedContainer private class Background : BufferedContainer

View File

@ -81,13 +81,13 @@ namespace osu.Game.Overlays
private void updateProcessingMode() private void updateProcessingMode()
{ {
bool enabled = OverlayActivationMode.Value == OverlayActivation.All || State == Visibility.Visible; bool enabled = OverlayActivationMode.Value == OverlayActivation.All || State.Value == Visibility.Visible;
notificationsEnabler?.Cancel(); notificationsEnabler?.Cancel();
if (enabled) if (enabled)
// we want a slight delay before toggling notifications on to avoid the user becoming overwhelmed. // we want a slight delay before toggling notifications on to avoid the user becoming overwhelmed.
notificationsEnabler = Scheduler.AddDelayed(() => processingPosts = true, State == Visibility.Visible ? 0 : 1000); notificationsEnabler = Scheduler.AddDelayed(() => processingPosts = true, State.Value == Visibility.Visible ? 0 : 1000);
else else
processingPosts = false; processingPosts = false;
} }
@ -96,7 +96,7 @@ namespace osu.Game.Overlays
{ {
base.LoadComplete(); base.LoadComplete();
StateChanged += _ => updateProcessingMode(); State.ValueChanged += _ => updateProcessingMode();
OverlayActivationMode.BindValueChanged(_ => updateProcessingMode(), true); OverlayActivationMode.BindValueChanged(_ => updateProcessingMode(), true);
} }
@ -128,7 +128,7 @@ namespace osu.Game.Overlays
section?.Add(notification, notification.DisplayOnTop ? -runningDepth : runningDepth); section?.Add(notification, notification.DisplayOnTop ? -runningDepth : runningDepth);
if (notification.IsImportant) if (notification.IsImportant)
State = Visibility.Visible; Show();
updateCounts(); updateCounts();
}); });

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