1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 09:32:55 +08:00

Merge branch 'master' into velocitychanges

This commit is contained in:
Dan Balasescu 2022-07-12 16:52:57 +09:00 committed by GitHub
commit 7ab75606a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
105 changed files with 1249 additions and 444 deletions

View File

@ -9,7 +9,6 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />

View File

@ -9,7 +9,6 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />

View File

@ -9,7 +9,6 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />

View File

@ -9,7 +9,6 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />

View File

@ -52,7 +52,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.702.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.703.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.707.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -24,7 +24,7 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Clowd.Squirrel" Version="2.9.40" />
<PackageReference Include="Clowd.Squirrel" Version="2.9.42" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="System.IO.Packaging" Version="6.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.IO;
using BenchmarkDotNet.Attributes;
using osu.Framework.IO.Stores;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using BenchmarkDotNet.Attributes;
using osu.Game.Rulesets.Osu.Mods;
@ -11,7 +9,7 @@ namespace osu.Game.Benchmarks
{
public class BenchmarkMod : BenchmarkTest
{
private OsuModDoubleTime mod;
private OsuModDoubleTime mod = null!;
[Params(1, 10, 100)]
public int Times { get; set; }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Linq;
using System.Threading;
using BenchmarkDotNet.Attributes;
@ -17,9 +15,9 @@ namespace osu.Game.Benchmarks
{
public class BenchmarkRealmReads : BenchmarkTest
{
private TemporaryNativeStorage storage;
private RealmAccess realm;
private UpdateThread updateThread;
private TemporaryNativeStorage storage = null!;
private RealmAccess realm = null!;
private UpdateThread updateThread = null!;
[Params(1, 100, 1000)]
public int ReadsPerFetch { get; set; }
@ -135,9 +133,9 @@ namespace osu.Game.Benchmarks
[GlobalCleanup]
public void Cleanup()
{
realm?.Dispose();
storage?.Dispose();
updateThread?.Exit();
realm.Dispose();
storage.Dispose();
updateThread.Exit();
}
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Engines;
using osu.Game.Online.API;
@ -13,9 +11,9 @@ namespace osu.Game.Benchmarks
{
public class BenchmarkRuleset : BenchmarkTest
{
private OsuRuleset ruleset;
private APIMod apiModDoubleTime;
private APIMod apiModDifficultyAdjust;
private OsuRuleset ruleset = null!;
private APIMod apiModDoubleTime = null!;
private APIMod apiModDifficultyAdjust = null!;
public override void SetUp()
{

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using NUnit.Framework;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Running;

View File

@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />

View File

@ -120,10 +120,10 @@ namespace osu.Game.Rulesets.Catch.UI
lastHyperDashState = Catcher.HyperDashing;
}
public void SetCatcherPosition(float X)
public void SetCatcherPosition(float x)
{
float lastPosition = Catcher.X;
float newPosition = Math.Clamp(X, 0, CatchPlayfield.WIDTH);
float newPosition = Math.Clamp(x, 0, CatchPlayfield.WIDTH);
Catcher.X = newPosition;

View File

@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Filter;
using osu.Game.Rulesets.Mania.Beatmaps;

View File

@ -0,0 +1,27 @@
// 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 NUnit.Framework;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModRepel : OsuModTestScene
{
[TestCase(0.1f)]
[TestCase(0.5f)]
[TestCase(1)]
public void TestRepel(float strength)
{
CreateModTest(new ModTestData
{
Mod = new OsuModRepel
{
RepulsionStrength = { Value = strength },
},
PassCondition = () => true,
Autoplay = false,
});
}
}
}

View File

@ -1,9 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="Moq" Version="4.18.1" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Automation;
public override string Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised) };
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised), typeof(OsuModRepel) };
public bool PerformFail() => false;

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAutoplay : ModAutoplay
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate) }).ToArray();
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });

View File

@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override string Description => "No need to chase the circles your cursor is a magnet!";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax) };
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax), typeof(OsuModRepel) };
private IFrameStableClock gameplayClock;

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
{
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModAlternate) }).ToArray();
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised), typeof(OsuModRepel), typeof(OsuModAlternate) }).ToArray();
/// <summary>
/// How early before a hitobject's start time to trigger a hit.

View File

@ -0,0 +1,98 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using osu.Framework.Bindables;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Osu.Utils;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
internal class OsuModRepel : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
{
public override string Name => "Repel";
public override string Acronym => "RP";
public override ModType Type => ModType.Fun;
public override string Description => "Hit objects run away!";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModMagnetised) };
private IFrameStableClock? gameplayClock;
[SettingSource("Repulsion strength", "How strong the repulsion is.", 0)]
public BindableFloat RepulsionStrength { get; } = new BindableFloat(0.5f)
{
Precision = 0.05f,
MinValue = 0.05f,
MaxValue = 1.0f,
};
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
gameplayClock = drawableRuleset.FrameStableClock;
// Hide judgment displays and follow points as they won't make any sense.
// Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart.
drawableRuleset.Playfield.DisplayJudgements.Value = false;
(drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide();
}
public void Update(Playfield playfield)
{
var cursorPos = playfield.Cursor.ActiveCursor.DrawPosition;
foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
{
var destination = Vector2.Clamp(2 * drawable.Position - cursorPos, Vector2.Zero, OsuPlayfield.BASE_SIZE);
if (drawable.HitObject is Slider thisSlider)
{
var possibleMovementBounds = OsuHitObjectGenerationUtils.CalculatePossibleMovementBounds(thisSlider);
destination = Vector2.Clamp(
destination,
new Vector2(possibleMovementBounds.Left, possibleMovementBounds.Top),
new Vector2(possibleMovementBounds.Right, possibleMovementBounds.Bottom)
);
}
switch (drawable)
{
case DrawableHitCircle circle:
easeTo(circle, destination, cursorPos);
break;
case DrawableSlider slider:
if (!slider.HeadCircle.Result.HasResult)
easeTo(slider, destination, cursorPos);
else
easeTo(slider, destination - slider.Ball.DrawPosition, cursorPos);
break;
}
}
}
private void easeTo(DrawableHitObject hitObject, Vector2 destination, Vector2 cursorPos)
{
Debug.Assert(gameplayClock != null);
double dampLength = Vector2.Distance(hitObject.Position, cursorPos) / (0.04 * RepulsionStrength.Value + 0.04);
float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime);
float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime);
hitObject.Position = new Vector2(x, y);
}
}
}

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override string Description => "Everything rotates. EVERYTHING.";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised) };
public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel) };
private float theta;

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun;
public override string Description => "They just won't stay still...";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised) };
public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised), typeof(OsuModRepel) };
private const int wiggle_duration = 90; // (ms) Higher = fewer wiggles
private const int wiggle_strength = 10; // Higher = stronger wiggles

View File

@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModApproachDifferent(),
new OsuModMuted(),
new OsuModNoScope(),
new OsuModMagnetised(),
new MultiMod(new OsuModMagnetised(), new OsuModRepel()),
new ModAdaptiveSpeed()
};

View File

@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
base.LoadComplete();
complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200));
complete.BindValueChanged(complete => updateDiscColour(complete.NewValue, 200));
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
@ -137,6 +137,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
this.ScaleTo(initial_scale);
this.RotateTo(0);
updateDiscColour(false);
using (BeginDelayedSequence(spinner.TimePreempt / 2))
{
// constant ambient rotation to give the spinner "spinning" character.
@ -177,12 +179,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
}
}
// transforms we have from completing the spinner will be rolled back, so reapply immediately.
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
updateComplete(state == ArmedState.Hit, 0);
if (drawableSpinner.Result?.TimeCompleted is double completionTime)
{
using (BeginAbsoluteSequence(completionTime))
updateDiscColour(true, 200);
}
}
private void updateComplete(bool complete, double duration)
private void updateDiscColour(bool complete, double duration = 0)
{
var colour = complete ? completeColour : normalColour;

View File

@ -194,7 +194,28 @@ namespace osu.Game.Rulesets.Osu.Utils
private static Vector2 clampSliderToPlayfield(WorkingObject workingObject)
{
var slider = (Slider)workingObject.HitObject;
var possibleMovementBounds = calculatePossibleMovementBounds(slider);
var possibleMovementBounds = CalculatePossibleMovementBounds(slider);
// The slider rotation applied in computeModifiedPosition might make it impossible to fit the slider into the playfield
// For example, a long horizontal slider will be off-screen when rotated by 90 degrees
// In this case, limit the rotation to either 0 or 180 degrees
if (possibleMovementBounds.Width < 0 || possibleMovementBounds.Height < 0)
{
float currentRotation = getSliderRotation(slider);
float diff1 = getAngleDifference(workingObject.RotationOriginal, currentRotation);
float diff2 = getAngleDifference(workingObject.RotationOriginal + MathF.PI, currentRotation);
if (diff1 < diff2)
{
RotateSlider(slider, workingObject.RotationOriginal - getSliderRotation(slider));
}
else
{
RotateSlider(slider, workingObject.RotationOriginal + MathF.PI - getSliderRotation(slider));
}
possibleMovementBounds = CalculatePossibleMovementBounds(slider);
}
var previousPosition = workingObject.PositionModified;
@ -239,10 +260,12 @@ namespace osu.Game.Rulesets.Osu.Utils
/// Calculates a <see cref="RectangleF"/> which contains all of the possible movements of the slider (in relative X/Y coordinates)
/// such that the entire slider is inside the playfield.
/// </summary>
/// <param name="slider">The <see cref="Slider"/> for which to calculate a movement bounding box.</param>
/// <returns>A <see cref="RectangleF"/> which contains all of the possible movements of the slider such that the entire slider is inside the playfield.</returns>
/// <remarks>
/// If the slider is larger than the playfield, the returned <see cref="RectangleF"/> may have negative width/height.
/// </remarks>
private static RectangleF calculatePossibleMovementBounds(Slider slider)
public static RectangleF CalculatePossibleMovementBounds(Slider slider)
{
var pathPositions = new List<Vector2>();
slider.Path.GetPathToProgress(pathPositions, 0, 1);
@ -353,6 +376,18 @@ namespace osu.Game.Rulesets.Osu.Utils
return MathF.Atan2(endPositionVector.Y, endPositionVector.X);
}
/// <summary>
/// Get the absolute difference between 2 angles measured in Radians.
/// </summary>
/// <param name="angle1">The first angle</param>
/// <param name="angle2">The second angle</param>
/// <returns>The absolute difference with interval <c>[0, MathF.PI)</c></returns>
private static float getAngleDifference(float angle1, float angle2)
{
float diff = MathF.Abs(angle1 - angle2) % (MathF.PI * 2);
return MathF.Min(diff, MathF.PI * 2 - diff);
}
public class ObjectPositionInfo
{
/// <summary>
@ -395,6 +430,7 @@ namespace osu.Game.Rulesets.Osu.Utils
private class WorkingObject
{
public float RotationOriginal { get; }
public Vector2 PositionOriginal { get; }
public Vector2 PositionModified { get; set; }
public Vector2 EndPositionModified { get; set; }
@ -405,6 +441,7 @@ namespace osu.Game.Rulesets.Osu.Utils
public WorkingObject(ObjectPositionInfo positionInfo)
{
PositionInfo = positionInfo;
RotationOriginal = HitObject is Slider slider ? getSliderRotation(slider) : 0;
PositionModified = PositionOriginal = HitObject.Position;
EndPositionModified = HitObject.EndPosition;
}

View File

@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />

View File

@ -138,7 +138,7 @@ namespace osu.Game.Tests.Collections.IO
{
string firstRunName;
using (var host = new CleanRunHeadlessGameHost(bypassCleanup: true))
using (var host = new CleanRunHeadlessGameHost(bypassCleanupOnDispose: true))
{
firstRunName = host.Name;

View File

@ -59,6 +59,64 @@ namespace osu.Game.Tests.Database
});
}
[Test]
public void TestFailedWritePerformsRollback()
{
RunTestWithRealm((realm, _) =>
{
Assert.Throws<InvalidOperationException>(() =>
{
realm.Write(r =>
{
r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()));
throw new InvalidOperationException();
});
});
Assert.That(realm.Run(r => r.All<BeatmapInfo>()), Is.Empty);
});
}
[Test]
public void TestFailedNestedWritePerformsRollback()
{
RunTestWithRealm((realm, _) =>
{
Assert.Throws<InvalidOperationException>(() =>
{
realm.Write(r =>
{
realm.Write(_ =>
{
r.Add(new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata()));
throw new InvalidOperationException();
});
});
});
Assert.That(realm.Run(r => r.All<BeatmapInfo>()), Is.Empty);
});
}
[Test]
public void TestNestedWriteCalls()
{
RunTestWithRealm((realm, _) =>
{
var beatmap = new BeatmapInfo(CreateRuleset(), new BeatmapDifficulty(), new BeatmapMetadata());
var liveBeatmap = beatmap.ToLive(realm);
realm.Run(r =>
r.Write(_ =>
r.Write(_ =>
r.Add(beatmap)))
);
Assert.IsFalse(liveBeatmap.PerformRead(l => l.Hidden));
});
}
[Test]
public void TestAccessAfterAttach()
{

View File

@ -9,7 +9,6 @@ using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play;
@ -116,10 +115,10 @@ namespace osu.Game.Tests.Gameplay
AddStep($"set audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
AddStep("seek to 2500", () => gameplayClockContainer.Seek(2500));
AddAssert("gameplay clock time = 2500", () => Precision.AlmostEquals(gameplayClockContainer.CurrentTime, 2500, 10f));
AddStep("gameplay clock time = 2500", () => Assert.AreEqual(gameplayClockContainer.CurrentTime, 2500, 10f));
AddStep("seek to 10000", () => gameplayClockContainer.Seek(10000));
AddAssert("gameplay clock time = 10000", () => Precision.AlmostEquals(gameplayClockContainer.CurrentTime, 10000, 10f));
AddStep("gameplay clock time = 10000", () => Assert.AreEqual(gameplayClockContainer.CurrentTime, 10000, 10f));
}
protected override void Dispose(bool isDisposing)

View File

@ -315,6 +315,26 @@ namespace osu.Game.Tests.NonVisual
}
}
[Test]
public void TestBackupCreatedOnCorruptRealm()
{
using (var host = new CustomTestHeadlessGameHost())
{
try
{
File.WriteAllText(host.InitialStorage.GetFullPath(OsuGameBase.CLIENT_DATABASE_FILENAME, true), "i am definitely not a realm file");
LoadOsuIntoHost(host);
Assert.That(host.InitialStorage.GetFiles(string.Empty, "*_corrupt.realm"), Has.One.Items);
}
finally
{
host.Exit();
}
}
}
private static string getDefaultLocationFor(CustomTestHeadlessGameHost host)
{
string path = Path.Combine(TestRunHeadlessGameHost.TemporaryTestDirectory, host.Name);
@ -347,7 +367,7 @@ namespace osu.Game.Tests.NonVisual
public Storage InitialStorage { get; }
public CustomTestHeadlessGameHost([CallerMemberName] string callingMethodName = @"")
: base(callingMethodName: callingMethodName)
: base(callingMethodName: callingMethodName, bypassCleanupOnSetup: true)
{
string defaultStorageLocation = getDefaultLocationFor(this);

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using NUnit.Framework;
using osu.Game.Beatmaps;
@ -256,7 +254,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
private class CustomFilterCriteria : IRulesetFilterCriteria
{
public string CustomValue { get; set; }
public string? CustomValue { get; set; }
public bool Matches(BeatmapInfo beatmapInfo) => true;

View File

@ -212,17 +212,17 @@ namespace osu.Game.Tests.Online
{
}
protected override BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapUpdater beatmapUpdater)
protected override BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm)
{
return new TestBeatmapImporter(this, storage, realm, beatmapUpdater);
return new TestBeatmapImporter(this, storage, realm);
}
internal class TestBeatmapImporter : BeatmapImporter
{
private readonly TestBeatmapManager testBeatmapManager;
public TestBeatmapImporter(TestBeatmapManager testBeatmapManager, Storage storage, RealmAccess databaseAccess, BeatmapUpdater beatmapUpdater)
: base(storage, databaseAccess, beatmapUpdater)
public TestBeatmapImporter(TestBeatmapManager testBeatmapManager, Storage storage, RealmAccess databaseAccess)
: base(storage, databaseAccess)
{
this.testBeatmapManager = testBeatmapManager;
}

View File

@ -83,20 +83,20 @@ namespace osu.Game.Tests.Skins.IO
#region Cases where imports should match existing
[Test]
public Task TestImportTwiceWithSameMetadataAndFilename() => runSkinTest(async osu =>
public Task TestImportTwiceWithSameMetadataAndFilename([Values] bool batchImport) => runSkinTest(async osu =>
{
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "skin.osk"));
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "skin.osk"));
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "skin.osk"), batchImport);
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "skin.osk"), batchImport);
assertImportedOnce(import1, import2);
});
[Test]
public Task TestImportTwiceWithNoMetadataSameDownloadFilename() => runSkinTest(async osu =>
public Task TestImportTwiceWithNoMetadataSameDownloadFilename([Values] bool batchImport) => runSkinTest(async osu =>
{
// if a user downloads two skins that do have skin.ini files but don't have any creator metadata in the skin.ini, they should both import separately just for safety.
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni(string.Empty, string.Empty), "download.osk"));
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni(string.Empty, string.Empty), "download.osk"));
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni(string.Empty, string.Empty), "download.osk"), batchImport);
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni(string.Empty, string.Empty), "download.osk"), batchImport);
assertImportedOnce(import1, import2);
});
@ -134,10 +134,10 @@ namespace osu.Game.Tests.Skins.IO
});
[Test]
public Task TestSameMetadataNameSameFolderName() => runSkinTest(async osu =>
public Task TestSameMetadataNameSameFolderName([Values] bool batchImport) => runSkinTest(async osu =>
{
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "my custom skin 1"));
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "my custom skin 1"));
var import1 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "my custom skin 1"), batchImport);
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "my custom skin 1"), batchImport);
assertImportedOnce(import1, import2);
assertCorrectMetadata(import1, "name 1 [my custom skin 1]", "author 1", osu);
@ -357,10 +357,10 @@ namespace osu.Game.Tests.Skins.IO
}
}
private async Task<Live<SkinInfo>> loadSkinIntoOsu(OsuGameBase osu, ImportTask import)
private async Task<Live<SkinInfo>> loadSkinIntoOsu(OsuGameBase osu, ImportTask import, bool batchImport = false)
{
var skinManager = osu.Dependencies.Get<SkinManager>();
return await skinManager.Import(import);
return await skinManager.Import(import, batchImport);
}
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Linq;
using NUnit.Framework;
@ -24,7 +22,7 @@ namespace osu.Game.Tests.Visual.Editing
[TestFixture]
public class TestSceneComposeScreen : EditorClockTestScene
{
private EditorBeatmap editorBeatmap;
private EditorBeatmap editorBeatmap = null!;
[Cached]
private EditorClipboard clipboard = new EditorClipboard();

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.IO;
using System.Linq;
@ -39,7 +37,9 @@ namespace osu.Game.Tests.Visual.Editing
protected override bool IsolateSavingFromDatabase => false;
[Resolved]
private BeatmapManager beatmapManager { get; set; }
private BeatmapManager beatmapManager { get; set; } = null!;
private Guid currentBeatmapSetID => EditorBeatmap.BeatmapInfo.BeatmapSet?.ID ?? Guid.Empty;
public override void SetUpSteps()
{
@ -50,19 +50,19 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("make new beatmap unique", () => EditorBeatmap.Metadata.Title = Guid.NewGuid().ToString());
}
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new DummyWorkingBeatmap(Audio, null);
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) => new DummyWorkingBeatmap(Audio, null);
[Test]
public void TestCreateNewBeatmap()
{
AddStep("save beatmap", () => Editor.Save());
AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.Value.DeletePending == false);
AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == currentBeatmapSetID)?.Value.DeletePending == false);
}
[Test]
public void TestExitWithoutSave()
{
EditorBeatmap editorBeatmap = null;
EditorBeatmap editorBeatmap = null!;
AddStep("store editor beatmap", () => editorBeatmap = EditorBeatmap);
@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen());
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == editorBeatmap.BeatmapInfo.BeatmapSet.ID)?.Value.DeletePending == true);
AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == editorBeatmap.BeatmapInfo.BeatmapSet.AsNonNull().ID)?.Value.DeletePending == true);
}
[Test]
@ -160,7 +160,7 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("new beatmap persisted", () =>
{
var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == firstDifficultyName);
var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID);
var set = beatmapManager.QueryBeatmapSet(s => s.ID == currentBeatmapSetID);
return beatmap != null
&& beatmap.DifficultyName == firstDifficultyName
@ -179,7 +179,7 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("wait for created", () =>
{
string difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != firstDifficultyName;
});
@ -195,7 +195,7 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("new beatmap persisted", () =>
{
var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == secondDifficultyName);
var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID);
var set = beatmapManager.QueryBeatmapSet(s => s.ID == currentBeatmapSetID);
return beatmap != null
&& beatmap.DifficultyName == secondDifficultyName
@ -246,7 +246,7 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("new beatmap persisted", () =>
{
var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == originalDifficultyName);
var set = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID);
var set = beatmapManager.QueryBeatmapSet(s => s.ID == currentBeatmapSetID);
return beatmap != null
&& beatmap.DifficultyName == originalDifficultyName
@ -262,7 +262,7 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("wait for created", () =>
{
string difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != originalDifficultyName;
});
@ -281,13 +281,13 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("save beatmap", () => Editor.Save());
BeatmapInfo refetchedBeatmap = null;
Live<BeatmapSetInfo> refetchedBeatmapSet = null;
BeatmapInfo? refetchedBeatmap = null;
Live<BeatmapSetInfo>? refetchedBeatmapSet = null;
AddStep("refetch from database", () =>
{
refetchedBeatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == copyDifficultyName);
refetchedBeatmapSet = beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID);
refetchedBeatmapSet = beatmapManager.QueryBeatmapSet(s => s.ID == currentBeatmapSetID);
});
AddAssert("new beatmap persisted", () =>
@ -323,7 +323,7 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("wait for created", () =>
{
string difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != "New Difficulty";
});
AddAssert("new difficulty has correct name", () => EditorBeatmap.BeatmapInfo.DifficultyName == "New Difficulty (1)");
@ -359,7 +359,7 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("wait for created", () =>
{
string difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != duplicate_difficulty_name;
});

View File

@ -6,11 +6,13 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Overlays;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Screens.Select;
@ -23,7 +25,9 @@ namespace osu.Game.Tests.Visual.Editing
[Test]
public void TestCantExitWithoutSaving()
{
AddUntilStep("Wait for dialog overlay load", () => ((Drawable)Game.Dependencies.Get<IDialogOverlay>()).IsLoaded);
AddRepeatStep("Exit", () => InputManager.Key(Key.Escape), 10);
AddAssert("Sample playback disabled", () => Editor.SamplePlaybackDisabled.Value);
AddAssert("Editor is still active screen", () => Game.ScreenStack.CurrentScreen is Editor);
}
@ -40,6 +44,8 @@ namespace osu.Game.Tests.Visual.Editing
SaveEditor();
AddAssert("Hash updated", () => !string.IsNullOrEmpty(EditorBeatmap.BeatmapInfo.BeatmapSet?.Hash));
AddAssert("Beatmap has correct metadata", () => EditorBeatmap.BeatmapInfo.Metadata.Artist == "artist" && EditorBeatmap.BeatmapInfo.Metadata.Title == "title");
AddAssert("Beatmap has correct author", () => EditorBeatmap.BeatmapInfo.Metadata.Author.Username == "author");
AddAssert("Beatmap has correct difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName == "difficulty");

View File

@ -0,0 +1,107 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestScenePlayerLocalScoreImport : PlayerTestScene
{
private BeatmapManager beatmaps = null!;
private RulesetStore rulesets = null!;
private BeatmapSetInfo? importedSet;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new ScoreManager(rulesets, () => beatmaps, LocalStorage, Realm, Scheduler, API));
Dependencies.Cache(Realm);
}
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("import beatmap", () =>
{
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
});
}
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => beatmaps.GetWorkingBeatmap(importedSet?.Beatmaps.First()).Beatmap;
private Ruleset? customRuleset;
protected override Ruleset CreatePlayerRuleset() => customRuleset ?? new OsuRuleset();
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false);
protected override bool HasCustomSteps => true;
protected override bool AllowFail => false;
[Test]
public void TestScoreStoredLocally()
{
AddStep("set no custom ruleset", () => customRuleset = null);
CreateTest();
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
AddUntilStep("score in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null));
}
[Test]
public void TestScoreStoredLocallyCustomRuleset()
{
Ruleset createCustomRuleset() => new CustomRuleset();
AddStep("import custom ruleset", () => Realm.Write(r => r.Add(createCustomRuleset().RulesetInfo)));
AddStep("set custom ruleset", () => customRuleset = createCustomRuleset());
CreateTest();
AddAssert("score has custom ruleset", () => Player.Score.ScoreInfo.Ruleset.Equals(customRuleset.AsNonNull().RulesetInfo));
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
AddUntilStep("score in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null));
}
private class CustomRuleset : OsuRuleset, ILegacyRuleset
{
public override string Description => "custom";
public override string ShortName => "custom";
int ILegacyRuleset.LegacyID => -1;
public override ScoreProcessor CreateScoreProcessor() => new ScoreProcessor(this);
}
}
}

View File

@ -365,21 +365,9 @@ namespace osu.Game.Tests.Visual.Gameplay
ImportedScore = score;
// It was discovered that Score members could sometimes be half-populated.
// In particular, the RulesetID property could be set to 0 even on non-osu! maps.
// We want to test that the state of that property is consistent in this test.
// EF makes this impossible.
//
// First off, because of the EF navigational property-explicit foreign key field duality,
// it can happen that - for example - the Ruleset navigational property is correctly initialised to mania,
// but the RulesetID foreign key property is not initialised and remains 0.
// EF silently bypasses this by prioritising the Ruleset navigational property over the RulesetID foreign key one.
//
// Additionally, adding an entity to an EF DbSet CAUSES SIDE EFFECTS with regard to the foreign key property.
// In the above instance, if a ScoreInfo with Ruleset = {mania} and RulesetID = 0 is attached to an EF context,
// RulesetID WILL BE SILENTLY SET TO THE CORRECT VALUE of 3.
//
// For the above reasons, actual importing is disabled in this test.
// Calling base.ImportScore is omitted as it will fail for the test method which uses a custom ruleset.
// This can be resolved by doing something similar to what TestScenePlayerLocalScoreImport is doing,
// but requires a bit of restructuring.
}
}
}

View File

@ -142,6 +142,28 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value);
}
[Test]
public void TestLocallyAvailableWithoutReplay()
{
Live<ScoreInfo> imported = null;
AddStep("import score", () => imported = scoreManager.Import(getScoreInfo(false, false)));
AddStep("create button without replay", () =>
{
Child = downloadButton = new TestReplayDownloadButton(imported.Value)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
});
AddUntilStep("wait for load", () => downloadButton.IsLoaded);
AddUntilStep("state is not downloaded", () => downloadButton.State.Value == DownloadState.NotDownloaded);
AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value);
}
[Test]
public void TestScoreImportThenDelete()
{
@ -189,11 +211,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value);
}
private ScoreInfo getScoreInfo(bool replayAvailable)
private ScoreInfo getScoreInfo(bool replayAvailable, bool hasOnlineId = true)
{
return new APIScore
{
OnlineID = online_score_id,
OnlineID = hasOnlineId ? online_score_id : 0,
RulesetID = 0,
Beatmap = CreateAPIBeatmapSet(new OsuRuleset().RulesetInfo).Beatmaps.First(),
HasReplay = replayAvailable,

View File

@ -24,10 +24,11 @@ namespace osu.Game.Tests.Visual.Menus
public void TestMusicPlayAction()
{
AddStep("ensure playing something", () => Game.MusicController.EnsurePlayingSomething());
AddUntilStep("music playing", () => Game.MusicController.IsPlaying);
AddStep("toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay));
AddAssert("music paused", () => !Game.MusicController.IsPlaying && Game.MusicController.UserPauseRequested);
AddUntilStep("music paused", () => !Game.MusicController.IsPlaying && Game.MusicController.UserPauseRequested);
AddStep("toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay));
AddAssert("music resumed", () => Game.MusicController.IsPlaying && !Game.MusicController.UserPauseRequested);
AddUntilStep("music resumed", () => Game.MusicController.IsPlaying && !Game.MusicController.UserPauseRequested);
}
[Test]

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
@ -33,20 +31,21 @@ namespace osu.Game.Tests.Visual.Multiplayer
public class TestSceneMultiSpectatorScreen : MultiplayerTestScene
{
[Resolved]
private OsuGameBase game { get; set; }
private OsuGameBase game { get; set; } = null!;
[Resolved]
private OsuConfigManager config { get; set; }
private OsuConfigManager config { get; set; } = null!;
[Resolved]
private BeatmapManager beatmapManager { get; set; }
private BeatmapManager beatmapManager { get; set; } = null!;
private MultiSpectatorScreen spectatorScreen;
private MultiSpectatorScreen spectatorScreen = null!;
private readonly List<MultiplayerRoomUser> playingUsers = new List<MultiplayerRoomUser>();
private BeatmapSetInfo importedSet;
private BeatmapInfo importedBeatmap;
private BeatmapSetInfo importedSet = null!;
private BeatmapInfo importedBeatmap = null!;
private int importedBeatmapId;
[BackgroundDependencyLoader]
@ -340,7 +339,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
sendFrames(getPlayerIds(count), 300);
}
Player player = null;
Player? player = null;
AddStep($"get {PLAYER_1_ID} player instance", () => player = getInstance(PLAYER_1_ID).ChildrenOfType<Player>().Single());
@ -369,7 +368,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
b.Storyboard.GetLayer("Background").Add(sprite);
});
private void testLeadIn(Action<WorkingBeatmap> applyToBeatmap = null)
private void testLeadIn(Action<WorkingBeatmap>? applyToBeatmap = null)
{
start(PLAYER_1_ID);
@ -387,7 +386,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
assertRunning(PLAYER_1_ID);
}
private void loadSpectateScreen(bool waitForPlayerLoad = true, Action<WorkingBeatmap> applyToBeatmap = null)
private void loadSpectateScreen(bool waitForPlayerLoad = true, Action<WorkingBeatmap>? applyToBeatmap = null)
{
AddStep("load screen", () =>
{

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Diagnostics;
using System.Linq;
@ -51,17 +49,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayer : ScreenTestScene
{
private BeatmapManager beatmaps;
private RulesetStore rulesets;
private BeatmapSetInfo importedSet;
private BeatmapManager beatmaps = null!;
private RulesetStore rulesets = null!;
private BeatmapSetInfo importedSet = null!;
private TestMultiplayerComponents multiplayerComponents;
private TestMultiplayerComponents multiplayerComponents = null!;
private TestMultiplayerClient multiplayerClient => multiplayerComponents.MultiplayerClient;
private TestMultiplayerRoomManager roomManager => multiplayerComponents.RoomManager;
[Resolved]
private OsuConfigManager config { get; set; }
private OsuConfigManager config { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
@ -146,7 +144,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void removeLastUser()
{
APIUser lastUser = multiplayerClient.ServerRoom?.Users.Last().User;
APIUser? lastUser = multiplayerClient.ServerRoom?.Users.Last().User;
if (lastUser == null || lastUser == multiplayerClient.LocalUser?.User)
return;
@ -156,7 +154,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void kickLastUser()
{
APIUser lastUser = multiplayerClient.ServerRoom?.Users.Last().User;
APIUser? lastUser = multiplayerClient.ServerRoom?.Users.Last().User;
if (lastUser == null || lastUser == multiplayerClient.LocalUser?.User)
return;
@ -351,7 +349,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room", () => InputManager.Key(Key.Enter));
DrawableLoungeRoom.PasswordEntryPopover passwordEntryPopover = null;
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "password");
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().TriggerClick());
@ -678,7 +676,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestGameplayExitFlow()
{
Bindable<double> holdDelay = null;
Bindable<double>? holdDelay = null;
AddStep("Set hold delay to zero", () =>
{
@ -709,7 +707,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for lounge", () => multiplayerComponents.CurrentScreen is Screens.OnlinePlay.Multiplayer.Multiplayer);
AddStep("stop holding", () => InputManager.ReleaseKey(Key.Escape));
AddStep("set hold delay to default", () => holdDelay.SetDefault());
AddStep("set hold delay to default", () => holdDelay?.SetDefault());
}
[Test]
@ -992,7 +990,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for ready button to be enabled", () => readyButton.Enabled.Value);
MultiplayerUserState lastState = MultiplayerUserState.Idle;
MultiplayerRoomUser user = null;
MultiplayerRoomUser? user = null;
AddStep("click ready button", () =>
{

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
@ -66,7 +64,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestRemoveUser()
{
APIUser secondUser = null;
APIUser? secondUser = null;
AddStep("add a user", () =>
{
@ -80,7 +78,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("remove host", () => MultiplayerClient.RemoveUser(API.LocalUser.Value));
AddAssert("single panel is for second user", () => this.ChildrenOfType<ParticipantPanel>().Single().User.UserID == secondUser.Id);
AddAssert("single panel is for second user", () => this.ChildrenOfType<ParticipantPanel>().Single().User.UserID == secondUser?.Id);
}
[Test]
@ -368,7 +366,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void createNewParticipantsList()
{
ParticipantsList participantsList = null;
ParticipantsList? participantsList = null;
AddStep("create new list", () => Child = participantsList = new ParticipantsList
{
@ -378,7 +376,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Size = new Vector2(380, 0.7f)
});
AddUntilStep("wait for list to load", () => participantsList.IsLoaded);
AddUntilStep("wait for list to load", () => participantsList?.IsLoaded == true);
}
private void checkProgressBarVisibility(bool visible) =>

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Game.Overlays.BeatmapSet;
using System.Collections.Specialized;
using System.Linq;
@ -29,7 +27,7 @@ namespace osu.Game.Tests.Visual.Online
LeaderboardModSelector modSelector;
FillFlowContainer<SpriteText> selectedMods;
var ruleset = new Bindable<IRulesetInfo>();
var ruleset = new Bindable<IRulesetInfo?>();
Add(selectedMods = new FillFlowContainer<SpriteText>
{

View File

@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.SongSelect
dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesetStore, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, Scheduler));
dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, Realm, Scheduler, API));
Dependencies.Cache(Realm);
return dependencies;

View File

@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(scoreManager = new ScoreManager(rulesets, () => beatmapManager, LocalStorage, Realm, Scheduler));
Dependencies.Cache(scoreManager = new ScoreManager(rulesets, () => beatmapManager, LocalStorage, Realm, Scheduler, API));
Dependencies.Cache(Realm);
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();

View File

@ -18,6 +18,7 @@ using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Models;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
@ -73,7 +74,7 @@ namespace osu.Game.Tests.Visual.UserInterface
dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesetStore, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get<RulesetStore>(), () => beatmapManager, LocalStorage, Realm, Scheduler));
dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get<RulesetStore>(), () => beatmapManager, LocalStorage, Realm, Scheduler, API));
Dependencies.Cache(Realm);
return dependencies;
@ -100,6 +101,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Rank = ScoreRank.XH,
User = new APIUser { Username = "TestUser" },
Ruleset = new OsuRuleset().RulesetInfo,
Files = { new RealmNamedFileUsage(new RealmFile { Hash = $"{i}" }, string.Empty) }
};
importedScores.Add(scoreManager.Import(score).Value);

View File

@ -1,10 +1,10 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
@ -13,10 +13,24 @@ namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneModIcon : OsuTestScene
{
[Test]
public void TestShowAllMods()
{
AddStep("create mod icons", () =>
{
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Full,
ChildrenEnumerable = Ruleset.Value.CreateInstance().CreateAllMods().Select(m => new ModIcon(m)),
};
});
}
[Test]
public void TestChangeModType()
{
ModIcon icon = null;
ModIcon icon = null!;
AddStep("create mod icon", () => Child = icon = new ModIcon(new OsuModDoubleTime()));
AddStep("change mod", () => icon.Mod = new OsuModEasy());
@ -25,7 +39,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestInterfaceModType()
{
ModIcon icon = null;
ModIcon icon = null!;
var ruleset = new OsuRuleset();

View File

@ -1,14 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="DeepEqual" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
<PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="Moq" Version="4.18.1" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -4,7 +4,6 @@
<StartupObject>osu.Game.Tournament.Tests.TournamentTestRunner</StartupObject>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />

View File

@ -31,12 +31,11 @@ namespace osu.Game.Beatmaps
protected override string[] HashableFileTypes => new[] { ".osu" };
private readonly BeatmapUpdater? beatmapUpdater;
public Action<BeatmapSetInfo>? ProcessBeatmap { private get; set; }
public BeatmapImporter(Storage storage, RealmAccess realm, BeatmapUpdater? beatmapUpdater = null)
public BeatmapImporter(Storage storage, RealmAccess realm)
: base(storage, realm)
{
this.beatmapUpdater = beatmapUpdater;
}
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path).ToLowerInvariant() == ".osz";
@ -100,7 +99,7 @@ namespace osu.Game.Beatmaps
{
base.PostImport(model, realm);
beatmapUpdater?.Process(model);
ProcessBeatmap?.Invoke(model);
}
private void validateOnlineIds(BeatmapSetInfo beatmapSet, Realm realm)

View File

@ -34,14 +34,15 @@ namespace osu.Game.Beatmaps
/// Handles general operations related to global beatmap management.
/// </summary>
[ExcludeFromDynamicCompile]
public class BeatmapManager : ModelManager<BeatmapSetInfo>, IModelImporter<BeatmapSetInfo>, IWorkingBeatmapCache, IDisposable
public class BeatmapManager : ModelManager<BeatmapSetInfo>, IModelImporter<BeatmapSetInfo>, IWorkingBeatmapCache
{
public ITrackStore BeatmapTrackStore { get; }
private readonly BeatmapImporter beatmapImporter;
private readonly WorkingBeatmapCache workingBeatmapCache;
private readonly BeatmapUpdater? beatmapUpdater;
public Action<BeatmapSetInfo>? ProcessBeatmap { private get; set; }
public BeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider? api, AudioManager audioManager, IResourceStore<byte[]> gameResources, GameHost? host = null,
WorkingBeatmap? defaultBeatmap = null, BeatmapDifficultyCache? difficultyCache = null, bool performOnlineLookups = false)
@ -54,15 +55,14 @@ namespace osu.Game.Beatmaps
if (difficultyCache == null)
throw new ArgumentNullException(nameof(difficultyCache), "Difficulty cache must be provided if online lookups are required.");
beatmapUpdater = new BeatmapUpdater(this, difficultyCache, api, storage);
}
var userResources = new RealmFileStore(realm, storage).Store;
BeatmapTrackStore = audioManager.GetTrackStore(userResources);
beatmapImporter = CreateBeatmapImporter(storage, realm, rulesets, beatmapUpdater);
beatmapImporter = CreateBeatmapImporter(storage, realm);
beatmapImporter.ProcessBeatmap = obj => ProcessBeatmap?.Invoke(obj);
beatmapImporter.PostNotification = obj => PostNotification?.Invoke(obj);
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host);
@ -74,8 +74,7 @@ namespace osu.Game.Beatmaps
return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host);
}
protected virtual BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapUpdater? beatmapUpdater) =>
new BeatmapImporter(storage, realm, beatmapUpdater);
protected virtual BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm) => new BeatmapImporter(storage, realm);
/// <summary>
/// Create a new beatmap set, backed by a <see cref="BeatmapSetInfo"/> model,
@ -317,13 +316,15 @@ namespace osu.Game.Beatmaps
AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));
setInfo.Hash = beatmapImporter.ComputeHash(setInfo);
Realm.Write(r =>
{
var liveBeatmapSet = r.Find<BeatmapSetInfo>(setInfo.ID);
setInfo.CopyChangesToRealm(liveBeatmapSet);
beatmapUpdater?.Process(liveBeatmapSet, r);
ProcessBeatmap?.Invoke(liveBeatmapSet);
});
}
@ -468,15 +469,6 @@ namespace osu.Game.Beatmaps
#endregion
#region Implementation of IDisposable
public void Dispose()
{
beatmapUpdater?.Dispose();
}
#endregion
#region Implementation of IPostImports<out BeatmapSetInfo>
public Action<IEnumerable<Live<BeatmapSetInfo>>>? PresentImport

View File

@ -10,7 +10,6 @@ using osu.Framework.Platform;
using osu.Game.Database;
using osu.Game.Online.API;
using osu.Game.Rulesets.Objects;
using Realms;
namespace osu.Game.Beatmaps
{
@ -31,6 +30,14 @@ namespace osu.Game.Beatmaps
onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
}
/// <summary>
/// Queue a beatmap for background processing.
/// </summary>
public void Queue(int beatmapSetId)
{
// TODO: implement
}
/// <summary>
/// Queue a beatmap for background processing.
/// </summary>
@ -44,9 +51,7 @@ namespace osu.Game.Beatmaps
/// <summary>
/// Run all processing on a beatmap immediately.
/// </summary>
public void Process(BeatmapSetInfo beatmapSet) => beatmapSet.Realm.Write(r => Process(beatmapSet, r));
public void Process(BeatmapSetInfo beatmapSet, Realm realm)
public void Process(BeatmapSetInfo beatmapSet) => beatmapSet.Realm.Write(r =>
{
// Before we use below, we want to invalidate.
workingBeatmapCache.Invalidate(beatmapSet);
@ -71,7 +76,7 @@ namespace osu.Game.Beatmaps
// And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required.
workingBeatmapCache.Invalidate(beatmapSet);
}
});
private double calculateLength(IBeatmap b)
{

View File

@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps.Formats
{
Section section = Section.General;
string line;
string? line;
while ((line = stream.ReadLine()) != null)
{

View File

@ -137,8 +137,17 @@ namespace osu.Game.Beatmaps
try
{
using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path))))
return Decoder.GetDecoder<Beatmap>(stream).Decode(stream);
string fileStorePath = BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path);
var stream = GetStream(fileStorePath);
if (stream == null)
{
Logger.Log($"Beatmap failed to load (file {BeatmapInfo.Path} not found on disk at expected location {fileStorePath}).", level: LogLevel.Error);
return null;
}
using (var reader = new LineBufferedReader(stream))
return Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
}
catch (Exception e)
{
@ -154,7 +163,16 @@ namespace osu.Game.Beatmaps
try
{
return resources.LargeTextureStore.Get(BeatmapSetInfo.GetPathForFile(Metadata.BackgroundFile));
string fileStorePath = BeatmapSetInfo.GetPathForFile(Metadata.BackgroundFile);
var texture = resources.LargeTextureStore.Get(fileStorePath);
if (texture == null)
{
Logger.Log($"Beatmap background failed to load (file {Metadata.BackgroundFile} not found on disk at expected location {fileStorePath}).", level: LogLevel.Error);
return null;
}
return texture;
}
catch (Exception e)
{
@ -173,7 +191,16 @@ namespace osu.Game.Beatmaps
try
{
return resources.Tracks.Get(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile));
string fileStorePath = BeatmapSetInfo.GetPathForFile(Metadata.AudioFile);
var track = resources.Tracks.Get(fileStorePath);
if (track == null)
{
Logger.Log($"Beatmap failed to load (file {Metadata.AudioFile} not found on disk at expected location {fileStorePath}).", level: LogLevel.Error);
return null;
}
return track;
}
catch (Exception e)
{
@ -192,8 +219,17 @@ namespace osu.Game.Beatmaps
try
{
var trackData = GetStream(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile));
return trackData == null ? null : new Waveform(trackData);
string fileStorePath = BeatmapSetInfo.GetPathForFile(Metadata.AudioFile);
var trackData = GetStream(fileStorePath);
if (trackData == null)
{
Logger.Log($"Beatmap waveform failed to load (file {Metadata.AudioFile} not found on disk at expected location {fileStorePath}).", level: LogLevel.Error);
return null;
}
return new Waveform(trackData);
}
catch (Exception e)
{
@ -211,20 +247,38 @@ namespace osu.Game.Beatmaps
try
{
using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path))))
string fileStorePath = BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path);
var beatmapFileStream = GetStream(fileStorePath);
if (beatmapFileStream == null)
{
var decoder = Decoder.GetDecoder<Storyboard>(stream);
Logger.Log($"Beatmap failed to load (file {BeatmapInfo.Path} not found on disk at expected location {fileStorePath})", level: LogLevel.Error);
return null;
}
string storyboardFilename = BeatmapSetInfo?.Files.FirstOrDefault(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename;
using (var reader = new LineBufferedReader(beatmapFileStream))
{
var decoder = Decoder.GetDecoder<Storyboard>(reader);
// todo: support loading from both set-wide storyboard *and* beatmap specific.
if (string.IsNullOrEmpty(storyboardFilename))
storyboard = decoder.Decode(stream);
else
Stream storyboardFileStream = null;
if (BeatmapSetInfo?.Files.FirstOrDefault(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename is string storyboardFilename)
{
using (var secondaryStream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(storyboardFilename))))
storyboard = decoder.Decode(stream, secondaryStream);
string storyboardFileStorePath = BeatmapSetInfo?.GetPathForFile(storyboardFilename);
storyboardFileStream = GetStream(storyboardFileStorePath);
if (storyboardFileStream == null)
Logger.Log($"Storyboard failed to load (file {storyboardFilename} not found on disk at expected location {storyboardFileStorePath})", level: LogLevel.Error);
}
if (storyboardFileStream != null)
{
// Stand-alone storyboard was found, so parse in addition to the beatmap's local storyboard.
using (var secondaryReader = new LineBufferedReader(storyboardFileStream))
storyboard = decoder.Decode(reader, secondaryReader);
}
else
storyboard = decoder.Decode(reader);
}
}
catch (Exception e)

View File

@ -167,6 +167,8 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full);
SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f);
SetDefault(OsuSetting.LastProcessedMetadataId, -1);
}
public IDictionary<OsuSetting, string> GetLoggableState() =>
@ -363,5 +365,6 @@ namespace osu.Game.Configuration
DiscordRichPresence,
AutomaticallyDownloadWhenSpectating,
ShowOnlineExplicitContent,
LastProcessedMetadataId
}
}

View File

@ -132,11 +132,12 @@ namespace osu.Game.Database
{
try
{
realm.CreateBackup(Path.Combine(backup_folder, $"client.{backupSuffix}.realm"), realmBlockOperations);
realm.CreateBackup(Path.Combine(backup_folder, $"client.{backupSuffix}.realm"));
}
finally
{
// Above call will dispose of the blocking token when done.
// Once the backup is created, we need to stop blocking operations so the migration can complete.
realmBlockOperations.Dispose();
// Clean up here so we don't accidentally dispose twice.
realmBlockOperations = null;
}

View File

@ -8,7 +8,9 @@ using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Graphics;
using osu.Framework.Statistics;
namespace osu.Game.Database
{
@ -20,8 +22,16 @@ namespace osu.Game.Database
{
private readonly ConcurrentDictionary<TLookup, TValue> cache = new ConcurrentDictionary<TLookup, TValue>();
private readonly GlobalStatistic<MemoryCachingStatistics> statistics;
protected virtual bool CacheNullValues => true;
protected MemoryCachingComponent()
{
statistics = GlobalStatistics.Get<MemoryCachingStatistics>(nameof(MemoryCachingComponent<TLookup, TValue>), GetType().ReadableName());
statistics.Value = new MemoryCachingStatistics();
}
/// <summary>
/// Retrieve the cached value for the given lookup.
/// </summary>
@ -30,12 +40,20 @@ namespace osu.Game.Database
protected async Task<TValue> GetAsync([NotNull] TLookup lookup, CancellationToken token = default)
{
if (CheckExists(lookup, out TValue performance))
{
statistics.Value.HitCount++;
return performance;
}
var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false);
statistics.Value.MissCount++;
if (computed != null || CacheNullValues)
{
cache[lookup] = computed;
statistics.Value.Usage = cache.Count;
}
return computed;
}
@ -51,6 +69,8 @@ namespace osu.Game.Database
if (matchKeyPredicate(kvp.Key))
cache.TryRemove(kvp.Key, out _);
}
statistics.Value.Usage = cache.Count;
}
protected bool CheckExists([NotNull] TLookup lookup, out TValue value) =>
@ -63,5 +83,31 @@ namespace osu.Game.Database
/// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param>
/// <returns>The computed value.</returns>
protected abstract Task<TValue> ComputeValueAsync(TLookup lookup, CancellationToken token = default);
private class MemoryCachingStatistics
{
/// <summary>
/// Total number of cache hits.
/// </summary>
public int HitCount;
/// <summary>
/// Total number of cache misses.
/// </summary>
public int MissCount;
/// <summary>
/// Total number of cached entities.
/// </summary>
public int Usage;
public override string ToString()
{
int totalAccesses = HitCount + MissCount;
double hitRate = totalAccesses == 0 ? 0 : (double)HitCount / totalAccesses;
return $"i:{Usage} h:{HitCount} m:{MissCount} {hitRate:0%}";
}
}
}
}

View File

@ -66,7 +66,10 @@ namespace osu.Game.Database
/// </summary>
private readonly SemaphoreSlim realmRetrievalLock = new SemaphoreSlim(1);
private readonly ThreadLocal<bool> currentThreadCanCreateRealmInstances = new ThreadLocal<bool>();
/// <summary>
/// <c>true</c> when the current thread has already entered the <see cref="realmRetrievalLock"/>.
/// </summary>
private readonly ThreadLocal<bool> currentThreadHasRealmRetrievalLock = new ThreadLocal<bool>();
/// <summary>
/// Holds a map of functions registered via <see cref="RegisterCustomSubscription"/> and <see cref="RegisterForNotifications{T}"/> and a coinciding action which when triggered,
@ -184,14 +187,14 @@ namespace osu.Game.Database
// If a newer version database already exists, don't backup again. We can presume that the first backup is the one we care about.
if (!storage.Exists(newerVersionFilename))
CreateBackup(newerVersionFilename);
createBackup(newerVersionFilename);
storage.Delete(Filename);
}
else
{
Logger.Error(e, "Realm startup failed with unrecoverable error; starting with a fresh database. A backup of your database has been made.");
CreateBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_corrupt{realm_extension}");
createBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_corrupt{realm_extension}");
storage.Delete(Filename);
}
@ -236,7 +239,7 @@ namespace osu.Game.Database
}
// For extra safety, also store the temporarily-used database which we are about to replace.
CreateBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_newer_version_before_recovery{realm_extension}");
createBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_newer_version_before_recovery{realm_extension}");
storage.Delete(Filename);
@ -584,10 +587,11 @@ namespace osu.Game.Database
try
{
if (!currentThreadCanCreateRealmInstances.Value)
// Ensure that the thread that currently has the `realmRetrievalLock` can retrieve nested contexts and not deadlock on itself.
if (!currentThreadHasRealmRetrievalLock.Value)
{
realmRetrievalLock.Wait();
currentThreadCanCreateRealmInstances.Value = true;
currentThreadHasRealmRetrievalLock.Value = true;
tookSemaphoreLock = true;
}
else
@ -611,7 +615,7 @@ namespace osu.Game.Database
if (tookSemaphoreLock)
{
realmRetrievalLock.Release();
currentThreadCanCreateRealmInstances.Value = false;
currentThreadHasRealmRetrievalLock.Value = false;
}
}
}
@ -778,28 +782,37 @@ namespace osu.Game.Database
private string? getRulesetShortNameFromLegacyID(long rulesetId) =>
efContextFactory?.Get().RulesetInfo.FirstOrDefault(r => r.ID == rulesetId)?.ShortName;
public void CreateBackup(string backupFilename, IDisposable? blockAllOperations = null)
/// <summary>
/// Create a full realm backup.
/// </summary>
/// <param name="backupFilename">The filename for the backup.</param>
public void CreateBackup(string backupFilename)
{
using (blockAllOperations ?? BlockAllOperations("creating backup"))
if (realmRetrievalLock.CurrentCount != 0)
throw new InvalidOperationException($"Call {nameof(BlockAllOperations)} before creating a backup.");
createBackup(backupFilename);
}
private void createBackup(string backupFilename)
{
Logger.Log($"Creating full realm database backup at {backupFilename}", LoggingTarget.Database);
int attempts = 10;
while (attempts-- > 0)
{
Logger.Log($"Creating full realm database backup at {backupFilename}", LoggingTarget.Database);
int attempts = 10;
while (attempts-- > 0)
try
{
try
{
using (var source = storage.GetStream(Filename, mode: FileMode.Open))
using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew))
source.CopyTo(destination);
return;
}
catch (IOException)
{
// file may be locked during use.
Thread.Sleep(500);
}
using (var source = storage.GetStream(Filename, mode: FileMode.Open))
using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew))
source.CopyTo(destination);
return;
}
catch (IOException)
{
// file may be locked during use.
Thread.Sleep(500);
}
}
}
@ -907,16 +920,39 @@ namespace osu.Game.Database
void restoreOperation()
{
// Release of lock needs to happen here rather than on the update thread, as there may be another
// operation already blocking the update thread waiting for the blocking operation to complete.
Logger.Log(@"Restoring realm operations.", LoggingTarget.Database);
realmRetrievalLock.Release();
if (syncContext == null) return;
ManualResetEventSlim updateRealmReestablished = new ManualResetEventSlim();
// Post back to the update thread to revive any subscriptions.
// In the case we are on the update thread, let's also require this to run synchronously.
// This requirement is mostly due to test coverage, but shouldn't cause any harm.
if (ThreadSafety.IsUpdateThread)
syncContext?.Send(_ => ensureUpdateRealm(), null);
{
syncContext.Send(_ =>
{
ensureUpdateRealm();
updateRealmReestablished.Set();
}, null);
}
else
syncContext?.Post(_ => ensureUpdateRealm(), null);
{
syncContext.Post(_ =>
{
ensureUpdateRealm();
updateRealmReestablished.Set();
}, null);
}
// Wait for the post to complete to ensure a second `Migrate` operation doesn't start in the mean time.
// This is important to ensure `ensureUpdateRealm` is run before another blocking migration operation starts.
if (!updateRealmReestablished.Wait(10000))
throw new TimeoutException(@"Reestablishing update realm after block took too long");
}
}

View File

@ -258,15 +258,13 @@ namespace osu.Game.Database
{
cancellationToken.ThrowIfCancellationRequested();
bool checkedExisting = false;
TModel? existing = null;
TModel? existing;
if (batchImport && archive != null)
{
// this is a fast bail condition to improve large import performance.
item.Hash = computeHashFast(archive);
checkedExisting = true;
existing = CheckForExisting(item, realm);
if (existing != null)
@ -311,8 +309,12 @@ namespace osu.Game.Database
// TODO: we may want to run this outside of the transaction.
Populate(item, archive, realm, cancellationToken);
if (!checkedExisting)
existing = CheckForExisting(item, realm);
// Populate() may have adjusted file content (see SkinImporter.updateSkinIniMetadata), so regardless of whether a fast check was done earlier, let's
// check for existing items a second time.
//
// If this is ever a performance issue, the fast-check hash can be compared and trigger a skip of this second check if it still matches.
// I don't think it is a huge deal doing a second indexed check, though.
existing = CheckForExisting(item, realm);
if (existing != null)
{
@ -336,11 +338,11 @@ namespace osu.Game.Database
// import to store
realm.Add(item);
PostImport(item, realm);
transaction.Commit();
}
PostImport(item, realm);
LogForModel(item, @"Import successfully completed!");
}
catch (Exception e)
@ -386,7 +388,7 @@ namespace osu.Game.Database
/// <remarks>
/// In the case of no matching files, a hash will be generated from the passed archive's <see cref="ArchiveReader.Name"/>.
/// </remarks>
protected string ComputeHash(TModel item)
public string ComputeHash(TModel item)
{
// for now, concatenate all hashable files in the set to create a unique hash.
MemoryStream hashable = new MemoryStream();
@ -477,7 +479,7 @@ namespace osu.Game.Database
}
/// <summary>
/// Perform any final actions after the import has been committed to the database.
/// Perform any final actions before the import has been committed to the database.
/// </summary>
/// <param name="model">The model prepared for import.</param>
/// <param name="realm">The current realm context.</param>

View File

@ -8,19 +8,60 @@ namespace osu.Game.Database
{
public static class RealmExtensions
{
/// <summary>
/// Perform a write operation against the provided realm instance.
/// </summary>
/// <remarks>
/// This will automatically start a transaction if not already in one.
/// </remarks>
/// <param name="realm">The realm to operate on.</param>
/// <param name="function">The write operation to run.</param>
public static void Write(this Realm realm, Action<Realm> function)
{
using var transaction = realm.BeginWrite();
function(realm);
transaction.Commit();
Transaction? transaction = null;
try
{
if (!realm.IsInTransaction)
transaction = realm.BeginWrite();
function(realm);
transaction?.Commit();
}
finally
{
transaction?.Dispose();
}
}
/// <summary>
/// Perform a write operation against the provided realm instance.
/// </summary>
/// <remarks>
/// This will automatically start a transaction if not already in one.
/// </remarks>
/// <param name="realm">The realm to operate on.</param>
/// <param name="function">The write operation to run.</param>
public static T Write<T>(this Realm realm, Func<Realm, T> function)
{
using var transaction = realm.BeginWrite();
var result = function(realm);
transaction.Commit();
return result;
Transaction? transaction = null;
try
{
if (!realm.IsInTransaction)
transaction = realm.BeginWrite();
var result = function(realm);
transaction?.Commit();
return result;
}
finally
{
transaction?.Dispose();
}
}
/// <summary>

View File

@ -38,15 +38,21 @@ namespace osu.Game.Graphics.Backgrounds
private void load(OsuConfigManager config, SessionStatics sessionStatics)
{
seasonalBackgroundMode = config.GetBindable<SeasonalBackgroundMode>(OsuSetting.SeasonalBackgroundMode);
seasonalBackgroundMode.BindValueChanged(_ => SeasonalBackgroundChanged?.Invoke());
seasonalBackgroundMode.BindValueChanged(_ => triggerSeasonalBackgroundChanged());
seasonalBackgrounds = sessionStatics.GetBindable<APISeasonalBackgrounds>(Static.SeasonalBackgrounds);
seasonalBackgrounds.BindValueChanged(_ => SeasonalBackgroundChanged?.Invoke());
seasonalBackgrounds.BindValueChanged(_ => triggerSeasonalBackgroundChanged());
apiState.BindTo(api.State);
apiState.BindValueChanged(fetchSeasonalBackgrounds, true);
}
private void triggerSeasonalBackgroundChanged()
{
if (shouldShowSeasonal)
SeasonalBackgroundChanged?.Invoke();
}
private void fetchSeasonalBackgrounds(ValueChangedEvent<APIState> stateChanged)
{
if (seasonalBackgrounds.Value != null || stateChanged.NewValue != APIState.Online)
@ -64,15 +70,10 @@ namespace osu.Game.Graphics.Backgrounds
public SeasonalBackground LoadNextBackground()
{
if (seasonalBackgroundMode.Value == SeasonalBackgroundMode.Never
|| (seasonalBackgroundMode.Value == SeasonalBackgroundMode.Sometimes && !isInSeason))
{
if (!shouldShowSeasonal)
return null;
}
var backgrounds = seasonalBackgrounds.Value?.Backgrounds;
if (backgrounds == null || !backgrounds.Any())
return null;
var backgrounds = seasonalBackgrounds.Value.Backgrounds;
current = (current + 1) % backgrounds.Count;
string url = backgrounds[current].Url;
@ -80,6 +81,20 @@ namespace osu.Game.Graphics.Backgrounds
return new SeasonalBackground(url);
}
private bool shouldShowSeasonal
{
get
{
if (seasonalBackgroundMode.Value == SeasonalBackgroundMode.Never)
return false;
if (seasonalBackgroundMode.Value == SeasonalBackgroundMode.Sometimes && !isInSeason)
return false;
return seasonalBackgrounds.Value?.Backgrounds?.Any() == true;
}
}
private bool isInSeason => seasonalBackgrounds.Value != null && DateTimeOffset.Now < seasonalBackgrounds.Value.EndDate;
}

View File

@ -86,7 +86,7 @@ namespace osu.Game.Graphics.Containers
TimingControlPoint timingPoint;
EffectControlPoint effectPoint;
IsBeatSyncedWithTrack = BeatSyncSource.Clock?.IsRunning == true;
IsBeatSyncedWithTrack = BeatSyncSource.Clock?.IsRunning == true && BeatSyncSource.ControlPoints != null;
double currentTrackTime;

View File

@ -1,10 +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.
#nullable disable
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
@ -17,34 +14,31 @@ namespace osu.Game.IO
public class LineBufferedReader : IDisposable
{
private readonly StreamReader streamReader;
private readonly Queue<string> lineBuffer;
private string? peekedLine;
public LineBufferedReader(Stream stream, bool leaveOpen = false)
{
streamReader = new StreamReader(stream, Encoding.UTF8, true, 1024, leaveOpen);
lineBuffer = new Queue<string>();
}
/// <summary>
/// Reads the next line from the stream without consuming it.
/// Subsequent calls to <see cref="PeekLine"/> without a <see cref="ReadLine"/> will return the same string.
/// </summary>
public string PeekLine()
{
if (lineBuffer.Count > 0)
return lineBuffer.Peek();
string line = streamReader.ReadLine();
if (line != null)
lineBuffer.Enqueue(line);
return line;
}
public string? PeekLine() => peekedLine ??= streamReader.ReadLine();
/// <summary>
/// Reads the next line from the stream and consumes it.
/// If a line was peeked, that same line will then be consumed and returned.
/// </summary>
public string ReadLine() => lineBuffer.Count > 0 ? lineBuffer.Dequeue() : streamReader.ReadLine();
public string? ReadLine()
{
string? line = peekedLine ?? streamReader.ReadLine();
peekedLine = null;
return line;
}
/// <summary>
/// Reads the stream to its end and returns the text read.
@ -53,14 +47,13 @@ namespace osu.Game.IO
public string ReadToEnd()
{
string remainingText = streamReader.ReadToEnd();
if (lineBuffer.Count == 0)
if (peekedLine == null)
return remainingText;
var builder = new StringBuilder();
// this might not be completely correct due to varying platform line endings
while (lineBuffer.Count > 0)
builder.AppendLine(lineBuffer.Dequeue());
builder.AppendLine(peekedLine);
builder.Append(remainingText);
return builder.ToString();
@ -68,7 +61,7 @@ namespace osu.Game.IO
public void Dispose()
{
streamReader?.Dispose();
streamReader.Dispose();
}
}
}

View File

@ -14,6 +14,7 @@ namespace osu.Game.Online
APIClientID = "5";
SpectatorEndpointUrl = $"{APIEndpointUrl}/spectator";
MultiplayerEndpointUrl = $"{APIEndpointUrl}/multiplayer";
MetadataEndpointUrl = $"{APIEndpointUrl}/metadata";
}
}
}

View File

@ -39,5 +39,10 @@ namespace osu.Game.Online
/// The endpoint for the SignalR multiplayer server.
/// </summary>
public string MultiplayerEndpointUrl { get; set; }
/// <summary>
/// The endpoint for the SignalR metadata server.
/// </summary>
public string MetadataEndpointUrl { get; set; }
}
}

View File

@ -144,7 +144,7 @@ namespace osu.Game.Online
/// </summary>
private async Task handleErrorAndDelay(Exception exception, CancellationToken cancellationToken)
{
Logger.Log($"{clientName} connection error: {exception}", LoggingTarget.Network);
Logger.Log($"{clientName} connect attempt failed: {exception.Message}", LoggingTarget.Network);
await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
}

View File

@ -426,10 +426,10 @@ namespace osu.Game.Online.Leaderboards
items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods));
if (Score.Files.Count > 0)
{
items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => new LegacyScoreExporter(storage).Export(Score)));
if (!isOnlineScope)
items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(Score))));
}
return items.ToArray();
}

View File

@ -0,0 +1,28 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using MessagePack;
namespace osu.Game.Online.Metadata
{
/// <summary>
/// Describes a set of beatmaps which have been updated in some way.
/// </summary>
[MessagePackObject]
[Serializable]
public class BeatmapUpdates
{
[Key(0)]
public int[] BeatmapSetIDs { get; set; }
[Key(1)]
public int LastProcessedQueueID { get; set; }
public BeatmapUpdates(int[] beatmapSetIDs, int lastProcessedQueueID)
{
BeatmapSetIDs = beatmapSetIDs;
LastProcessedQueueID = lastProcessedQueueID;
}
}
}

View File

@ -0,0 +1,12 @@
// 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.Online.Metadata
{
public interface IMetadataClient
{
Task BeatmapSetsUpdated(BeatmapUpdates updates);
}
}

View File

@ -0,0 +1,21 @@
// 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.Online.Metadata
{
/// <summary>
/// Metadata server is responsible for keeping the osu! client up-to-date with any changes.
/// </summary>
public interface IMetadataServer
{
/// <summary>
/// Get any changes since a specific point in the queue.
/// Should be used to allow the client to catch up with any changes after being closed or disconnected.
/// </summary>
/// <param name="queueId">The last processed queue ID.</param>
/// <returns></returns>
Task<BeatmapUpdates> GetChangesSince(int queueId);
}
}

View File

@ -0,0 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Threading.Tasks;
using osu.Framework.Graphics;
namespace osu.Game.Online.Metadata
{
public abstract class MetadataClient : Component, IMetadataClient, IMetadataServer
{
public abstract Task BeatmapSetsUpdated(BeatmapUpdates updates);
public abstract Task<BeatmapUpdates> GetChangesSince(int queueId);
}
}

View File

@ -0,0 +1,134 @@
// 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.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.SignalR.Client;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Online.API;
namespace osu.Game.Online.Metadata
{
public class OnlineMetadataClient : MetadataClient
{
private readonly BeatmapUpdater beatmapUpdater;
private readonly string endpoint;
private IHubClientConnector? connector;
private Bindable<int> lastQueueId = null!;
private HubConnection? connection => connector?.CurrentConnection;
public OnlineMetadataClient(EndpointConfiguration endpoints, BeatmapUpdater beatmapUpdater)
{
this.beatmapUpdater = beatmapUpdater;
endpoint = endpoints.MetadataEndpointUrl;
}
[BackgroundDependencyLoader]
private void load(IAPIProvider api, OsuConfigManager config)
{
// Importantly, we are intentionally not using MessagePack here to correctly support derived class serialization.
// More information on the limitations / reasoning can be found in osu-server-spectator's initialisation code.
connector = api.GetHubConnector(nameof(OnlineMetadataClient), endpoint);
if (connector != null)
{
connector.ConfigureConnection = connection =>
{
// this is kind of SILLY
// https://github.com/dotnet/aspnetcore/issues/15198
connection.On<BeatmapUpdates>(nameof(IMetadataClient.BeatmapSetsUpdated), ((IMetadataClient)this).BeatmapSetsUpdated);
};
connector.IsConnected.BindValueChanged(isConnectedChanged, true);
}
lastQueueId = config.GetBindable<int>(OsuSetting.LastProcessedMetadataId);
}
private bool catchingUp;
private void isConnectedChanged(ValueChangedEvent<bool> connected)
{
if (!connected.NewValue)
return;
if (lastQueueId.Value >= 0)
{
catchingUp = true;
Task.Run(async () =>
{
try
{
while (true)
{
Logger.Log($"Requesting catch-up from {lastQueueId.Value}");
var catchUpChanges = await GetChangesSince(lastQueueId.Value);
lastQueueId.Value = catchUpChanges.LastProcessedQueueID;
if (catchUpChanges.BeatmapSetIDs.Length == 0)
{
Logger.Log($"Catch-up complete at {lastQueueId.Value}");
break;
}
await ProcessChanges(catchUpChanges.BeatmapSetIDs);
}
}
finally
{
catchingUp = false;
}
});
}
}
public override async Task BeatmapSetsUpdated(BeatmapUpdates updates)
{
Logger.Log($"Received beatmap updates {updates.BeatmapSetIDs.Length} updates with last id {updates.LastProcessedQueueID}");
// If we're still catching up, avoid updating the last ID as it will interfere with catch-up efforts.
if (!catchingUp)
lastQueueId.Value = updates.LastProcessedQueueID;
await ProcessChanges(updates.BeatmapSetIDs);
}
protected Task ProcessChanges(int[] beatmapSetIDs)
{
foreach (int id in beatmapSetIDs)
{
Logger.Log($"Processing {id}...");
beatmapUpdater.Queue(id);
}
return Task.CompletedTask;
}
public override Task<BeatmapUpdates> GetChangesSince(int queueId)
{
if (connector?.IsConnected.Value != true)
return Task.FromCanceled<BeatmapUpdates>(default);
Logger.Log($"Requesting any changes since last known queue id {queueId}");
Debug.Assert(connection != null);
return connection.InvokeAsync<BeatmapUpdates>(nameof(IMetadataServer.GetChangesSince), queueId);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
connector?.Dispose();
}
}
}

View File

@ -14,6 +14,7 @@ namespace osu.Game.Online
APIClientID = "5";
SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator";
MultiplayerEndpointUrl = "https://spectator.ppy.sh/multiplayer";
MetadataEndpointUrl = "https://spectator.ppy.sh/metadata";
}
}
}

View File

@ -5,7 +5,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
@ -41,6 +40,7 @@ using osu.Game.IO;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.Chat;
using osu.Game.Online.Metadata;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Spectator;
using osu.Game.Overlays;
@ -52,6 +52,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Skinning;
using osu.Game.Utils;
using File = System.IO.File;
using RuntimeInfo = osu.Framework.RuntimeInfo;
namespace osu.Game
@ -170,6 +171,7 @@ namespace osu.Game
public readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> AvailableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>(new Dictionary<ModType, IReadOnlyList<Mod>>());
private BeatmapDifficultyCache difficultyCache;
private BeatmapUpdater beatmapUpdater;
private UserLookupCache userCache;
private BeatmapLookupCache beatmapCache;
@ -180,6 +182,8 @@ namespace osu.Game
private MultiplayerClient multiplayerClient;
private MetadataClient metadataClient;
private RealmAccess realm;
protected override Container<Drawable> Content => content;
@ -263,15 +267,13 @@ namespace osu.Game
dependencies.CacheAs(API ??= new APIAccess(LocalConfig, endpoints, VersionHash));
dependencies.CacheAs(spectatorClient = new OnlineSpectatorClient(endpoints));
dependencies.CacheAs(multiplayerClient = new OnlineMultiplayerClient(endpoints));
var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures);
dependencies.Cache(difficultyCache = new BeatmapDifficultyCache());
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, difficultyCache, LocalConfig));
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, API, difficultyCache, LocalConfig));
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, difficultyCache, performOnlineLookups: true));
dependencies.Cache(BeatmapDownloader = new BeatmapModelDownloader(BeatmapManager, API));
@ -280,6 +282,15 @@ namespace osu.Game
// Add after all the above cache operations as it depends on them.
AddInternal(difficultyCache);
// TODO: OsuGame or OsuGameBase?
beatmapUpdater = new BeatmapUpdater(BeatmapManager, difficultyCache, API, Storage);
dependencies.CacheAs(spectatorClient = new OnlineSpectatorClient(endpoints));
dependencies.CacheAs(multiplayerClient = new OnlineMultiplayerClient(endpoints));
dependencies.CacheAs(metadataClient = new OnlineMetadataClient(endpoints, beatmapUpdater));
BeatmapManager.ProcessBeatmap = set => beatmapUpdater.Process(set);
dependencies.Cache(userCache = new UserLookupCache());
AddInternal(userCache);
@ -316,8 +327,10 @@ namespace osu.Game
// add api components to hierarchy.
if (API is APIAccess apiAccess)
AddInternal(apiAccess);
AddInternal(spectatorClient);
AddInternal(multiplayerClient);
AddInternal(metadataClient);
AddInternal(rulesetConfigCache);
@ -574,16 +587,17 @@ namespace osu.Game
base.Dispose(isDisposing);
RulesetStore?.Dispose();
BeatmapManager?.Dispose();
LocalConfig?.Dispose();
beatmapUpdater?.Dispose();
realm?.Dispose();
if (Host != null)
Host.ExceptionThrown -= onExceptionThrown;
}
ControlPointInfo IBeatSyncProvider.ControlPoints => Beatmap.Value.Beatmap.ControlPointInfo;
ControlPointInfo IBeatSyncProvider.ControlPoints => Beatmap.Value.BeatmapLoaded ? Beatmap.Value.Beatmap.ControlPointInfo : null;
IClock IBeatSyncProvider.Clock => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track : (IClock)null;
ChannelAmplitudes? IBeatSyncProvider.Amplitudes => Beatmap.Value.TrackLoaded ? Beatmap.Value.Track.CurrentAmplitudes : null;
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
@ -23,19 +21,19 @@ namespace osu.Game.Overlays.Login
{
public class LoginForm : FillFlowContainer
{
private TextBox username;
private TextBox password;
private ShakeContainer shakeSignIn;
private TextBox username = null!;
private TextBox password = null!;
private ShakeContainer shakeSignIn = null!;
[Resolved(CanBeNull = true)]
private IAPIProvider api { get; set; }
[Resolved]
private IAPIProvider api { get; set; } = null!;
public Action RequestHide;
public Action? RequestHide;
private void performLogin()
{
if (!string.IsNullOrEmpty(username.Text) && !string.IsNullOrEmpty(password.Text))
api?.Login(username.Text, password.Text);
api.Login(username.Text, password.Text);
else
shakeSignIn.Shake();
}
@ -49,6 +47,7 @@ namespace osu.Game.Overlays.Login
RelativeSizeAxes = Axes.X;
ErrorTextFlowContainer errorText;
LinkFlowContainer forgottenPaswordLink;
Children = new Drawable[]
{
@ -56,7 +55,7 @@ namespace osu.Game.Overlays.Login
{
PlaceholderText = UsersStrings.LoginUsername.ToLower(),
RelativeSizeAxes = Axes.X,
Text = api?.ProvidedUsername ?? string.Empty,
Text = api.ProvidedUsername,
TabbableContentContainer = this
},
password = new OsuPasswordTextBox
@ -80,6 +79,12 @@ namespace osu.Game.Overlays.Login
LabelText = "Stay signed in",
Current = config.GetBindable<bool>(OsuSetting.SavePassword),
},
forgottenPaswordLink = new LinkFlowContainer
{
Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
},
new Container
{
RelativeSizeAxes = Axes.X,
@ -103,15 +108,17 @@ namespace osu.Game.Overlays.Login
Text = "Register",
Action = () =>
{
RequestHide();
RequestHide?.Invoke();
accountCreation.Show();
}
}
};
forgottenPaswordLink.AddLink(LayoutStrings.PopupLoginLoginForgot, $"{api.WebsiteRootUrl}/home/password-reset");
password.OnCommit += (_, _) => performLogin();
if (api?.LastLoginError?.Message is string error)
if (api.LastLoginError?.Message is string error)
errorText.AddErrors(new[] { error });
}

View File

@ -133,9 +133,9 @@ namespace osu.Game.Overlays
UserPauseRequested = false;
if (restart)
CurrentTrack.Restart();
CurrentTrack.RestartAsync();
else if (!IsPlaying)
CurrentTrack.Start();
CurrentTrack.StartAsync();
return true;
}
@ -152,7 +152,7 @@ namespace osu.Game.Overlays
{
UserPauseRequested |= requestedByUser;
if (CurrentTrack.IsRunning)
CurrentTrack.Stop();
CurrentTrack.StopAsync();
}
/// <summary>
@ -250,7 +250,7 @@ namespace osu.Game.Overlays
{
// if not scheduled, the previously track will be stopped one frame later (see ScheduleAfterChildren logic in GameBase).
// we probably want to move this to a central method for switching to a new working beatmap in the future.
Schedule(() => CurrentTrack.Restart());
Schedule(() => CurrentTrack.RestartAsync());
}
private WorkingBeatmap current;

View File

@ -62,7 +62,8 @@ namespace osu.Game.Overlays.Settings.Sections
{
skinDropdown = new SkinSettingsDropdown
{
LabelText = SkinSettingsStrings.CurrentSkin
LabelText = SkinSettingsStrings.CurrentSkin,
Keywords = new[] { @"skins" }
},
new SettingsButton
{

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Game.Beatmaps;
using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Filter;

View File

@ -15,6 +15,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
using osuTK;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Localisation;
namespace osu.Game.Rulesets.UI
@ -53,7 +54,6 @@ namespace osu.Game.Rulesets.UI
private OsuColour colours { get; set; }
private Color4 backgroundColour;
private Color4 highlightedColour;
/// <summary>
/// Construct a new instance.
@ -123,47 +123,13 @@ namespace osu.Game.Rulesets.UI
modAcronym.FadeOut();
}
switch (value.Type)
{
default:
case ModType.DifficultyIncrease:
backgroundColour = colours.Yellow;
highlightedColour = colours.YellowLight;
break;
case ModType.DifficultyReduction:
backgroundColour = colours.Green;
highlightedColour = colours.GreenLight;
break;
case ModType.Automation:
backgroundColour = colours.Blue;
highlightedColour = colours.BlueLight;
break;
case ModType.Conversion:
backgroundColour = colours.Purple;
highlightedColour = colours.PurpleLight;
break;
case ModType.Fun:
backgroundColour = colours.Pink;
highlightedColour = colours.PinkLight;
break;
case ModType.System:
backgroundColour = colours.Gray6;
highlightedColour = colours.Gray7;
modIcon.Colour = colours.Yellow;
break;
}
backgroundColour = colours.ForModType(value.Type);
updateColour();
}
private void updateColour()
{
background.Colour = Selected.Value ? highlightedColour : backgroundColour;
background.Colour = Selected.Value ? backgroundColour.Lighten(0.2f) : backgroundColour;
}
}
}

View File

@ -13,6 +13,9 @@ using osu.Game.Database;
using osu.Game.IO.Archives;
using osu.Game.Rulesets;
using osu.Game.Scoring.Legacy;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using Realms;
namespace osu.Game.Scoring
@ -26,11 +29,14 @@ namespace osu.Game.Scoring
private readonly RulesetStore rulesets;
private readonly Func<BeatmapManager> beatmaps;
public ScoreImporter(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, RealmAccess realm)
private readonly IAPIProvider api;
public ScoreImporter(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, RealmAccess realm, IAPIProvider api)
: base(storage, realm)
{
this.rulesets = rulesets;
this.beatmaps = beatmaps;
this.api = api;
}
protected override ScoreInfo? CreateModel(ArchiveReader archive)
@ -68,5 +74,17 @@ namespace osu.Game.Scoring
if (string.IsNullOrEmpty(model.StatisticsJson))
model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics);
}
protected override void PostImport(ScoreInfo model, Realm realm)
{
base.PostImport(model, realm);
var userRequest = new GetUserRequest(model.RealmUser.Username);
api.Perform(userRequest);
if (userRequest.Response is APIUser user)
model.RealmUser.OnlineID = user.Id;
}
}
}

View File

@ -21,6 +21,7 @@ using osu.Game.IO.Archives;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring;
using osu.Game.Online.API;
namespace osu.Game.Scoring
{
@ -31,7 +32,7 @@ namespace osu.Game.Scoring
private readonly OsuConfigManager configManager;
private readonly ScoreImporter scoreImporter;
public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, RealmAccess realm, Scheduler scheduler,
public ScoreManager(RulesetStore rulesets, Func<BeatmapManager> beatmaps, Storage storage, RealmAccess realm, Scheduler scheduler, IAPIProvider api,
BeatmapDifficultyCache difficultyCache = null, OsuConfigManager configManager = null)
: base(storage, realm)
{
@ -39,7 +40,7 @@ namespace osu.Game.Scoring
this.difficultyCache = difficultyCache;
this.configManager = configManager;
scoreImporter = new ScoreImporter(rulesets, beatmaps, storage, realm)
scoreImporter = new ScoreImporter(rulesets, beatmaps, storage, realm, api)
{
PostNotification = obj => PostNotification?.Invoke(obj)
};

View File

@ -7,6 +7,7 @@ using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
@ -26,7 +27,7 @@ namespace osu.Game.Screens.Backgrounds
private const int background_count = 7;
private IBindable<APIUser> user;
private Bindable<Skin> skin;
private Bindable<BackgroundSource> mode;
private Bindable<BackgroundSource> source;
private Bindable<IntroSequence> introSequence;
private readonly SeasonalBackgroundLoader seasonalBackgroundLoader = new SeasonalBackgroundLoader();
@ -45,24 +46,29 @@ namespace osu.Game.Screens.Backgrounds
{
user = api.LocalUser.GetBoundCopy();
skin = skinManager.CurrentSkin.GetBoundCopy();
mode = config.GetBindable<BackgroundSource>(OsuSetting.MenuBackgroundSource);
source = config.GetBindable<BackgroundSource>(OsuSetting.MenuBackgroundSource);
introSequence = config.GetBindable<IntroSequence>(OsuSetting.IntroSequence);
AddInternal(seasonalBackgroundLoader);
user.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired);
skin.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired);
mode.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired);
beatmap.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired);
introSequence.ValueChanged += _ => Scheduler.AddOnce(loadNextIfRequired);
seasonalBackgroundLoader.SeasonalBackgroundChanged += () => Scheduler.AddOnce(loadNextIfRequired);
// Load first background asynchronously as part of BDL load.
currentDisplay = RNG.Next(0, background_count);
Next();
}
protected override void LoadComplete()
{
base.LoadComplete();
user.ValueChanged += _ => Scheduler.AddOnce(next);
skin.ValueChanged += _ => Scheduler.AddOnce(next);
source.ValueChanged += _ => Scheduler.AddOnce(next);
beatmap.ValueChanged += _ => Scheduler.AddOnce(next);
introSequence.ValueChanged += _ => Scheduler.AddOnce(next);
seasonalBackgroundLoader.SeasonalBackgroundChanged += () => Scheduler.AddOnce(next);
// helper function required for AddOnce usage.
void loadNextIfRequired() => Next();
void next() => Next();
}
private ScheduledDelegate nextTask;
@ -80,6 +86,8 @@ namespace osu.Game.Screens.Backgrounds
if (nextBackground == background)
return false;
Logger.Log("🌅 Background change queued");
cancellationTokenSource?.Cancel();
cancellationTokenSource = new CancellationTokenSource();
@ -108,12 +116,12 @@ namespace osu.Game.Screens.Backgrounds
if (newBackground == null && user.Value?.IsSupporter == true)
{
switch (mode.Value)
switch (source.Value)
{
case BackgroundSource.Beatmap:
case BackgroundSource.BeatmapWithStoryboard:
{
if (mode.Value == BackgroundSource.BeatmapWithStoryboard && AllowStoryboardBackground)
if (source.Value == BackgroundSource.BeatmapWithStoryboard && AllowStoryboardBackground)
newBackground = new BeatmapBackgroundWithStoryboard(beatmap.Value, getBackgroundTextureName());
newBackground ??= new BeatmapBackground(beatmap.Value, getBackgroundTextureName());

View File

@ -921,7 +921,7 @@ namespace osu.Game.Screens.Edit
private void cancelExit()
{
samplePlaybackDisabled.Value = false;
updateSampleDisabledState();
loader?.CancelPendingDifficultySwitch();
}

View File

@ -191,20 +191,48 @@ namespace osu.Game.Screens.Menu
State = ButtonSystemState.Initial;
}
protected override bool OnKeyDown(KeyDownEvent e)
/// <summary>
/// Triggers the <see cref="logo"/> if the current <see cref="State"/> is <see cref="ButtonSystemState.Initial"/>.
/// </summary>
/// <returns><c>true</c> if the <see cref="logo"/> was triggered, <c>false</c> otherwise.</returns>
private bool triggerInitialOsuLogo()
{
if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed || e.SuperPressed)
return false;
if (State == ButtonSystemState.Initial)
{
logo?.TriggerClick();
return true;
}
return false;
}
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Repeat || e.ControlPressed || e.ShiftPressed || e.AltPressed || e.SuperPressed)
return false;
if (triggerInitialOsuLogo())
return true;
return base.OnKeyDown(e);
}
protected override bool OnJoystickPress(JoystickPressEvent e)
{
if (triggerInitialOsuLogo())
return true;
return base.OnJoystickPress(e);
}
protected override bool OnMidiDown(MidiDownEvent e)
{
if (triggerInitialOsuLogo())
return true;
return base.OnMidiDown(e);
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat)

View File

@ -88,6 +88,11 @@ namespace osu.Game.Screens.Menu
/// </summary>
protected bool UsingThemedIntro { get; private set; }
protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault(false)
{
Colour = Color4.Black
};
protected IntroScreen([CanBeNull] Func<MainMenu> createNextScreen = null)
{
this.createNextScreen = createNextScreen;
@ -201,6 +206,8 @@ namespace osu.Game.Screens.Menu
{
this.FadeIn(300);
ApplyToBackground(b => b.FadeColour(Color4.Black, 100));
double fadeOutTime = exit_delay;
var track = musicController.CurrentTrack;
@ -243,13 +250,22 @@ namespace osu.Game.Screens.Menu
base.OnResuming(e);
}
private bool backgroundFaded;
protected void FadeInBackground(float duration = 0)
{
ApplyToBackground(b => b.FadeColour(Color4.White, duration));
backgroundFaded = true;
}
public override void OnSuspending(ScreenTransitionEvent e)
{
base.OnSuspending(e);
initialBeatmap = null;
}
protected override BackgroundScreen CreateBackground() => new BackgroundScreenBlack();
if (!backgroundFaded)
FadeInBackground(200);
}
protected virtual void StartTrack()
{

View File

@ -8,19 +8,18 @@ using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Screens;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures;
using osu.Framework.Logging;
using osu.Framework.Utils;
using osu.Framework.Screens;
using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets;
using osu.Game.Screens.Backgrounds;
using osuTK;
using osuTK.Graphics;
@ -32,16 +31,9 @@ namespace osu.Game.Screens.Menu
protected override string BeatmapFile => "triangles.osz";
protected override BackgroundScreen CreateBackground() => background = new BackgroundScreenDefault(false)
{
Alpha = 0,
};
[Resolved]
private AudioManager audio { get; set; }
private BackgroundScreenDefault background;
private Sample welcome;
private DecoupleableInterpolatingFramedClock decoupledClock;
@ -75,7 +67,7 @@ namespace osu.Game.Screens.Menu
if (UsingThemedIntro)
decoupledClock.ChangeSource(Track);
LoadComponentAsync(intro = new TrianglesIntroSequence(logo, background)
LoadComponentAsync(intro = new TrianglesIntroSequence(logo, () => FadeInBackground())
{
RelativeSizeAxes = Axes.Both,
Clock = decoupledClock,
@ -95,19 +87,10 @@ namespace osu.Game.Screens.Menu
{
base.OnSuspending(e);
// ensure the background is shown, even if the TriangleIntroSequence failed to do so.
background.ApplyToBackground(b => b.Show());
// important as there is a clock attached to a track which will likely be disposed before returning to this screen.
intro.Expire();
}
public override void OnResuming(ScreenTransitionEvent e)
{
base.OnResuming(e);
background.FadeOut(100);
}
protected override void StartTrack()
{
decoupledClock.Start();
@ -116,7 +99,7 @@ namespace osu.Game.Screens.Menu
private class TrianglesIntroSequence : CompositeDrawable
{
private readonly OsuLogo logo;
private readonly BackgroundScreenDefault background;
private readonly Action showBackgroundAction;
private OsuSpriteText welcomeText;
private RulesetFlow rulesets;
@ -128,10 +111,10 @@ namespace osu.Game.Screens.Menu
public Action LoadMenu;
public TrianglesIntroSequence(OsuLogo logo, BackgroundScreenDefault background)
public TrianglesIntroSequence(OsuLogo logo, Action showBackgroundAction)
{
this.logo = logo;
this.background = background;
this.showBackgroundAction = showBackgroundAction;
}
[Resolved]
@ -205,7 +188,6 @@ namespace osu.Game.Screens.Menu
rulesets.Hide();
lazerLogo.Hide();
background.ApplyToBackground(b => b.Hide());
using (BeginAbsoluteSequence(0))
{
@ -267,7 +249,7 @@ namespace osu.Game.Screens.Menu
logo.FadeIn();
background.ApplyToBackground(b => b.Show());
showBackgroundAction();
game.Add(new GameWideFlash());

View File

@ -5,11 +5,9 @@
using System;
using JetBrains.Annotations;
using osuTK;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Screens;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -17,8 +15,8 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Audio;
using osu.Game.Online.API;
using osu.Game.Screens.Backgrounds;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Menu
@ -35,13 +33,6 @@ namespace osu.Game.Screens.Menu
private ISample pianoReverb;
protected override string SeeyaSampleName => "Intro/Welcome/seeya";
protected override BackgroundScreen CreateBackground() => background = new BackgroundScreenDefault(false)
{
Alpha = 0,
};
private BackgroundScreenDefault background;
public IntroWelcome([CanBeNull] Func<MainMenu> createNextScreen = null)
: base(createNextScreen)
{
@ -100,7 +91,7 @@ namespace osu.Game.Screens.Menu
logo.ScaleTo(1);
logo.FadeIn(fade_in_time);
background.FadeIn(fade_in_time);
FadeInBackground(fade_in_time);
LoadMenu();
}, delay_step_two);
@ -108,12 +99,6 @@ namespace osu.Game.Screens.Menu
}
}
public override void OnResuming(ScreenTransitionEvent e)
{
base.OnResuming(e);
background.FadeOut(100);
}
private class WelcomeIntroSequence : Container
{
private Drawable welcomeText;

View File

@ -65,9 +65,7 @@ namespace osu.Game.Screens.Menu
[Resolved(canBeNull: true)]
private IDialogOverlay dialogOverlay { get; set; }
private BackgroundScreenDefault background;
protected override BackgroundScreen CreateBackground() => background;
protected override BackgroundScreen CreateBackground() => new BackgroundScreenDefault();
protected override bool PlayExitSound => false;
@ -148,7 +146,6 @@ namespace osu.Game.Screens.Menu
Buttons.OnSettings = () => settings?.ToggleVisibility();
Buttons.OnBeatmapListing = () => beatmapListing?.ToggleVisibility();
LoadComponentAsync(background = new BackgroundScreenDefault());
preloadSongSelect();
}

View File

@ -314,6 +314,9 @@ namespace osu.Game.Screens.OnlinePlay.Match
public override void OnSuspending(ScreenTransitionEvent e)
{
// Should be a noop in most cases, but let's ensure beyond doubt that the beatmap is in a correct state.
updateWorkingBeatmap();
onLeaving();
base.OnSuspending(e);
}

View File

@ -53,9 +53,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
[CanBeNull]
public Score Score { get; private set; }
[Resolved]
private BeatmapManager beatmapManager { get; set; }
private readonly BindableDouble volumeAdjustment = new BindableDouble();
private readonly Container gameplayContent;
private readonly LoadingLayer loadingLayer;
@ -84,6 +81,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
GameplayClock.Source = masterClock;
}
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; }
public void LoadScore([NotNull] Score score)
{
if (Score != null)
@ -91,7 +91,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
Score = score;
gameplayContent.Child = new PlayerIsolationContainer(beatmapManager.GetWorkingBeatmap(Score.ScoreInfo.BeatmapInfo), Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods)
gameplayContent.Child = new PlayerIsolationContainer(beatmap.Value, Score.ScoreInfo.Ruleset, Score.ScoreInfo.Mods)
{
RelativeSizeAxes = Axes.Both,
Child = stack = new OsuScreenStack

View File

@ -140,9 +140,14 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
},
new Section("Duration")
{
Child = DurationField = new DurationDropdown
Child = new Container
{
RelativeSizeAxes = Axes.X,
Height = 40,
Child = DurationField = new DurationDropdown
{
RelativeSizeAxes = Axes.X
}
}
},
new Section("Allowed attempts (across all playlist items)")

View File

@ -22,6 +22,7 @@ using osu.Framework.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Graphics.Containers;
using osu.Game.IO.Archives;
using osu.Game.Online.API;
@ -1064,12 +1065,15 @@ namespace osu.Game.Screens.Play
if (DrawableRuleset.ReplayScore != null)
return Task.CompletedTask;
LegacyByteArrayReader replayReader;
LegacyByteArrayReader replayReader = null;
using (var stream = new MemoryStream())
if (score.ScoreInfo.Ruleset.IsLegacyRuleset())
{
new LegacyScoreEncoder(score, GameplayState.Beatmap).Encode(stream);
replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr");
using (var stream = new MemoryStream())
{
new LegacyScoreEncoder(score, GameplayState.Beatmap).Encode(stream);
replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr");
}
}
// the import process will re-attach managed beatmap/rulesets to this score. we don't want this for now, so create a temporary copy to import.

View File

@ -168,7 +168,7 @@ namespace osu.Game.Screens.Play
request.Failure += e =>
{
Logger.Error(e, "Failed to submit score");
Logger.Error(e, $"Failed to submit score ({e.Message})");
scoreSubmissionSource.SetResult(false);
};

View File

@ -135,7 +135,7 @@ namespace osu.Game.Screens.Select
// TODO: Add realm queries to hint at which ruleset results are available in (and allow clicking to switch).
// TODO: Make this message more certain by ensuring the osu! beatmaps exist before suggesting.
if (filter?.Ruleset?.OnlineID > 0 && !filter.AllowConvertedBeatmaps)
if (filter?.Ruleset?.OnlineID != 0 && filter?.AllowConvertedBeatmaps == false)
{
textFlow.AddParagraph("- Try");
textFlow.AddLink(" enabling ", () => config.SetValue(OsuSetting.ShowConvertedBeatmaps, true));

View File

@ -15,30 +15,38 @@ namespace osu.Game.Tests
/// </summary>
public class CleanRunHeadlessGameHost : TestRunHeadlessGameHost
{
private readonly bool bypassCleanupOnSetup;
/// <summary>
/// Create a new instance.
/// </summary>
/// <param name="bindIPC">Whether to bind IPC channels.</param>
/// <param name="realtime">Whether the host should be forced to run in realtime, rather than accelerated test time.</param>
/// <param name="bypassCleanup">Whether to bypass directory cleanup on host disposal. Should be used only if a subsequent test relies on the files still existing.</param>
/// <param name="bypassCleanupOnSetup">Whether to bypass directory cleanup on <see cref="SetupForRun"/>.</param>
/// <param name="bypassCleanupOnDispose">Whether to bypass directory cleanup on host disposal. Should be used only if a subsequent test relies on the files still existing.</param>
/// <param name="callingMethodName">The name of the calling method, used for test file isolation and clean-up.</param>
public CleanRunHeadlessGameHost(bool bindIPC = false, bool realtime = true, bool bypassCleanup = false, [CallerMemberName] string callingMethodName = @"")
public CleanRunHeadlessGameHost(bool bindIPC = false, bool realtime = true, bool bypassCleanupOnSetup = false, bool bypassCleanupOnDispose = false,
[CallerMemberName] string callingMethodName = @"")
: base($"{callingMethodName}-{Guid.NewGuid()}", new HostOptions
{
BindIPC = bindIPC,
}, bypassCleanup: bypassCleanup, realtime: realtime)
}, bypassCleanup: bypassCleanupOnDispose, realtime: realtime)
{
this.bypassCleanupOnSetup = bypassCleanupOnSetup;
}
protected override void SetupForRun()
{
try
if (!bypassCleanupOnSetup)
{
Storage.DeleteDirectory(string.Empty);
}
catch
{
// May fail if a logging target has already been set via OsuStorage.ChangeTargetStorage.
try
{
Storage.DeleteDirectory(string.Empty);
}
catch
{
// May fail if a logging target has already been set via OsuStorage.ChangeTargetStorage.
}
}
// base call needs to be run *after* storage is emptied, as it updates the (static) logger's storage and may start writing

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