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

Merge remote-tracking branch 'upstream/master' into settings-footer-show-changelog-current-build

This commit is contained in:
Dean Herbert 2019-06-12 18:28:43 +09:00
commit b2b3df3885
91 changed files with 646 additions and 428 deletions

View File

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

View File

@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
{
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
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,
Mods = mods,
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

@ -379,8 +379,8 @@ namespace osu.Game.Rulesets.Catch.UI
X = (float)MathHelper.Clamp(X + direction * Clock.ElapsedFrameTime * speed, 0, 1);
// Correct overshooting.
if (hyperDashDirection > 0 && hyperDashTargetPosition < X ||
hyperDashDirection < 0 && hyperDashTargetPosition > X)
if ((hyperDashDirection > 0 && hyperDashTargetPosition < X) ||
(hyperDashDirection < 0 && hyperDashTargetPosition > X))
{
X = hyperDashTargetPosition;
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)
{
if (beatmap.HitObjects.Count == 0)
return new ManiaDifficultyAttributes { Mods = mods };
return new ManiaDifficultyAttributes { Mods = mods, Skills = skills };
return new ManiaDifficultyAttributes
{
@ -38,6 +38,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
Mods = mods,
// 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,
Skills = skills
};
}

View File

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

View File

@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
break;
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;

View File

@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
{
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 speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
@ -50,7 +50,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
SpeedStrain = speedRating,
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
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.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
@ -11,7 +14,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Mods
{
internal class OsuModGrow : Mod, IApplicableToDrawableHitObjects
internal class OsuModGrow : Mod, IReadFromConfig, IApplicableToDrawableHitObjects
{
public override string Name => "Grow";
@ -25,9 +28,16 @@ namespace osu.Game.Rulesets.Osu.Mods
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)
{
foreach (var drawable in drawables)
foreach (var drawable in drawables.Skip(increaseFirstObjectVisibility.Value ? 1 : 0))
{
switch (drawable)
{

View File

@ -37,11 +37,11 @@ namespace osu.Game.Rulesets.Osu.Mods
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;
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)

View File

@ -6,7 +6,7 @@ using System.Diagnostics;
using System.Runtime.InteropServices;
using osu.Framework.Allocation;
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.Primitives;
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
// 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].WasUpdated = true;
}
}
@ -149,7 +148,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
public Vector2 Position;
public float Time;
public long InvalidationID;
public bool WasUpdated;
}
private class TrailDrawNode : DrawNode
@ -164,16 +162,13 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private readonly TrailPart[] parts = new TrailPart[max_sprites];
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)
: base(source)
{
for (int i = 0; i < max_sprites; i++)
{
parts[i].InvalidationID = 0;
parts[i].WasUpdated = false;
}
}
public override void ApplyState()
@ -194,56 +189,29 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
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);
shader.Bind();
shader.GetUniform<float>("g_FadeClock").UpdateValue(ref time);
texture.TextureGL.Bind();
vertexBuffer.Draw();
for (int i = 0; i < parts.Length; ++i)
{
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();
}
@ -252,7 +220,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
{
base.Dispose(isDisposing);
vertexBuffer.Dispose();
vertexBatch.Dispose();
}
}

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.UI
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";

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
{
if (beatmap.HitObjects.Count == 0)
return new TaikoDifficultyAttributes { Mods = mods };
return new TaikoDifficultyAttributes { Mods = mods, Skills = skills };
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
GreatHitWindow = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / clockRate,
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.Game.IPC;
using osu.Framework.Allocation;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.IO;
using osu.Game.Tests.Resources;
using SharpCompress.Archives.Zip;
@ -21,14 +23,14 @@ namespace osu.Game.Tests.Beatmaps.IO
public class ImportBeatmapTest
{
[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.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWhenClosed"))
{
try
{
LoadOszIntoOsu(loadOsu(host));
await LoadOszIntoOsu(loadOsu(host));
}
finally
{
@ -38,7 +40,7 @@ namespace osu.Game.Tests.Beatmaps.IO
}
[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.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenDelete"))
@ -47,7 +49,7 @@ namespace osu.Game.Tests.Beatmaps.IO
{
var osu = loadOsu(host);
var imported = LoadOszIntoOsu(osu);
var imported = await LoadOszIntoOsu(osu);
deleteBeatmapSet(imported, osu);
}
@ -59,7 +61,7 @@ namespace osu.Game.Tests.Beatmaps.IO
}
[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.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenImport"))
@ -68,17 +70,15 @@ namespace osu.Game.Tests.Beatmaps.IO
{
var osu = loadOsu(host);
var imported = LoadOszIntoOsu(osu);
var importedSecondTime = LoadOszIntoOsu(osu);
var imported = await LoadOszIntoOsu(osu);
var importedSecondTime = await LoadOszIntoOsu(osu);
// 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.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
var manager = osu.Dependencies.Get<BeatmapManager>();
Assert.AreEqual(1, manager.GetAllUsableBeatmapSets().Count);
Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count);
checkBeatmapSetCount(osu, 1);
checkSingleReferencedFileCount(osu, 18);
}
finally
{
@ -88,30 +88,41 @@ namespace osu.Game.Tests.Beatmaps.IO
}
[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.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestRollbackOnFailure"))
{
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 manager = osu.Dependencies.Get<BeatmapManager>();
int fireCount = 0;
// ReSharper disable once AccessToModifiedClosure
manager.ItemAdded += (_, __) => fireCount++;
manager.ItemRemoved += _ => fireCount++;
manager.ItemAdded += (_, __) => Interlocked.Increment(ref itemAddRemoveFireCount);
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";
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();
@ -127,19 +138,24 @@ namespace osu.Game.Tests.Beatmaps.IO
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.
manager.Import(breakTemp);
try
{
await manager.Import(breakTemp);
}
catch
{
}
// 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);
Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count);
Assert.AreEqual(12, manager.QueryBeatmaps(_ => true).ToList().Count);
checkBeatmapSetCount(osu, 1);
checkBeatmapCount(osu, 12);
checkSingleReferencedFileCount(osu, 18);
Assert.AreEqual(1, loggedExceptionCount);
}
finally
{
@ -149,7 +165,7 @@ namespace osu.Game.Tests.Beatmaps.IO
}
[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.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenImportDifferentHash"))
@ -159,19 +175,18 @@ namespace osu.Game.Tests.Beatmaps.IO
var osu = loadOsu(host);
var manager = osu.Dependencies.Get<BeatmapManager>();
var imported = LoadOszIntoOsu(osu);
var imported = await LoadOszIntoOsu(osu);
imported.Hash += "-changed";
manager.Update(imported);
var importedSecondTime = LoadOszIntoOsu(osu);
var importedSecondTime = await LoadOszIntoOsu(osu);
Assert.IsTrue(imported.ID != importedSecondTime.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.
Assert.AreEqual(1, manager.GetAllUsableBeatmapSets().Count);
Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count);
checkBeatmapSetCount(osu, 1);
}
finally
{
@ -181,7 +196,7 @@ namespace osu.Game.Tests.Beatmaps.IO
}
[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.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportThenDeleteThenImport"))
@ -190,11 +205,11 @@ namespace osu.Game.Tests.Beatmaps.IO
{
var osu = loadOsu(host);
var imported = LoadOszIntoOsu(osu);
var imported = await LoadOszIntoOsu(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.
Assert.IsTrue(imported.ID == importedSecondTime.ID);
@ -209,7 +224,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[TestCase(true)]
[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.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"TestImportThenDeleteThenImport-{set}"))
@ -218,7 +233,7 @@ namespace osu.Game.Tests.Beatmaps.IO
{
var osu = loadOsu(host);
var imported = LoadOszIntoOsu(osu);
var imported = await LoadOszIntoOsu(osu);
if (set)
imported.OnlineBeatmapSetID = 1234;
@ -229,7 +244,7 @@ namespace osu.Game.Tests.Beatmaps.IO
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)
Assert.IsTrue(imported.ID != importedSecondTime.ID);
@ -243,7 +258,7 @@ namespace osu.Game.Tests.Beatmaps.IO
}
[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.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWithDuplicateBeatmapID"))
@ -284,7 +299,7 @@ namespace osu.Game.Tests.Beatmaps.IO
var manager = osu.Dependencies.Get<BeatmapManager>();
var imported = manager.Import(toImport);
var imported = await manager.Import(toImport);
Assert.NotNull(imported);
Assert.AreEqual(null, imported.Beatmaps[0].OnlineBeatmapID);
@ -330,7 +345,7 @@ namespace osu.Game.Tests.Beatmaps.IO
}
[Test]
public void TestImportWhenFileOpen()
public async Task TestImportWhenFileOpen()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWhenFileOpen"))
{
@ -339,7 +354,7 @@ namespace osu.Game.Tests.Beatmaps.IO
var osu = loadOsu(host);
var temp = TestResources.GetTestBeatmapForImport();
using (File.OpenRead(temp))
osu.Dependencies.Get<BeatmapManager>().Import(temp);
await osu.Dependencies.Get<BeatmapManager>().Import(temp);
ensureLoaded(osu);
File.Delete(temp);
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 manager = osu.Dependencies.Get<BeatmapManager>();
manager.Import(temp);
await manager.Import(temp);
var imported = manager.GetAllUsableBeatmapSets();
@ -373,11 +388,32 @@ namespace osu.Game.Tests.Beatmaps.IO
var manager = osu.Dependencies.Get<BeatmapManager>();
manager.Delete(imported);
Assert.IsTrue(manager.GetAllUsableBeatmapSets().Count == 0);
Assert.AreEqual(1, manager.QueryBeatmapSets(_ => true).ToList().Count);
checkBeatmapSetCount(osu, 0);
checkBeatmapSetCount(osu, 1, true);
checkSingleReferencedFileCount(osu, 0);
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)
{
var osu = new OsuGameBase();

View File

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

View File

@ -28,7 +28,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override void AddCheckSteps()
{
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

View File

@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("Show overlay", () => pauseOverlay.Show());
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());
}
@ -237,7 +237,7 @@ namespace osu.Game.Tests.Visual.Gameplay
});
AddAssert("Action was triggered", () => triggered);
AddAssert("Overlay is closed", () => pauseOverlay.State == Visibility.Hidden);
AddAssert("Overlay is closed", () => pauseOverlay.State.Value == Visibility.Hidden);
}
/// <summary>
@ -272,7 +272,7 @@ namespace osu.Game.Tests.Visual.Gameplay
return triggered;
});
AddAssert("Overlay is closed", () => pauseOverlay.State == Visibility.Hidden);
AddAssert("Overlay is closed", () => pauseOverlay.State.Value == Visibility.Hidden);
}
private void press(Key key)

View File

@ -189,7 +189,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("pause overlay " + (isShown ? "shown" : "hidden"), () => Player.PauseOverlayVisible == isShown);
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;
@ -203,9 +203,9 @@ namespace osu.Game.Tests.Visual.Gameplay
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)
{

View File

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

View File

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

View File

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

View File

@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Online
api.Logout();
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());
}
}

View File

@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Online
Children = new Drawable[]
{
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();
int fireCount = 0;
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

View File

@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Settings
{
settings = new SettingsOverlay
{
State = Visibility.Visible
State = { Value = Visibility.Visible }
};
Add(dialogOverlay = new DialogOverlay
{

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.
using System.Collections.Generic;
@ -7,7 +7,6 @@ using JetBrains.Annotations;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets;
@ -48,7 +47,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("show", () =>
{
infoWedge.State = Visibility.Visible;
infoWedge.Show();
infoWedge.Beatmap = Beatmap.Value;
});
@ -57,11 +56,11 @@ namespace osu.Game.Tests.Visual.SongSelect
AddWaitStep("wait for select", 3);
AddStep("hide", () => { infoWedge.State = Visibility.Hidden; });
AddStep("hide", () => { infoWedge.Hide(); });
AddWaitStep("wait for hide", 3);
AddStep("show", () => { infoWedge.State = Visibility.Visible; });
AddStep("show", () => { infoWedge.Show(); });
foreach (var rulesetInfo in rulesets.AvailableRulesets)
{

View File

@ -138,7 +138,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
createSongSelect();
changeRuleset(2);
importForRuleset(0);
addRulesetImportStep(0);
AddUntilStep("no selection", () => songSelect.Carousel.SelectedBeatmap == null);
}
@ -147,8 +147,8 @@ namespace osu.Game.Tests.Visual.SongSelect
{
createSongSelect();
changeRuleset(2);
importForRuleset(2);
importForRuleset(1);
addRulesetImportStep(2);
addRulesetImportStep(1);
AddUntilStep("has selection", () => songSelect.Carousel.SelectedBeatmap.RulesetID == 2);
changeRuleset(1);
@ -210,7 +210,52 @@ namespace osu.Game.Tests.Visual.SongSelect
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 int getImportId() => ++importId;
@ -232,7 +277,7 @@ namespace osu.Game.Tests.Visual.SongSelect
var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray();
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.
/// </summary>
/// <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>
/// 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 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;
@ -218,7 +218,7 @@ namespace osu.Game.Tests.Visual.UserInterface
},
Cursor = new TestCursorContainer
{
State = providesUserCursor ? Visibility.Hidden : Visibility.Visible,
State = { Value = providesUserCursor ? Visibility.Hidden : Visibility.Visible },
}
};
}

View File

@ -23,8 +23,8 @@ namespace osu.Game.Tests.Visual.UserInterface
};
Add(mc);
AddToggleStep(@"toggle visibility", state => mc.State = state ? Visibility.Visible : Visibility.Hidden);
AddStep(@"show", () => mc.State = Visibility.Visible);
AddToggleStep(@"toggle visibility", state => mc.State.Value = state ? Visibility.Visible : Visibility.Hidden);
AddStep(@"show", () => mc.Show());
AddToggleStep(@"toggle beatmap lock", state => Beatmap.Disabled = state);
}
}

View File

@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.UserInterface
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);
manager.UnreadCount.ValueChanged += count => { displayedCount.Text = $"displayed count: {count.NewValue}"; };

View File

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

View File

@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.UserInterface
this.api = api;
this.rulesets = rulesets;
testBeatmap = ImportBeatmapTest.LoadOszIntoOsu(osu);
testBeatmap = ImportBeatmapTest.LoadOszIntoOsu(osu).Result;
}
[Test]

View File

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

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Audio;
@ -14,6 +15,7 @@ using osu.Framework.Extensions;
using osu.Framework.Graphics.Textures;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.Beatmaps.Formats;
using osu.Game.Database;
using osu.Game.IO.Archives;
@ -72,6 +74,8 @@ namespace osu.Game.Beatmaps
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,
WorkingBeatmap defaultBeatmap = null)
: base(storage, contextFactory, new BeatmapStore(contextFactory), host)
@ -86,9 +90,11 @@ namespace osu.Game.Beatmaps
beatmaps = (BeatmapStore)ModelStore;
beatmaps.BeatmapHidden += b => BeatmapHidden?.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)
beatmapSet.Beatmaps = createBeatmapDifficulties(archive);
@ -104,8 +110,7 @@ namespace osu.Game.Beatmaps
validateOnlineIds(beatmapSet);
foreach (BeatmapInfo b in beatmapSet.Beatmaps)
fetchAndPopulateOnlineValues(b);
return updateQueue.UpdateAsync(beatmapSet, cancellationToken);
}
protected override void PreImport(BeatmapSetInfo beatmapSet)
@ -122,7 +127,7 @@ namespace osu.Game.Beatmaps
{
Delete(existingOnlineId);
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 =>
{
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.
Import(downloadNotification, filename);
await Import(downloadNotification, filename);
currentDownloads.Remove(request);
}, TaskCreationOptions.LongRunning);
};
@ -381,47 +386,6 @@ namespace osu.Game.Beatmaps
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>
/// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation.
/// </summary>
@ -455,5 +419,55 @@ namespace osu.Game.Beatmaps
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

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore;
@ -13,6 +14,7 @@ using osu.Framework.Extensions;
using osu.Framework.IO.File;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.IO;
using osu.Game.IO.Archives;
using osu.Game.IPC;
@ -29,7 +31,7 @@ namespace osu.Game.Database
/// </summary>
/// <typeparam name="TModel">The model 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 TFileModel : INamedFileInfo, new()
{
@ -130,56 +132,50 @@ namespace osu.Game.Database
/// This will post notifications tracking progress.
/// </summary>
/// <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 };
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.Text = "Import is initialising...";
var term = $"{typeof(TModel).Name.Replace("Info", "").ToLower()}";
List<TModel> imported = new List<TModel>();
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)
// user requested abort
return;
notification.CancellationToken.ThrowIfCancellationRequested();
try
{
var text = "Importing ";
var model = await Import(path, notification.CancellationToken);
if (path.Length > 1)
text += $"{++current} of {paths.Length} {term}s..";
else
text += $"{term}..";
lock (imported)
{
imported.Add(model);
current++;
// only show the filename if it isn't a temporary one (as those look ugly).
if (!path.Contains(Path.GetTempPath()))
text += $"\n{Path.GetFileName(path)}";
notification.Text = text;
imported.Add(Import(path));
notification.Progress = (float)current / paths.Length;
notification.Text = $"Imported {current} of {paths.Length} {humanisedModelName}s";
notification.Progress = (float)current / paths.Length;
}
}
catch (TaskCanceledException)
{
throw;
}
catch (Exception e)
{
e = e.InnerException ?? e;
Logger.Error(e, $@"Could not import ({Path.GetFileName(path)})");
Logger.Error(e, $@"Could not import ({Path.GetFileName(path)})", LoggingTarget.Database);
}
}
}));
if (imported.Count == 0)
{
@ -190,7 +186,7 @@ namespace osu.Game.Database
{
notification.CompletionText = imported.Count == 1
? $"Imported {imported.First()}!"
: $"Imported {current} {term}s!";
: $"Imported {current} {humanisedModelName}s!";
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.
/// </summary>
/// <param name="path">The archive location on disk.</param>
/// <param name="cancellationToken">An optional cancellation token.</param>
/// <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;
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.
// e.g. reconstructing/repairing database with items from default storage.
@ -228,7 +227,7 @@ namespace osu.Game.Database
}
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;
@ -243,23 +242,32 @@ namespace osu.Game.Database
/// Import an item from an <see cref="ArchiveReader"/>.
/// </summary>
/// <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
{
var model = CreateModel(archive);
model = CreateModel(archive);
if (model == null) return null;
model.Hash = computeHash(archive);
return Import(model, archive);
}
catch (TaskCanceledException)
{
throw;
}
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 Import(model, archive, cancellationToken);
}
/// <summary>
@ -269,6 +277,16 @@ namespace osu.Game.Database
/// </summary>
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>
/// Create a SHA-2 hash from the provided archive based on file content of all files matching <see cref="HashableFileTypes"/>.
/// </summary>
@ -288,13 +306,30 @@ namespace osu.Game.Database
/// </summary>
/// <param name="item">The model to be imported.</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();
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
{
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.
{
@ -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 (archive != null)
item.Files = createFileInfos(archive, Files);
Populate(item, archive);
var existing = CheckForExisting(item);
if (existing != null)
@ -314,15 +344,17 @@ namespace osu.Game.Database
if (CanUndelete(existing, item))
{
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));
// existing item will be used; rollback new import and exit early.
rollback();
flushEvents(true);
return existing;
}
else
{
Delete(existing);
ModelStore.PurgeDeletable(s => s.ID == existing.ID);
}
Delete(existing);
ModelStore.PurgeDeletable(s => s.ID == existing.ID);
}
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)
{
Logger.Error(e, $"Import of {item} failed and has been rolled back.", LoggingTarget.Database);
item = null;
}
finally
{
// we only want to flush events after we've confirmed the write context didn't have any errors.
flushEvents(item != null);
if (!(e is TaskCanceledException))
LogForModel(item, "Database import or population failed and has been rolled back.", e);
rollback();
flushEvents(false);
throw;
}
flushEvents(true);
return item;
}
}, cancellationToken, TaskCreationOptions.HideScheduler, IMPORT_SCHEDULER).Unwrap();
/// <summary>
/// Perform an update of the specified item.
@ -533,7 +565,7 @@ namespace osu.Game.Database
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
@ -552,9 +584,8 @@ namespace osu.Game.Database
/// </summary>
/// <param name="model">The model to populate.</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>
/// Perform any final actions before the import to database executes.
@ -582,6 +613,8 @@ namespace osu.Game.Database
private DbSet<TModel> queryModel() => ContextFactory.Get().Set<TModel>();
private string humanisedModelName => $"{typeof(TModel).Name.Replace("Info", "").ToLower()}";
/// <summary>
/// Creates an <see cref="ArchiveReader"/> from a valid storage path.
/// </summary>
@ -599,4 +632,18 @@ namespace osu.Game.Database
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.
// See the LICENCE file in the repository root for full licence text.
using System.Threading.Tasks;
namespace osu.Game.Database
{
/// <summary>
@ -12,7 +14,7 @@ namespace osu.Game.Database
/// Import the specified paths.
/// </summary>
/// <param name="paths">The files which should be imported.</param>
void Import(params string[] paths);
Task Import(params string[] paths);
/// <summary>
/// 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()
{
if (confirming || !AllowMultipleFires && fired) return;
if (confirming || (!AllowMultipleFires && fired)) return;
confirming = true;

View File

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

View File

@ -103,7 +103,7 @@ namespace osu.Game.Graphics.Containers
protected override void PopIn()
{
foreach (var w in wavesContainer.Children)
w.State = Visibility.Visible;
w.Show();
this.FadeIn(100, 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);
foreach (var w in wavesContainer.Children)
w.State = Visibility.Hidden;
w.Hide();
this.FadeOut(DISAPPEAR_DURATION, Easing.InQuint);
}

View File

@ -29,7 +29,7 @@ namespace osu.Game.Graphics.Cursor
{
AddRangeInternal(new Drawable[]
{
Cursor = new MenuCursor { State = Visibility.Hidden },
Cursor = new MenuCursor { State = { Value = Visibility.Hidden } },
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)
: base(value)
{

View File

@ -19,14 +19,14 @@ namespace osu.Game.Graphics.UserInterface
public class OsuAnimatedButton : OsuClickableContainer
{
/// <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>
protected Color4 FlashColour = Color4.White.Opacity(0.3f);
private Color4 hoverColour = Color4.White.Opacity(0.1f);
/// <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>
protected Color4 HoverColour
{

View File

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

View File

@ -34,7 +34,7 @@ namespace osu.Game.Graphics.UserInterface
RelativeSizeAxes = Axes.Both,
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.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Extensions;
using osu.Framework.IO.Stores;
using osu.Framework.Logging;
@ -27,6 +30,13 @@ namespace osu.Game.IO
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)
{
using (var usage = ContextFactory.GetForWrite())

View File

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

View File

@ -37,7 +37,7 @@ namespace osu.Game.Online.API
public Bindable<User> LocalUser { get; } = new Bindable<User>(createGuestUser());
protected bool HasLogin => authentication.Token.Value != null || !string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password);
protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password));
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();

View File

@ -69,7 +69,7 @@ namespace osu.Game.Online.Chat
if (displayText.Length == 0 || linkText.Length == 0) continue;
// 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);

View File

@ -132,12 +132,12 @@ namespace osu.Game
public void CloseAllOverlays(bool hideToolbarElements = true)
{
foreach (var overlay in overlays)
overlay.State = Visibility.Hidden;
overlay.Hide();
if (hideToolbarElements)
{
foreach (var overlay in toolbarElements)
overlay.State = Visibility.Hidden;
overlay.Hide();
}
}
@ -461,7 +461,7 @@ namespace osu.Game
loadComponentSingleFile(new DialogOverlay(), topMostOverlayContent.Add, true);
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());
@ -470,9 +470,9 @@ namespace osu.Game
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());
};
@ -484,9 +484,9 @@ namespace osu.Game
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());
};
@ -498,12 +498,12 @@ namespace osu.Game
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.
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());
};
@ -518,16 +518,16 @@ namespace osu.Game
{
float offset = 0;
if (settings.State == Visibility.Visible)
if (settings.State.Value == Visibility.Visible)
offset += ToolbarButton.WIDTH / 2;
if (notifications.State == Visibility.Visible)
if (notifications.State.Value == Visibility.Visible)
offset -= ToolbarButton.WIDTH / 2;
screenContainer.MoveToX(offset, SettingsPanel.TRANSITION_LENGTH, Easing.OutQuint);
}
settings.StateChanged += _ => updateScreenOffset();
notifications.StateChanged += _ => updateScreenOffset();
settings.State.ValueChanged += _ => updateScreenOffset();
notifications.State.ValueChanged += _ => updateScreenOffset();
}
public class GameIdleTracker : IdleTracker
@ -768,7 +768,7 @@ namespace osu.Game
if (newOsuScreen.HideOverlaysOnEnter)
CloseAllOverlays();
else
Toolbar.State = Visibility.Visible;
Toolbar.Show();
}
}

View File

@ -6,6 +6,7 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
@ -270,13 +271,13 @@ namespace osu.Game
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();
foreach (var importer in fileImporters)
if (importer.HandledExtensions.Contains(extension))
importer.Import(paths);
await importer.Import(paths);
}
public string[] HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions).ToArray();

View File

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

View File

@ -92,7 +92,7 @@ namespace osu.Game.Overlays
protected override bool OnClick(ClickEvent e)
{
State = Visibility.Hidden;
Hide();
return true;
}

View File

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

View File

@ -56,7 +56,7 @@ namespace osu.Game.Overlays
private readonly Container channelSelectionContainer;
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()
{
@ -130,7 +130,7 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both,
Height = 1,
PlaceholderText = "type your message",
Exit = () => State = Visibility.Hidden,
Exit = Hide,
OnCommit = postMessage,
ReleaseFocusOnCommit = false,
HoldFocus = true,
@ -163,19 +163,19 @@ namespace osu.Game.Overlays
};
channelTabControl.Current.ValueChanged += current => channelManager.CurrentChannel.Value = current.NewValue;
channelTabControl.ChannelSelectorActive.ValueChanged += active => channelSelectionOverlay.State = active.NewValue ? Visibility.Visible : Visibility.Hidden;
channelSelectionOverlay.StateChanged += state =>
channelTabControl.ChannelSelectorActive.ValueChanged += active => channelSelectionOverlay.State.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden;
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;
State = Visibility.Hidden;
channelSelectionOverlay.Show();
Hide();
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;
if (1f - ChatHeight.Value < channel_selection_min_height)
@ -195,7 +195,7 @@ namespace osu.Game.Overlays
{
textbox.Current.Disabled = true;
currentChannelContainer.Clear(false);
channelSelectionOverlay.State = Visibility.Visible;
channelSelectionOverlay.Show();
return;
}
@ -253,7 +253,7 @@ namespace osu.Game.Overlays
double targetChatHeight = startDragChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y;
// 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;
ChatHeight.Value = targetChatHeight;
@ -325,7 +325,7 @@ namespace osu.Game.Overlays
this.MoveToY(Height, transition_length, Easing.InSine);
this.FadeOut(transition_length, Easing.InSine);
channelSelectionOverlay.State = Visibility.Hidden;
channelSelectionOverlay.Hide();
textbox.HoldFocus = false;
base.PopOut();

View File

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

View File

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

View File

@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Game.Graphics;
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()
{
base.PopIn();

View File

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

View File

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

View File

@ -200,7 +200,7 @@ namespace osu.Game.Overlays
beatmaps.ItemAdded += handleBeatmapAdded;
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;
@ -449,7 +449,7 @@ namespace osu.Game.Overlays
// 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).
playlist.State = Visibility.Hidden;
playlist.Hide();
this.FadeOut(transition_length, Easing.OutQuint);
dragContainer.ScaleTo(0.9f, transition_length, Easing.OutQuint);

View File

@ -81,13 +81,13 @@ namespace osu.Game.Overlays
private void updateProcessingMode()
{
bool enabled = OverlayActivationMode.Value == OverlayActivation.All || State == Visibility.Visible;
bool enabled = OverlayActivationMode.Value == OverlayActivation.All || State.Value == Visibility.Visible;
notificationsEnabler?.Cancel();
if (enabled)
// 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
processingPosts = false;
}
@ -96,7 +96,7 @@ namespace osu.Game.Overlays
{
base.LoadComplete();
StateChanged += _ => updateProcessingMode();
State.ValueChanged += _ => updateProcessingMode();
OverlayActivationMode.BindValueChanged(_ => updateProcessingMode(), true);
}
@ -128,7 +128,7 @@ namespace osu.Game.Overlays
section?.Add(notification, notification.DisplayOnTop ? -runningDepth : runningDepth);
if (notification.IsImportant)
State = Visibility.Visible;
Show();
updateCounts();
});

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -36,6 +37,10 @@ namespace osu.Game.Overlays.Notifications
State = state;
}
private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource();
public CancellationToken CancellationToken => cancellationTokenSource.Token;
public virtual ProgressNotificationState State
{
get => state;
@ -62,6 +67,8 @@ namespace osu.Game.Overlays.Notifications
break;
case ProgressNotificationState.Cancelled:
cancellationTokenSource.Cancel();
Light.Colour = colourCancelled;
Light.Pulsate = false;
progressBar.Active = false;

View File

@ -17,7 +17,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
{
new SettingsCheckbox
{
LabelText = "Increase visibility of first object with \"Hidden\" mod",
LabelText = "Increase visibility of first object when visual impairment mods are enabled",
Bindable = config.GetBindable<bool>(OsuSetting.IncreaseFirstObjectVisibility)
},
};

View File

@ -97,7 +97,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
{
new LoadingAnimation
{
State = Visibility.Visible,
State = { Value = Visibility.Visible },
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
},

View File

@ -10,12 +10,14 @@ namespace osu.Game.Overlays.Settings
{
private OsuCheckbox checkbox;
private string labelText;
protected override Drawable CreateControl() => checkbox = new OsuCheckbox();
public override string LabelText
{
get => checkbox.LabelText;
set => checkbox.LabelText = value;
get => labelText;
set => checkbox.LabelText = labelText = value;
}
}
}

View File

@ -9,6 +9,7 @@ using osu.Game.Overlays.Settings.Sections;
using osuTK.Graphics;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
namespace osu.Game.Overlays
{
@ -37,23 +38,23 @@ namespace osu.Game.Overlays
{
}
public override bool AcceptsFocus => subPanels.All(s => s.State != Visibility.Visible);
public override bool AcceptsFocus => subPanels.All(s => s.State.Value != Visibility.Visible);
private T createSubPanel<T>(T subPanel)
where T : SettingsSubPanel
{
subPanel.Depth = 1;
subPanel.Anchor = Anchor.TopRight;
subPanel.StateChanged += subPanelStateChanged;
subPanel.State.ValueChanged += subPanelStateChanged;
subPanels.Add(subPanel);
return subPanel;
}
private void subPanelStateChanged(Visibility visibility)
private void subPanelStateChanged(ValueChangedEvent<Visibility> state)
{
switch (visibility)
switch (state.NewValue)
{
case Visibility.Visible:
Background.FadeTo(0.9f, 300, Easing.OutQuint);
@ -73,7 +74,7 @@ namespace osu.Game.Overlays
}
}
protected override float ExpandedPosition => subPanels.Any(s => s.State == Visibility.Visible) ? -WIDTH : base.ExpandedPosition;
protected override float ExpandedPosition => subPanels.Any(s => s.State.Value == Visibility.Visible) ? -WIDTH : base.ExpandedPosition;
[BackgroundDependencyLoader]
private void load()

View File

@ -84,10 +84,10 @@ namespace osu.Game.Overlays.Toolbar
}
};
StateChanged += visibility =>
State.ValueChanged += visibility =>
{
if (overlayActivationMode.Value == OverlayActivation.Disabled)
State = Visibility.Hidden;
Hide();
};
if (osuGame != null)

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -15,6 +16,8 @@ namespace osu.Game.Overlays.Toolbar
private OverlayContainer stateContainer;
private readonly Bindable<Visibility> overlayState = new Bindable<Visibility>();
public OverlayContainer StateContainer
{
get => stateContainer;
@ -22,10 +25,12 @@ namespace osu.Game.Overlays.Toolbar
{
stateContainer = value;
overlayState.UnbindBindings();
if (stateContainer != null)
{
Action = stateContainer.ToggleVisibility;
stateContainer.StateChanged += stateChanged;
overlayState.BindTo(stateContainer.State);
}
}
}
@ -40,18 +45,13 @@ namespace osu.Game.Overlays.Toolbar
Depth = 2,
Alpha = 0,
});
overlayState.ValueChanged += stateChanged;
}
protected override void Dispose(bool isDisposing)
private void stateChanged(ValueChangedEvent<Visibility> state)
{
base.Dispose(isDisposing);
if (stateContainer != null)
stateContainer.StateChanged -= stateChanged;
}
private void stateChanged(Visibility state)
{
switch (state)
switch (state.NewValue)
{
case Visibility.Hidden:
stateBackground.FadeOut(200);

View File

@ -100,14 +100,14 @@ namespace osu.Game.Overlays
switch (action)
{
case GlobalAction.DecreaseVolume:
if (State == Visibility.Hidden)
if (State.Value == Visibility.Hidden)
Show();
else
volumeMeterMaster.Decrease(amount, isPrecise);
return true;
case GlobalAction.IncreaseVolume:
if (State == Visibility.Hidden)
if (State.Value == Visibility.Hidden)
Show();
else
volumeMeterMaster.Increase(amount, isPrecise);
@ -126,7 +126,7 @@ namespace osu.Game.Overlays
public override void Show()
{
if (State == Visibility.Visible)
if (State.Value == Visibility.Visible)
schedulePopOut();
base.Show();

View File

@ -1,6 +1,7 @@
// 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.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Difficulty
@ -8,6 +9,7 @@ namespace osu.Game.Rulesets.Difficulty
public class DifficultyAttributes
{
public Mod[] Mods;
public Skill[] Skills;
public double StarRating;
@ -15,9 +17,10 @@ namespace osu.Game.Rulesets.Difficulty
{
}
public DifficultyAttributes(Mod[] mods, double starRating)
public DifficultyAttributes(Mod[] mods, Skill[] skills, double starRating)
{
Mods = mods;
Skills = skills;
StarRating = starRating;
}
}

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Utils;
@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills
/// <summary>
/// The peak strain for each <see cref="DifficultyCalculator.SectionLength"/> section of the beatmap.
/// </summary>
public IList<double> StrainPeaks => strainPeaks;
public IReadOnlyList<double> StrainPeaks => strainPeaks;
/// <summary>
/// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other.
@ -84,13 +85,12 @@ namespace osu.Game.Rulesets.Difficulty.Skills
/// </summary>
public double DifficultyValue()
{
strainPeaks.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
double difficulty = 0;
double weight = 1;
// Difficulty is the weighted sum of the highest strains from every section.
foreach (double strain in strainPeaks)
// We're sorting from highest to lowest strain.
foreach (double strain in strainPeaks.OrderByDescending(d => d))
{
difficulty += strain * weight;
weight *= DecayWeight;

View File

@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Edit
/// </summary>
public readonly DrawableHitObject HitObject;
protected override bool ShouldBeAlive => HitObject.IsAlive && HitObject.IsPresent || State == SelectionState.Selected;
protected override bool ShouldBeAlive => (HitObject.IsAlive && HitObject.IsPresent) || State == SelectionState.Selected;
public override bool HandlePositionalInput => ShouldBeAlive;
public override bool RemoveWhenNotAlive => false;

View File

@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
public override bool RemoveCompletedTransforms => false;
protected override bool RequiresChildrenUpdate => true;
public override bool IsPresent => base.IsPresent || State.Value == ArmedState.Idle && Clock?.CurrentTime >= LifetimeStart;
public override bool IsPresent => base.IsPresent || (State.Value == ArmedState.Idle && Clock?.CurrentTime >= LifetimeStart);
public readonly Bindable<ArmedState> State = new Bindable<ArmedState>();

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.UI
protected override void Update()
{
base.Update();
LastFrameState = State;
LastFrameState = State.Value;
}
}
}

View File

@ -136,9 +136,9 @@ namespace osu.Game.Scoring.Legacy
score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X;
else if (ratio300 > 0.9 && ratio50 <= 0.01 && countMiss == 0)
score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S;
else if (ratio300 > 0.8 && countMiss == 0 || ratio300 > 0.9)
else if ((ratio300 > 0.8 && countMiss == 0) || ratio300 > 0.9)
score.Rank = ScoreRank.A;
else if (ratio300 > 0.7 && countMiss == 0 || ratio300 > 0.8)
else if ((ratio300 > 0.7 && countMiss == 0) || ratio300 > 0.8)
score.Rank = ScoreRank.B;
else if (ratio300 > 0.6)
score.Rank = ScoreRank.C;
@ -159,9 +159,9 @@ namespace osu.Game.Scoring.Legacy
score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.XH : ScoreRank.X;
else if (ratio300 > 0.9 && ratio50 <= 0.01 && countMiss == 0)
score.Rank = score.Mods.Any(m => m is ModHidden || m is ModFlashlight) ? ScoreRank.SH : ScoreRank.S;
else if (ratio300 > 0.8 && countMiss == 0 || ratio300 > 0.9)
else if ((ratio300 > 0.8 && countMiss == 0) || ratio300 > 0.9)
score.Rank = ScoreRank.A;
else if (ratio300 > 0.7 && countMiss == 0 || ratio300 > 0.8)
else if ((ratio300 > 0.7 && countMiss == 0) || ratio300 > 0.8)
score.Rank = ScoreRank.B;
else if (ratio300 > 0.6)
score.Rank = ScoreRank.C;

View File

@ -56,12 +56,12 @@ namespace osu.Game.Screens.Menu
case ButtonSystemState.Exit:
case ButtonSystemState.Initial:
case ButtonSystemState.EnteringMode:
State = Visibility.Hidden;
Hide();
break;
case ButtonSystemState.TopLevel:
case ButtonSystemState.Play:
State = Visibility.Visible;
Show();
break;
}
@ -82,6 +82,10 @@ namespace osu.Game.Screens.Menu
}
}
public override void Hide() => State = Visibility.Hidden;
public override void Show() => State = Visibility.Visible;
public event Action<Visibility> StateChanged;
private class ButtonAreaBackground : Box, IStateful<ButtonAreaBackgroundState>

View File

@ -66,7 +66,7 @@ namespace osu.Game.Screens.Menu
if (setInfo == null)
{
// we need to import the default menu background beatmap
setInfo = beatmaps.Import(new ZipArchiveReader(game.Resources.GetStream(@"Tracks/circles.osz"), "circles.osz"));
setInfo = beatmaps.Import(new ZipArchiveReader(game.Resources.GetStream(@"Tracks/circles.osz"), "circles.osz")).Result;
setInfo.Protected = true;
beatmaps.Update(setInfo);

View File

@ -69,6 +69,7 @@ namespace osu.Game.Screens.Play
RelativeSizeAxes = Axes.Both;
sourceClock = (IAdjustableClock)beatmap.Track ?? new StopwatchClock();
(sourceClock as IAdjustableAudioComponent)?.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false };
@ -87,6 +88,8 @@ namespace osu.Game.Screens.Play
private double totalOffset => userOffsetClock.Offset + platformOffsetClock.Offset;
private readonly BindableDouble pauseFreqAdjust = new BindableDouble(1);
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
@ -122,6 +125,8 @@ namespace osu.Game.Screens.Play
Seek(GameplayClock.CurrentTime);
adjustableClock.Start();
IsPaused.Value = false;
this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In);
}
/// <summary>
@ -143,7 +148,8 @@ namespace osu.Game.Screens.Play
public void Stop()
{
adjustableClock.Stop();
this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => adjustableClock.Stop());
IsPaused.Value = true;
}
@ -175,5 +181,11 @@ namespace osu.Game.Screens.Play
foreach (var mod in mods.OfType<IApplicableToClock>())
mod.ApplyToClock(sourceClock);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
(sourceClock as IAdjustableAudioComponent)?.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust);
}
}
}

View File

@ -59,7 +59,7 @@ namespace osu.Game.Screens.Play
{
RelativeSizeAxes = Axes.Both;
StateChanged += s => selectionIndex = -1;
State.ValueChanged += s => selectionIndex = -1;
}
[BackgroundDependencyLoader]

View File

@ -46,7 +46,7 @@ namespace osu.Game.Screens.Play.HUD
}
};
State = Visibility.Visible;
Show();
}
protected override void PopIn() => this.FadeIn(fade_duration);

View File

@ -279,7 +279,7 @@ namespace osu.Game.Screens.Play
var score = CreateScore();
if (DrawableRuleset.ReplayScore == null)
scoreManager.Import(score);
scoreManager.Import(score).Wait();
this.Push(CreateResults(score));
@ -358,7 +358,7 @@ namespace osu.Game.Screens.Play
// There is a chance that we could be in a paused state as the ruleset's internal clock (see FrameStabilityContainer)
// could process an extra frame after the GameplayClock is stopped.
// In such cases we want the fail state to precede a user triggered pause.
if (PauseOverlay.State == Visibility.Visible)
if (PauseOverlay.State.Value == Visibility.Visible)
PauseOverlay.Hide();
failAnimation.Start();

View File

@ -46,7 +46,7 @@ namespace osu.Game.Screens.Play
{
this.startTime = startTime;
State = Visibility.Visible;
Show();
RelativePositionAxes = Axes.Both;
RelativeSizeAxes = Axes.X;
@ -136,7 +136,7 @@ namespace osu.Game.Screens.Play
protected override bool OnMouseMove(MouseMoveEvent e)
{
if (!e.HasAnyButtonPressed)
fadeContainer.State = Visibility.Visible;
fadeContainer.Show();
return base.OnMouseMove(e);
}
@ -181,7 +181,7 @@ namespace osu.Game.Screens.Play
if (!IsHovered && !IsDragged)
using (BeginDelayedSequence(1000))
scheduledHide = Schedule(() => State = Visibility.Hidden);
scheduledHide = Schedule(Hide);
break;
case Visibility.Hidden:
@ -196,7 +196,7 @@ namespace osu.Game.Screens.Play
protected override void LoadComplete()
{
base.LoadComplete();
State = Visibility.Visible;
Show();
}
protected override bool OnMouseDown(MouseDownEvent e)
@ -207,9 +207,13 @@ namespace osu.Game.Screens.Play
protected override bool OnMouseUp(MouseUpEvent e)
{
State = Visibility.Visible;
Show();
return base.OnMouseUp(e);
}
public override void Hide() => State = Visibility.Hidden;
public override void Show() => State = Visibility.Visible;
}
private class Button : OsuClickableContainer

View File

@ -106,7 +106,7 @@ namespace osu.Game.Screens.Play
protected override void LoadComplete()
{
State = Visibility.Visible;
Show();
replayLoaded.ValueChanged += loaded => AllowSeeking = loaded.NewValue;
replayLoaded.TriggerChange();

View File

@ -275,7 +275,7 @@ namespace osu.Game.Screens.Ranking
currentPage = page.NewValue?.CreatePage();
if (currentPage != null)
circleInner.Add(currentPage);
LoadComponentAsync(currentPage, circleInner.Add);
}, true);
}

View File

@ -152,9 +152,12 @@ namespace osu.Game.Screens.Select
{
Schedule(() =>
{
int? previouslySelectedID = null;
CarouselBeatmapSet existingSet = beatmapSets.FirstOrDefault(b => b.BeatmapSet.ID == beatmapSet.ID);
bool hadSelection = existingSet?.State?.Value == CarouselItemState.Selected;
// If the selected beatmap is about to be removed, store its ID so it can be re-selected if required
if (existingSet?.State?.Value == CarouselItemState.Selected)
previouslySelectedID = selectedBeatmap?.Beatmap.ID;
var newSet = createCarouselSet(beatmapSet);
@ -172,8 +175,8 @@ namespace osu.Game.Screens.Select
applyActiveCriteria(false, false);
//check if we can/need to maintain our current selection.
if (hadSelection)
select((CarouselItem)newSet.Beatmaps.FirstOrDefault(b => b.Beatmap.ID == selectedBeatmap?.Beatmap.ID) ?? newSet);
if (previouslySelectedID != null)
select((CarouselItem)newSet.Beatmaps.FirstOrDefault(b => b.Beatmap.ID == previouslySelectedID) ?? newSet);
itemsCache.Invalidate();
Schedule(() => BeatmapSetsChanged?.Invoke());

View File

@ -360,13 +360,13 @@ namespace osu.Game.Screens.Select
protected override void PopIn()
{
this.FadeIn(transition_duration, Easing.OutQuint);
loading.State = Visibility.Visible;
loading.Show();
}
protected override void PopOut()
{
this.FadeOut(transition_duration, Easing.OutQuint);
loading.State = Visibility.Hidden;
loading.Hide();
}
}
}

View File

@ -100,7 +100,7 @@ namespace osu.Game.Screens.Select
{
void removeOldInfo()
{
State = beatmap == null ? Visibility.Hidden : Visibility.Visible;
State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible;
Info?.FadeOut(250);
Info?.Expire();

View File

@ -32,6 +32,7 @@ using osuTK.Input;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Graphics.Sprites;
namespace osu.Game.Screens.Select
@ -256,8 +257,8 @@ namespace osu.Game.Screens.Select
if (!beatmaps.GetAllUsableBeatmapSets().Any() && beatmaps.StableInstallationAvailable)
dialogOverlay.Push(new ImportFromStablePopup(() =>
{
beatmaps.ImportFromStableAsync();
skins.ImportFromStableAsync();
Task.Run(beatmaps.ImportFromStableAsync);
Task.Run(skins.ImportFromStableAsync);
}));
});
}
@ -278,7 +279,7 @@ namespace osu.Game.Screens.Select
protected virtual void ExitFromBack()
{
if (ModSelect.State == Visibility.Visible)
if (ModSelect.State.Value == Visibility.Visible)
{
ModSelect.Hide();
return;
@ -520,7 +521,7 @@ namespace osu.Game.Screens.Select
if (base.OnExiting(next))
return true;
beatmapInfoWedge.State = Visibility.Hidden;
beatmapInfoWedge.Hide();
this.FadeOut(100);
@ -594,11 +595,17 @@ namespace osu.Game.Screens.Select
{
bindBindables();
// If a selection was already obtained, do not attempt to update the selected beatmap.
if (Carousel.SelectedBeatmapSet != null)
return;
// Attempt to select the current beatmap on the carousel, if it is valid to be selected.
if (!Beatmap.IsDefault && Beatmap.Value.BeatmapSetInfo?.DeletePending == false && Beatmap.Value.BeatmapSetInfo?.Protected == false
&& Carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo, false))
return;
if (Carousel.SelectedBeatmapSet == null && !Carousel.SelectNextRandom())
// If the current active beatmap could not be selected, select a new random beatmap.
if (!Carousel.SelectNextRandom())
{
// in the case random selection failed, we want to trigger selectionChanged
// to show the dummy beatmap (we have nothing else to display).

View File

@ -5,6 +5,8 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@ -71,9 +73,9 @@ namespace osu.Game.Skinning
protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo { Name = archive.Name };
protected override void Populate(SkinInfo model, ArchiveReader archive)
protected override async Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
{
base.Populate(model, archive);
await base.Populate(model, archive, cancellationToken);
Skin reference = getSkin(model);

View File

@ -15,7 +15,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.4" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.2" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.609.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.607.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.611.1" />
<PackageReference Include="SharpCompress" Version="0.23.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />

View File

@ -105,8 +105,8 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.1" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2019.518.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.607.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2019.607.0" />
<PackageReference Include="ppy.osu.Framework" Version="2019.611.1" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2019.611.1" />
<PackageReference Include="SharpCompress" Version="0.22.0" />
<PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />