mirror of
https://github.com/ppy/osu.git
synced 2024-12-15 02:42:54 +08:00
Merge branch 'master' into stable-slider-followcircle-anims
This commit is contained in:
commit
0bc42ef67d
@ -9,7 +9,6 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||||
|
@ -9,7 +9,6 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||||
|
@ -9,7 +9,6 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||||
|
@ -9,7 +9,6 @@
|
|||||||
<GenerateProgramFile>false</GenerateProgramFile>
|
<GenerateProgramFile>false</GenerateProgramFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||||
|
@ -52,7 +52,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.702.0" />
|
<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>
|
||||||
<ItemGroup Label="Transitive Dependencies">
|
<ItemGroup Label="Transitive Dependencies">
|
||||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||||
|
@ -14,6 +14,7 @@ using osu.Framework.Platform;
|
|||||||
using osu.Game;
|
using osu.Game;
|
||||||
using osu.Game.IPC;
|
using osu.Game.IPC;
|
||||||
using osu.Game.Tournament;
|
using osu.Game.Tournament;
|
||||||
|
using SDL2;
|
||||||
using Squirrel;
|
using Squirrel;
|
||||||
|
|
||||||
namespace osu.Desktop
|
namespace osu.Desktop
|
||||||
@ -29,7 +30,21 @@ namespace osu.Desktop
|
|||||||
{
|
{
|
||||||
// run Squirrel first, as the app may exit after these run
|
// run Squirrel first, as the app may exit after these run
|
||||||
if (OperatingSystem.IsWindows())
|
if (OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
var windowsVersion = Environment.OSVersion.Version;
|
||||||
|
|
||||||
|
// While .NET 6 still supports Windows 7 and above, we are limited by realm currently, as they choose to only support 8.1 and higher.
|
||||||
|
// See https://www.mongodb.com/docs/realm/sdk/dotnet/#supported-platforms
|
||||||
|
if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2))
|
||||||
|
{
|
||||||
|
SDL.SDL_ShowSimpleMessageBox(SDL.SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
|
||||||
|
"Your operating system is too old to run osu!",
|
||||||
|
"This version of osu! requires at least Windows 8.1 to run.\nPlease upgrade your operating system or consider using an older version of osu!.", IntPtr.Zero);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
setupSquirrel();
|
setupSquirrel();
|
||||||
|
}
|
||||||
|
|
||||||
// Back up the cwd before DesktopGameHost changes it
|
// Back up the cwd before DesktopGameHost changes it
|
||||||
string cwd = Environment.CurrentDirectory;
|
string cwd = Environment.CurrentDirectory;
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<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="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||||
<PackageReference Include="System.IO.Packaging" Version="6.0.0" />
|
<PackageReference Include="System.IO.Packaging" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
using osu.Framework.IO.Stores;
|
using osu.Framework.IO.Stores;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
@ -11,7 +9,7 @@ namespace osu.Game.Benchmarks
|
|||||||
{
|
{
|
||||||
public class BenchmarkMod : BenchmarkTest
|
public class BenchmarkMod : BenchmarkTest
|
||||||
{
|
{
|
||||||
private OsuModDoubleTime mod;
|
private OsuModDoubleTime mod = null!;
|
||||||
|
|
||||||
[Params(1, 10, 100)]
|
[Params(1, 10, 100)]
|
||||||
public int Times { get; set; }
|
public int Times { get; set; }
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
@ -17,9 +15,9 @@ namespace osu.Game.Benchmarks
|
|||||||
{
|
{
|
||||||
public class BenchmarkRealmReads : BenchmarkTest
|
public class BenchmarkRealmReads : BenchmarkTest
|
||||||
{
|
{
|
||||||
private TemporaryNativeStorage storage;
|
private TemporaryNativeStorage storage = null!;
|
||||||
private RealmAccess realm;
|
private RealmAccess realm = null!;
|
||||||
private UpdateThread updateThread;
|
private UpdateThread updateThread = null!;
|
||||||
|
|
||||||
[Params(1, 100, 1000)]
|
[Params(1, 100, 1000)]
|
||||||
public int ReadsPerFetch { get; set; }
|
public int ReadsPerFetch { get; set; }
|
||||||
@ -135,9 +133,9 @@ namespace osu.Game.Benchmarks
|
|||||||
[GlobalCleanup]
|
[GlobalCleanup]
|
||||||
public void Cleanup()
|
public void Cleanup()
|
||||||
{
|
{
|
||||||
realm?.Dispose();
|
realm.Dispose();
|
||||||
storage?.Dispose();
|
storage.Dispose();
|
||||||
updateThread?.Exit();
|
updateThread.Exit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
using BenchmarkDotNet.Engines;
|
using BenchmarkDotNet.Engines;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
@ -13,9 +11,9 @@ namespace osu.Game.Benchmarks
|
|||||||
{
|
{
|
||||||
public class BenchmarkRuleset : BenchmarkTest
|
public class BenchmarkRuleset : BenchmarkTest
|
||||||
{
|
{
|
||||||
private OsuRuleset ruleset;
|
private OsuRuleset ruleset = null!;
|
||||||
private APIMod apiModDoubleTime;
|
private APIMod apiModDoubleTime = null!;
|
||||||
private APIMod apiModDifficultyAdjust;
|
private APIMod apiModDifficultyAdjust = null!;
|
||||||
|
|
||||||
public override void SetUp()
|
public override void SetUp()
|
||||||
{
|
{
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using BenchmarkDotNet.Attributes;
|
using BenchmarkDotNet.Attributes;
|
||||||
using BenchmarkDotNet.Running;
|
using BenchmarkDotNet.Running;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using BenchmarkDotNet.Configs;
|
using BenchmarkDotNet.Configs;
|
||||||
using BenchmarkDotNet.Running;
|
using BenchmarkDotNet.Running;
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||||
|
@ -120,10 +120,10 @@ namespace osu.Game.Rulesets.Catch.UI
|
|||||||
lastHyperDashState = Catcher.HyperDashing;
|
lastHyperDashState = Catcher.HyperDashing;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetCatcherPosition(float X)
|
public void SetCatcherPosition(float x)
|
||||||
{
|
{
|
||||||
float lastPosition = Catcher.X;
|
float lastPosition = Catcher.X;
|
||||||
float newPosition = Math.Clamp(X, 0, CatchPlayfield.WIDTH);
|
float newPosition = Math.Clamp(x, 0, CatchPlayfield.WIDTH);
|
||||||
|
|
||||||
Catcher.X = newPosition;
|
Catcher.X = newPosition;
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Filter;
|
using osu.Game.Rulesets.Filter;
|
||||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||||
|
27
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRepel.cs
Normal file
27
osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModRepel.cs
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,8 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.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="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||||
|
@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override ModType Type => ModType.Automation;
|
public override ModType Type => ModType.Automation;
|
||||||
public override string Description => @"Automatic cursor movement - just follow the rhythm.";
|
public override string Description => @"Automatic cursor movement - just follow the rhythm.";
|
||||||
public override double ScoreMultiplier => 1;
|
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;
|
public bool PerformFail() => false;
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
{
|
{
|
||||||
public class OsuModAutoplay : ModAutoplay
|
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)
|
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
|
||||||
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
|
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });
|
||||||
|
@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override ModType Type => ModType.Fun;
|
public override ModType Type => ModType.Fun;
|
||||||
public override string Description => "No need to chase the circles – your cursor is a magnet!";
|
public override string Description => "No need to chase the circles – your cursor is a magnet!";
|
||||||
public override double ScoreMultiplier => 1;
|
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;
|
private IFrameStableClock gameplayClock;
|
||||||
|
|
||||||
|
98
osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs
Normal file
98
osu.Game.Rulesets.Osu/Mods/OsuModRepel.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override ModType Type => ModType.Fun;
|
public override ModType Type => ModType.Fun;
|
||||||
public override string Description => "Everything rotates. EVERYTHING.";
|
public override string Description => "Everything rotates. EVERYTHING.";
|
||||||
public override double ScoreMultiplier => 1;
|
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;
|
private float theta;
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
|||||||
public override ModType Type => ModType.Fun;
|
public override ModType Type => ModType.Fun;
|
||||||
public override string Description => "They just won't stay still...";
|
public override string Description => "They just won't stay still...";
|
||||||
public override double ScoreMultiplier => 1;
|
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_duration = 90; // (ms) Higher = fewer wiggles
|
||||||
private const int wiggle_strength = 10; // Higher = stronger wiggles
|
private const int wiggle_strength = 10; // Higher = stronger wiggles
|
||||||
|
@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
new OsuModApproachDifferent(),
|
new OsuModApproachDifferent(),
|
||||||
new OsuModMuted(),
|
new OsuModMuted(),
|
||||||
new OsuModNoScope(),
|
new OsuModNoScope(),
|
||||||
new OsuModMagnetised(),
|
new MultiMod(new OsuModMagnetised(), new OsuModRepel()),
|
||||||
new ModAdaptiveSpeed()
|
new ModAdaptiveSpeed()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
{
|
{
|
||||||
base.LoadComplete();
|
base.LoadComplete();
|
||||||
|
|
||||||
complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200));
|
complete.BindValueChanged(complete => updateDiscColour(complete.NewValue, 200));
|
||||||
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
|
drawableSpinner.ApplyCustomUpdateState += updateStateTransforms;
|
||||||
|
|
||||||
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
|
updateStateTransforms(drawableSpinner, drawableSpinner.State.Value);
|
||||||
@ -137,6 +137,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
|||||||
this.ScaleTo(initial_scale);
|
this.ScaleTo(initial_scale);
|
||||||
this.RotateTo(0);
|
this.RotateTo(0);
|
||||||
|
|
||||||
|
updateDiscColour(false);
|
||||||
|
|
||||||
using (BeginDelayedSequence(spinner.TimePreempt / 2))
|
using (BeginDelayedSequence(spinner.TimePreempt / 2))
|
||||||
{
|
{
|
||||||
// constant ambient rotation to give the spinner "spinning" character.
|
// 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.
|
if (drawableSpinner.Result?.TimeCompleted is double completionTime)
|
||||||
using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt))
|
{
|
||||||
updateComplete(state == ArmedState.Hit, 0);
|
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;
|
var colour = complete ? completeColour : normalColour;
|
||||||
|
|
||||||
|
@ -194,7 +194,28 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
private static Vector2 clampSliderToPlayfield(WorkingObject workingObject)
|
private static Vector2 clampSliderToPlayfield(WorkingObject workingObject)
|
||||||
{
|
{
|
||||||
var slider = (Slider)workingObject.HitObject;
|
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;
|
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)
|
/// 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.
|
/// such that the entire slider is inside the playfield.
|
||||||
/// </summary>
|
/// </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>
|
/// <remarks>
|
||||||
/// If the slider is larger than the playfield, the returned <see cref="RectangleF"/> may have negative width/height.
|
/// If the slider is larger than the playfield, the returned <see cref="RectangleF"/> may have negative width/height.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
private static RectangleF calculatePossibleMovementBounds(Slider slider)
|
public static RectangleF CalculatePossibleMovementBounds(Slider slider)
|
||||||
{
|
{
|
||||||
var pathPositions = new List<Vector2>();
|
var pathPositions = new List<Vector2>();
|
||||||
slider.Path.GetPathToProgress(pathPositions, 0, 1);
|
slider.Path.GetPathToProgress(pathPositions, 0, 1);
|
||||||
@ -353,6 +376,18 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
return MathF.Atan2(endPositionVector.Y, endPositionVector.X);
|
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
|
public class ObjectPositionInfo
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -395,6 +430,7 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
|
|
||||||
private class WorkingObject
|
private class WorkingObject
|
||||||
{
|
{
|
||||||
|
public float RotationOriginal { get; }
|
||||||
public Vector2 PositionOriginal { get; }
|
public Vector2 PositionOriginal { get; }
|
||||||
public Vector2 PositionModified { get; set; }
|
public Vector2 PositionModified { get; set; }
|
||||||
public Vector2 EndPositionModified { get; set; }
|
public Vector2 EndPositionModified { get; set; }
|
||||||
@ -405,6 +441,7 @@ namespace osu.Game.Rulesets.Osu.Utils
|
|||||||
public WorkingObject(ObjectPositionInfo positionInfo)
|
public WorkingObject(ObjectPositionInfo positionInfo)
|
||||||
{
|
{
|
||||||
PositionInfo = positionInfo;
|
PositionInfo = positionInfo;
|
||||||
|
RotationOriginal = HitObject is Slider slider ? getSliderRotation(slider) : 0;
|
||||||
PositionModified = PositionOriginal = HitObject.Position;
|
PositionModified = PositionOriginal = HitObject.Position;
|
||||||
EndPositionModified = HitObject.EndPosition;
|
EndPositionModified = HitObject.EndPosition;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||||
|
@ -138,7 +138,7 @@ namespace osu.Game.Tests.Collections.IO
|
|||||||
{
|
{
|
||||||
string firstRunName;
|
string firstRunName;
|
||||||
|
|
||||||
using (var host = new CleanRunHeadlessGameHost(bypassCleanup: true))
|
using (var host = new CleanRunHeadlessGameHost(bypassCleanupOnDispose: true))
|
||||||
{
|
{
|
||||||
firstRunName = host.Name;
|
firstRunName = host.Name;
|
||||||
|
|
||||||
|
@ -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]
|
[Test]
|
||||||
public void TestAccessAfterAttach()
|
public void TestAccessAfterAttach()
|
||||||
{
|
{
|
||||||
|
@ -9,7 +9,6 @@ using osu.Framework.Audio;
|
|||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Timing;
|
using osu.Framework.Timing;
|
||||||
using osu.Framework.Utils;
|
|
||||||
using osu.Game.Configuration;
|
using osu.Game.Configuration;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Screens.Play;
|
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($"set audio offset to {userOffset}", () => localConfig.SetValue(OsuSetting.AudioOffset, userOffset));
|
||||||
|
|
||||||
AddStep("seek to 2500", () => gameplayClockContainer.Seek(2500));
|
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));
|
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)
|
protected override void Dispose(bool isDisposing)
|
||||||
|
@ -4,9 +4,12 @@
|
|||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Extensions;
|
using osu.Game.Extensions;
|
||||||
|
using osu.Game.Models;
|
||||||
|
using osu.Game.Tests.Resources;
|
||||||
|
|
||||||
namespace osu.Game.Tests.NonVisual
|
namespace osu.Game.Tests.NonVisual
|
||||||
{
|
{
|
||||||
@ -23,6 +26,47 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
Assert.IsTrue(ourInfo.MatchesOnlineID(otherInfo));
|
Assert.IsTrue(ourInfo.MatchesOnlineID(otherInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAudioEqualityNoFile()
|
||||||
|
{
|
||||||
|
var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
|
||||||
|
var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
|
||||||
|
|
||||||
|
Assert.AreNotEqual(beatmapSetA, beatmapSetB);
|
||||||
|
Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAudioEqualitySameHash()
|
||||||
|
{
|
||||||
|
var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
|
||||||
|
var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
|
||||||
|
|
||||||
|
addAudioFile(beatmapSetA, "abc");
|
||||||
|
addAudioFile(beatmapSetB, "abc");
|
||||||
|
|
||||||
|
Assert.AreNotEqual(beatmapSetA, beatmapSetB);
|
||||||
|
Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestAudioEqualityDifferentHash()
|
||||||
|
{
|
||||||
|
var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
|
||||||
|
var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
|
||||||
|
|
||||||
|
addAudioFile(beatmapSetA);
|
||||||
|
addAudioFile(beatmapSetB);
|
||||||
|
|
||||||
|
Assert.AreNotEqual(beatmapSetA, beatmapSetB);
|
||||||
|
Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void addAudioFile(BeatmapSetInfo beatmapSetInfo, string hash = null)
|
||||||
|
{
|
||||||
|
beatmapSetInfo.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = hash ?? Guid.NewGuid().ToString() }, "audio.mp3"));
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestDatabasedWithDatabased()
|
public void TestDatabasedWithDatabased()
|
||||||
{
|
{
|
||||||
|
@ -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)
|
private static string getDefaultLocationFor(CustomTestHeadlessGameHost host)
|
||||||
{
|
{
|
||||||
string path = Path.Combine(TestRunHeadlessGameHost.TemporaryTestDirectory, host.Name);
|
string path = Path.Combine(TestRunHeadlessGameHost.TemporaryTestDirectory, host.Name);
|
||||||
@ -347,7 +367,7 @@ namespace osu.Game.Tests.NonVisual
|
|||||||
public Storage InitialStorage { get; }
|
public Storage InitialStorage { get; }
|
||||||
|
|
||||||
public CustomTestHeadlessGameHost([CallerMemberName] string callingMethodName = @"")
|
public CustomTestHeadlessGameHost([CallerMemberName] string callingMethodName = @"")
|
||||||
: base(callingMethodName: callingMethodName)
|
: base(callingMethodName: callingMethodName, bypassCleanupOnSetup: true)
|
||||||
{
|
{
|
||||||
string defaultStorageLocation = getDefaultLocationFor(this);
|
string defaultStorageLocation = getDefaultLocationFor(this);
|
||||||
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -256,7 +254,7 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
|||||||
|
|
||||||
private class CustomFilterCriteria : IRulesetFilterCriteria
|
private class CustomFilterCriteria : IRulesetFilterCriteria
|
||||||
{
|
{
|
||||||
public string CustomValue { get; set; }
|
public string? CustomValue { get; set; }
|
||||||
|
|
||||||
public bool Matches(BeatmapInfo beatmapInfo) => true;
|
public bool Matches(BeatmapInfo beatmapInfo) => true;
|
||||||
|
|
||||||
|
@ -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
|
internal class TestBeatmapImporter : BeatmapImporter
|
||||||
{
|
{
|
||||||
private readonly TestBeatmapManager testBeatmapManager;
|
private readonly TestBeatmapManager testBeatmapManager;
|
||||||
|
|
||||||
public TestBeatmapImporter(TestBeatmapManager testBeatmapManager, Storage storage, RealmAccess databaseAccess, BeatmapUpdater beatmapUpdater)
|
public TestBeatmapImporter(TestBeatmapManager testBeatmapManager, Storage storage, RealmAccess databaseAccess)
|
||||||
: base(storage, databaseAccess, beatmapUpdater)
|
: base(storage, databaseAccess)
|
||||||
{
|
{
|
||||||
this.testBeatmapManager = testBeatmapManager;
|
this.testBeatmapManager = testBeatmapManager;
|
||||||
}
|
}
|
||||||
|
@ -134,6 +134,7 @@ namespace osu.Game.Tests.Resources
|
|||||||
DifficultyName = $"{version} {beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
|
DifficultyName = $"{version} {beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
|
||||||
StarRating = diff,
|
StarRating = diff,
|
||||||
Length = length,
|
Length = length,
|
||||||
|
BeatmapSet = beatmapSet,
|
||||||
BPM = bpm,
|
BPM = bpm,
|
||||||
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
|
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
|
||||||
Ruleset = rulesetInfo,
|
Ruleset = rulesetInfo,
|
||||||
|
@ -83,20 +83,20 @@ namespace osu.Game.Tests.Skins.IO
|
|||||||
#region Cases where imports should match existing
|
#region Cases where imports should match existing
|
||||||
|
|
||||||
[Test]
|
[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 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"));
|
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("test skin", "skinner"), "skin.osk"), batchImport);
|
||||||
|
|
||||||
assertImportedOnce(import1, import2);
|
assertImportedOnce(import1, import2);
|
||||||
});
|
});
|
||||||
|
|
||||||
[Test]
|
[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.
|
// 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 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"));
|
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni(string.Empty, string.Empty), "download.osk"), batchImport);
|
||||||
|
|
||||||
assertImportedOnce(import1, import2);
|
assertImportedOnce(import1, import2);
|
||||||
});
|
});
|
||||||
@ -134,10 +134,10 @@ namespace osu.Game.Tests.Skins.IO
|
|||||||
});
|
});
|
||||||
|
|
||||||
[Test]
|
[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 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"));
|
var import2 = await loadSkinIntoOsu(osu, new ImportTask(createOskWithIni("name 1", "author 1"), "my custom skin 1"), batchImport);
|
||||||
|
|
||||||
assertImportedOnce(import1, import2);
|
assertImportedOnce(import1, import2);
|
||||||
assertCorrectMetadata(import1, "name 1 [my custom skin 1]", "author 1", osu);
|
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>();
|
var skinManager = osu.Dependencies.Get<SkinManager>();
|
||||||
return await skinManager.Import(import);
|
return await skinManager.Import(import, batchImport);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
@ -24,7 +22,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class TestSceneComposeScreen : EditorClockTestScene
|
public class TestSceneComposeScreen : EditorClockTestScene
|
||||||
{
|
{
|
||||||
private EditorBeatmap editorBeatmap;
|
private EditorBeatmap editorBeatmap = null!;
|
||||||
|
|
||||||
[Cached]
|
[Cached]
|
||||||
private EditorClipboard clipboard = new EditorClipboard();
|
private EditorClipboard clipboard = new EditorClipboard();
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Audio.Track;
|
||||||
using osu.Framework.Extensions.ObjectExtensions;
|
using osu.Framework.Extensions.ObjectExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
@ -39,7 +38,9 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
protected override bool IsolateSavingFromDatabase => false;
|
protected override bool IsolateSavingFromDatabase => false;
|
||||||
|
|
||||||
[Resolved]
|
[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()
|
public override void SetUpSteps()
|
||||||
{
|
{
|
||||||
@ -50,19 +51,19 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddStep("make new beatmap unique", () => EditorBeatmap.Metadata.Title = Guid.NewGuid().ToString());
|
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]
|
[Test]
|
||||||
public void TestCreateNewBeatmap()
|
public void TestCreateNewBeatmap()
|
||||||
{
|
{
|
||||||
AddStep("save beatmap", () => Editor.Save());
|
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]
|
[Test]
|
||||||
public void TestExitWithoutSave()
|
public void TestExitWithoutSave()
|
||||||
{
|
{
|
||||||
EditorBeatmap editorBeatmap = null;
|
EditorBeatmap editorBeatmap = null!;
|
||||||
|
|
||||||
AddStep("store editor beatmap", () => editorBeatmap = EditorBeatmap);
|
AddStep("store editor beatmap", () => editorBeatmap = EditorBeatmap);
|
||||||
|
|
||||||
@ -78,7 +79,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen());
|
AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen());
|
||||||
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
|
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]
|
[Test]
|
||||||
@ -103,6 +104,8 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
*/
|
*/
|
||||||
public void TestAddAudioTrack()
|
public void TestAddAudioTrack()
|
||||||
{
|
{
|
||||||
|
AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual);
|
||||||
|
|
||||||
AddAssert("switch track to real track", () =>
|
AddAssert("switch track to real track", () =>
|
||||||
{
|
{
|
||||||
var setup = Editor.ChildrenOfType<SetupScreen>().First();
|
var setup = Editor.ChildrenOfType<SetupScreen>().First();
|
||||||
@ -131,6 +134,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
AddAssert("track is not virtual", () => Beatmap.Value.Track is not TrackVirtual);
|
||||||
AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000);
|
AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,7 +164,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddAssert("new beatmap persisted", () =>
|
AddAssert("new beatmap persisted", () =>
|
||||||
{
|
{
|
||||||
var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == firstDifficultyName);
|
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
|
return beatmap != null
|
||||||
&& beatmap.DifficultyName == firstDifficultyName
|
&& beatmap.DifficultyName == firstDifficultyName
|
||||||
@ -179,7 +183,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
AddUntilStep("wait for created", () =>
|
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;
|
return difficultyName != null && difficultyName != firstDifficultyName;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -195,7 +199,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddAssert("new beatmap persisted", () =>
|
AddAssert("new beatmap persisted", () =>
|
||||||
{
|
{
|
||||||
var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == secondDifficultyName);
|
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
|
return beatmap != null
|
||||||
&& beatmap.DifficultyName == secondDifficultyName
|
&& beatmap.DifficultyName == secondDifficultyName
|
||||||
@ -246,7 +250,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
AddAssert("new beatmap persisted", () =>
|
AddAssert("new beatmap persisted", () =>
|
||||||
{
|
{
|
||||||
var beatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == originalDifficultyName);
|
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
|
return beatmap != null
|
||||||
&& beatmap.DifficultyName == originalDifficultyName
|
&& beatmap.DifficultyName == originalDifficultyName
|
||||||
@ -262,7 +266,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
AddUntilStep("wait for created", () =>
|
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;
|
return difficultyName != null && difficultyName != originalDifficultyName;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -281,13 +285,13 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
AddStep("save beatmap", () => Editor.Save());
|
AddStep("save beatmap", () => Editor.Save());
|
||||||
|
|
||||||
BeatmapInfo refetchedBeatmap = null;
|
BeatmapInfo? refetchedBeatmap = null;
|
||||||
Live<BeatmapSetInfo> refetchedBeatmapSet = null;
|
Live<BeatmapSetInfo>? refetchedBeatmapSet = null;
|
||||||
|
|
||||||
AddStep("refetch from database", () =>
|
AddStep("refetch from database", () =>
|
||||||
{
|
{
|
||||||
refetchedBeatmap = beatmapManager.QueryBeatmap(b => b.DifficultyName == copyDifficultyName);
|
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", () =>
|
AddAssert("new beatmap persisted", () =>
|
||||||
@ -323,7 +327,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
AddUntilStep("wait for created", () =>
|
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";
|
return difficultyName != null && difficultyName != "New Difficulty";
|
||||||
});
|
});
|
||||||
AddAssert("new difficulty has correct name", () => EditorBeatmap.BeatmapInfo.DifficultyName == "New Difficulty (1)");
|
AddAssert("new difficulty has correct name", () => EditorBeatmap.BeatmapInfo.DifficultyName == "New Difficulty (1)");
|
||||||
@ -359,7 +363,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
AddUntilStep("wait for created", () =>
|
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;
|
return difficultyName != null && difficultyName != duplicate_difficulty_name;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -6,11 +6,13 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Screens;
|
using osu.Framework.Screens;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Utils;
|
using osu.Framework.Utils;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Screens.Edit;
|
using osu.Game.Screens.Edit;
|
||||||
using osu.Game.Screens.Edit.Compose.Components.Timeline;
|
using osu.Game.Screens.Edit.Compose.Components.Timeline;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
@ -23,7 +25,9 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestCantExitWithoutSaving()
|
public void TestCantExitWithoutSaving()
|
||||||
{
|
{
|
||||||
|
AddUntilStep("Wait for dialog overlay load", () => ((Drawable)Game.Dependencies.Get<IDialogOverlay>()).IsLoaded);
|
||||||
AddRepeatStep("Exit", () => InputManager.Key(Key.Escape), 10);
|
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);
|
AddAssert("Editor is still active screen", () => Game.ScreenStack.CurrentScreen is Editor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,6 +44,8 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
SaveEditor();
|
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 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 author", () => EditorBeatmap.BeatmapInfo.Metadata.Author.Username == "author");
|
||||||
AddAssert("Beatmap has correct difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName == "difficulty");
|
AddAssert("Beatmap has correct difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName == "difficulty");
|
||||||
|
@ -0,0 +1,122 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using 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 TestLastPlayedUpdated()
|
||||||
|
{
|
||||||
|
DateTimeOffset? getLastPlayed() => Realm.Run(r => r.Find<BeatmapInfo>(Beatmap.Value.BeatmapInfo.ID)?.LastPlayed);
|
||||||
|
|
||||||
|
AddStep("set no custom ruleset", () => customRuleset = null);
|
||||||
|
AddAssert("last played is null", () => getLastPlayed() == null);
|
||||||
|
|
||||||
|
CreateTest();
|
||||||
|
|
||||||
|
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
|
||||||
|
AddUntilStep("wait for last played to update", () => getLastPlayed() != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -365,21 +365,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
|
|
||||||
ImportedScore = score;
|
ImportedScore = score;
|
||||||
|
|
||||||
// It was discovered that Score members could sometimes be half-populated.
|
// Calling base.ImportScore is omitted as it will fail for the test method which uses a custom ruleset.
|
||||||
// In particular, the RulesetID property could be set to 0 even on non-osu! maps.
|
// This can be resolved by doing something similar to what TestScenePlayerLocalScoreImport is doing,
|
||||||
// We want to test that the state of that property is consistent in this test.
|
// but requires a bit of restructuring.
|
||||||
// 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.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -142,6 +142,28 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value);
|
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]
|
[Test]
|
||||||
public void TestScoreImportThenDelete()
|
public void TestScoreImportThenDelete()
|
||||||
{
|
{
|
||||||
@ -189,11 +211,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
|||||||
AddAssert("button is not enabled", () => !downloadButton.ChildrenOfType<DownloadButton>().First().Enabled.Value);
|
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
|
return new APIScore
|
||||||
{
|
{
|
||||||
OnlineID = online_score_id,
|
OnlineID = hasOnlineId ? online_score_id : 0,
|
||||||
RulesetID = 0,
|
RulesetID = 0,
|
||||||
Beatmap = CreateAPIBeatmapSet(new OsuRuleset().RulesetInfo).Beatmaps.First(),
|
Beatmap = CreateAPIBeatmapSet(new OsuRuleset().RulesetInfo).Beatmaps.First(),
|
||||||
HasReplay = replayAvailable,
|
HasReplay = replayAvailable,
|
||||||
|
@ -24,10 +24,11 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
public void TestMusicPlayAction()
|
public void TestMusicPlayAction()
|
||||||
{
|
{
|
||||||
AddStep("ensure playing something", () => Game.MusicController.EnsurePlayingSomething());
|
AddStep("ensure playing something", () => Game.MusicController.EnsurePlayingSomething());
|
||||||
|
AddUntilStep("music playing", () => Game.MusicController.IsPlaying);
|
||||||
AddStep("toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay));
|
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));
|
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]
|
[Test]
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -33,20 +31,21 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
public class TestSceneMultiSpectatorScreen : MultiplayerTestScene
|
public class TestSceneMultiSpectatorScreen : MultiplayerTestScene
|
||||||
{
|
{
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuGameBase game { get; set; }
|
private OsuGameBase game { get; set; } = null!;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuConfigManager config { get; set; }
|
private OsuConfigManager config { get; set; } = null!;
|
||||||
|
|
||||||
[Resolved]
|
[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 readonly List<MultiplayerRoomUser> playingUsers = new List<MultiplayerRoomUser>();
|
||||||
|
|
||||||
private BeatmapSetInfo importedSet;
|
private BeatmapSetInfo importedSet = null!;
|
||||||
private BeatmapInfo importedBeatmap;
|
private BeatmapInfo importedBeatmap = null!;
|
||||||
|
|
||||||
private int importedBeatmapId;
|
private int importedBeatmapId;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
@ -340,7 +339,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
sendFrames(getPlayerIds(count), 300);
|
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());
|
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);
|
b.Storyboard.GetLayer("Background").Add(sprite);
|
||||||
});
|
});
|
||||||
|
|
||||||
private void testLeadIn(Action<WorkingBeatmap> applyToBeatmap = null)
|
private void testLeadIn(Action<WorkingBeatmap>? applyToBeatmap = null)
|
||||||
{
|
{
|
||||||
start(PLAYER_1_ID);
|
start(PLAYER_1_ID);
|
||||||
|
|
||||||
@ -387,7 +386,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
assertRunning(PLAYER_1_ID);
|
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", () =>
|
AddStep("load screen", () =>
|
||||||
{
|
{
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -51,17 +49,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
public class TestSceneMultiplayer : ScreenTestScene
|
public class TestSceneMultiplayer : ScreenTestScene
|
||||||
{
|
{
|
||||||
private BeatmapManager beatmaps;
|
private BeatmapManager beatmaps = null!;
|
||||||
private RulesetStore rulesets;
|
private RulesetStore rulesets = null!;
|
||||||
private BeatmapSetInfo importedSet;
|
private BeatmapSetInfo importedSet = null!;
|
||||||
|
|
||||||
private TestMultiplayerComponents multiplayerComponents;
|
private TestMultiplayerComponents multiplayerComponents = null!;
|
||||||
|
|
||||||
private TestMultiplayerClient multiplayerClient => multiplayerComponents.MultiplayerClient;
|
private TestMultiplayerClient multiplayerClient => multiplayerComponents.MultiplayerClient;
|
||||||
private TestMultiplayerRoomManager roomManager => multiplayerComponents.RoomManager;
|
private TestMultiplayerRoomManager roomManager => multiplayerComponents.RoomManager;
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
private OsuConfigManager config { get; set; }
|
private OsuConfigManager config { get; set; } = null!;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(GameHost host, AudioManager audio)
|
private void load(GameHost host, AudioManager audio)
|
||||||
@ -146,7 +144,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
private void removeLastUser()
|
private void removeLastUser()
|
||||||
{
|
{
|
||||||
APIUser lastUser = multiplayerClient.ServerRoom?.Users.Last().User;
|
APIUser? lastUser = multiplayerClient.ServerRoom?.Users.Last().User;
|
||||||
|
|
||||||
if (lastUser == null || lastUser == multiplayerClient.LocalUser?.User)
|
if (lastUser == null || lastUser == multiplayerClient.LocalUser?.User)
|
||||||
return;
|
return;
|
||||||
@ -156,7 +154,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
private void kickLastUser()
|
private void kickLastUser()
|
||||||
{
|
{
|
||||||
APIUser lastUser = multiplayerClient.ServerRoom?.Users.Last().User;
|
APIUser? lastUser = multiplayerClient.ServerRoom?.Users.Last().User;
|
||||||
|
|
||||||
if (lastUser == null || lastUser == multiplayerClient.LocalUser?.User)
|
if (lastUser == null || lastUser == multiplayerClient.LocalUser?.User)
|
||||||
return;
|
return;
|
||||||
@ -351,7 +349,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
AddStep("select room", () => InputManager.Key(Key.Down));
|
AddStep("select room", () => InputManager.Key(Key.Down));
|
||||||
AddStep("join room", () => InputManager.Key(Key.Enter));
|
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);
|
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
|
||||||
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "password");
|
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "password");
|
||||||
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().TriggerClick());
|
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().TriggerClick());
|
||||||
@ -678,7 +676,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestGameplayExitFlow()
|
public void TestGameplayExitFlow()
|
||||||
{
|
{
|
||||||
Bindable<double> holdDelay = null;
|
Bindable<double>? holdDelay = null;
|
||||||
|
|
||||||
AddStep("Set hold delay to zero", () =>
|
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);
|
AddUntilStep("wait for lounge", () => multiplayerComponents.CurrentScreen is Screens.OnlinePlay.Multiplayer.Multiplayer);
|
||||||
|
|
||||||
AddStep("stop holding", () => InputManager.ReleaseKey(Key.Escape));
|
AddStep("stop holding", () => InputManager.ReleaseKey(Key.Escape));
|
||||||
AddStep("set hold delay to default", () => holdDelay.SetDefault());
|
AddStep("set hold delay to default", () => holdDelay?.SetDefault());
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -992,7 +990,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
AddUntilStep("wait for ready button to be enabled", () => readyButton.Enabled.Value);
|
AddUntilStep("wait for ready button to be enabled", () => readyButton.Enabled.Value);
|
||||||
|
|
||||||
MultiplayerUserState lastState = MultiplayerUserState.Idle;
|
MultiplayerUserState lastState = MultiplayerUserState.Idle;
|
||||||
MultiplayerRoomUser user = null;
|
MultiplayerRoomUser? user = null;
|
||||||
|
|
||||||
AddStep("click ready button", () =>
|
AddStep("click ready button", () =>
|
||||||
{
|
{
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
@ -66,7 +64,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestRemoveUser()
|
public void TestRemoveUser()
|
||||||
{
|
{
|
||||||
APIUser secondUser = null;
|
APIUser? secondUser = null;
|
||||||
|
|
||||||
AddStep("add a user", () =>
|
AddStep("add a user", () =>
|
||||||
{
|
{
|
||||||
@ -80,7 +78,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
AddStep("remove host", () => MultiplayerClient.RemoveUser(API.LocalUser.Value));
|
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]
|
[Test]
|
||||||
@ -368,7 +366,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
|
|
||||||
private void createNewParticipantsList()
|
private void createNewParticipantsList()
|
||||||
{
|
{
|
||||||
ParticipantsList participantsList = null;
|
ParticipantsList? participantsList = null;
|
||||||
|
|
||||||
AddStep("create new list", () => Child = participantsList = new ParticipantsList
|
AddStep("create new list", () => Child = participantsList = new ParticipantsList
|
||||||
{
|
{
|
||||||
@ -378,7 +376,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
Size = new Vector2(380, 0.7f)
|
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) =>
|
private void checkProgressBarVisibility(bool visible) =>
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using osu.Game.Overlays.BeatmapSet;
|
using osu.Game.Overlays.BeatmapSet;
|
||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@ -29,7 +27,7 @@ namespace osu.Game.Tests.Visual.Online
|
|||||||
LeaderboardModSelector modSelector;
|
LeaderboardModSelector modSelector;
|
||||||
FillFlowContainer<SpriteText> selectedMods;
|
FillFlowContainer<SpriteText> selectedMods;
|
||||||
|
|
||||||
var ruleset = new Bindable<IRulesetInfo>();
|
var ruleset = new Bindable<IRulesetInfo?>();
|
||||||
|
|
||||||
Add(selectedMods = new FillFlowContainer<SpriteText>
|
Add(selectedMods = new FillFlowContainer<SpriteText>
|
||||||
{
|
{
|
||||||
|
@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
|
|
||||||
dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm));
|
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(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);
|
Dependencies.Cache(Realm);
|
||||||
|
|
||||||
return dependencies;
|
return dependencies;
|
||||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
{
|
{
|
||||||
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
|
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
|
||||||
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
|
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);
|
Dependencies.Cache(Realm);
|
||||||
|
|
||||||
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
|
||||||
|
@ -18,6 +18,7 @@ using osu.Game.Beatmaps;
|
|||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Graphics.Cursor;
|
using osu.Game.Graphics.Cursor;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
|
using osu.Game.Models;
|
||||||
using osu.Game.Online.API.Requests.Responses;
|
using osu.Game.Online.API.Requests.Responses;
|
||||||
using osu.Game.Online.Leaderboards;
|
using osu.Game.Online.Leaderboards;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
@ -73,7 +74,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
|
|
||||||
dependencies.Cache(rulesetStore = new RealmRulesetStore(Realm));
|
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(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);
|
Dependencies.Cache(Realm);
|
||||||
|
|
||||||
return dependencies;
|
return dependencies;
|
||||||
@ -100,6 +101,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
Rank = ScoreRank.XH,
|
Rank = ScoreRank.XH,
|
||||||
User = new APIUser { Username = "TestUser" },
|
User = new APIUser { Username = "TestUser" },
|
||||||
Ruleset = new OsuRuleset().RulesetInfo,
|
Ruleset = new OsuRuleset().RulesetInfo,
|
||||||
|
Files = { new RealmNamedFileUsage(new RealmFile { Hash = $"{i}" }, string.Empty) }
|
||||||
};
|
};
|
||||||
|
|
||||||
importedScores.Add(scoreManager.Import(score).Value);
|
importedScores.Add(scoreManager.Import(score).Value);
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
@ -13,10 +13,24 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
{
|
{
|
||||||
public class TestSceneModIcon : OsuTestScene
|
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]
|
[Test]
|
||||||
public void TestChangeModType()
|
public void TestChangeModType()
|
||||||
{
|
{
|
||||||
ModIcon icon = null;
|
ModIcon icon = null!;
|
||||||
|
|
||||||
AddStep("create mod icon", () => Child = icon = new ModIcon(new OsuModDoubleTime()));
|
AddStep("create mod icon", () => Child = icon = new ModIcon(new OsuModDoubleTime()));
|
||||||
AddStep("change mod", () => icon.Mod = new OsuModEasy());
|
AddStep("change mod", () => icon.Mod = new OsuModEasy());
|
||||||
@ -25,7 +39,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestInterfaceModType()
|
public void TestInterfaceModType()
|
||||||
{
|
{
|
||||||
ModIcon icon = null;
|
ModIcon icon = null!;
|
||||||
|
|
||||||
var ruleset = new OsuRuleset();
|
var ruleset = new OsuRuleset();
|
||||||
|
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<Import Project="..\osu.TestProject.props" />
|
<Import Project="..\osu.TestProject.props" />
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
|
||||||
<PackageReference Include="DeepEqual" Version="2.0.0" />
|
<PackageReference Include="DeepEqual" Version="2.0.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||||
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
|
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||||
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
|
||||||
<PackageReference Include="Moq" Version="4.17.2" />
|
<PackageReference Include="Moq" Version="4.18.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<PropertyGroup Label="Project">
|
<PropertyGroup Label="Project">
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
<StartupObject>osu.Game.Tournament.Tests.TournamentTestRunner</StartupObject>
|
<StartupObject>osu.Game.Tournament.Tests.TournamentTestRunner</StartupObject>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||||
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
|
||||||
|
@ -12,7 +12,8 @@ namespace osu.Game.Tournament.Components
|
|||||||
{
|
{
|
||||||
public class TournamentSpriteTextWithBackground : CompositeDrawable
|
public class TournamentSpriteTextWithBackground : CompositeDrawable
|
||||||
{
|
{
|
||||||
protected readonly TournamentSpriteText Text;
|
public readonly TournamentSpriteText Text;
|
||||||
|
|
||||||
protected readonly Box Background;
|
protected readonly Box Background;
|
||||||
|
|
||||||
public TournamentSpriteTextWithBackground(string text = "")
|
public TournamentSpriteTextWithBackground(string text = "")
|
||||||
|
@ -22,6 +22,8 @@ namespace osu.Game.Tournament.Components
|
|||||||
private Video video;
|
private Video video;
|
||||||
private ManualClock manualClock;
|
private ManualClock manualClock;
|
||||||
|
|
||||||
|
public bool VideoAvailable => video != null;
|
||||||
|
|
||||||
public TourneyVideo(string filename, bool drawFallbackGradient = false)
|
public TourneyVideo(string filename, bool drawFallbackGradient = false)
|
||||||
{
|
{
|
||||||
this.filename = filename;
|
this.filename = filename;
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
|
||||||
@ -13,7 +11,7 @@ namespace osu.Game.Tournament.Models
|
|||||||
public int ID;
|
public int ID;
|
||||||
|
|
||||||
[JsonProperty("BeatmapInfo")]
|
[JsonProperty("BeatmapInfo")]
|
||||||
public TournamentBeatmap Beatmap;
|
public TournamentBeatmap? Beatmap;
|
||||||
|
|
||||||
public long Score;
|
public long Score;
|
||||||
|
|
||||||
|
@ -12,8 +12,6 @@ using osu.Framework.Allocation;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Graphics.Sprites;
|
|
||||||
using osu.Framework.Graphics.Textures;
|
|
||||||
using osu.Framework.Logging;
|
using osu.Framework.Logging;
|
||||||
using osu.Framework.Platform;
|
using osu.Framework.Platform;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
@ -45,7 +43,7 @@ namespace osu.Game.Tournament.Screens.Drawings
|
|||||||
public ITeamList TeamList;
|
public ITeamList TeamList;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(TextureStore textures, Storage storage)
|
private void load(Storage storage)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.Both;
|
RelativeSizeAxes = Axes.Both;
|
||||||
|
|
||||||
@ -91,11 +89,10 @@ namespace osu.Game.Tournament.Screens.Drawings
|
|||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new Sprite
|
new TourneyVideo("drawings")
|
||||||
{
|
{
|
||||||
|
Loop = true,
|
||||||
RelativeSizeAxes = Axes.Both,
|
RelativeSizeAxes = Axes.Both,
|
||||||
FillMode = FillMode.Fill,
|
|
||||||
Texture = textures.Get(@"Backgrounds/Drawings/background.png")
|
|
||||||
},
|
},
|
||||||
// Visualiser
|
// Visualiser
|
||||||
new VisualiserContainer
|
new VisualiserContainer
|
||||||
|
@ -298,10 +298,10 @@ namespace osu.Game.Tournament.Screens.Editors
|
|||||||
}, true);
|
}, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updatePanel()
|
private void updatePanel() => Scheduler.AddOnce(() =>
|
||||||
{
|
{
|
||||||
drawableContainer.Child = new UserGridPanel(user.ToAPIUser()) { Width = 300 };
|
drawableContainer.Child = new UserGridPanel(user.ToAPIUser()) { Width = 300 };
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Tournament.Screens.Editors
|
namespace osu.Game.Tournament.Screens.Editors
|
||||||
{
|
{
|
||||||
public abstract class TournamentEditorScreen<TDrawable, TModel> : TournamentScreen, IProvideVideo
|
public abstract class TournamentEditorScreen<TDrawable, TModel> : TournamentScreen
|
||||||
where TDrawable : Drawable, IModelBacked<TModel>
|
where TDrawable : Drawable, IModelBacked<TModel>
|
||||||
where TModel : class, new()
|
where TModel : class, new()
|
||||||
{
|
{
|
||||||
|
@ -16,6 +16,10 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
|
|||||||
{
|
{
|
||||||
private readonly TeamScore score;
|
private readonly TeamScore score;
|
||||||
|
|
||||||
|
private readonly TournamentSpriteTextWithBackground teamText;
|
||||||
|
|
||||||
|
private readonly Bindable<string> teamName = new Bindable<string>("???");
|
||||||
|
|
||||||
private bool showScore;
|
private bool showScore;
|
||||||
|
|
||||||
public bool ShowScore
|
public bool ShowScore
|
||||||
@ -93,7 +97,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
new TournamentSpriteTextWithBackground(team?.FullName.Value ?? "???")
|
teamText = new TournamentSpriteTextWithBackground
|
||||||
{
|
{
|
||||||
Scale = new Vector2(0.5f),
|
Scale = new Vector2(0.5f),
|
||||||
Origin = anchor,
|
Origin = anchor,
|
||||||
@ -113,6 +117,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
|
|||||||
|
|
||||||
updateDisplay();
|
updateDisplay();
|
||||||
FinishTransforms(true);
|
FinishTransforms(true);
|
||||||
|
|
||||||
|
if (Team != null)
|
||||||
|
teamName.BindTo(Team.FullName);
|
||||||
|
|
||||||
|
teamName.BindValueChanged(name => teamText.Text.Text = name.NewValue, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateDisplay()
|
private void updateDisplay()
|
||||||
|
@ -42,6 +42,8 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
|
|||||||
currentMatch.BindTo(ladder.CurrentMatch);
|
currentMatch.BindTo(ladder.CurrentMatch);
|
||||||
currentMatch.BindValueChanged(matchChanged);
|
currentMatch.BindValueChanged(matchChanged);
|
||||||
|
|
||||||
|
currentTeam.BindValueChanged(teamChanged);
|
||||||
|
|
||||||
updateMatch();
|
updateMatch();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,7 +69,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
|
|||||||
|
|
||||||
// team may change to same team, which means score is not in a good state.
|
// team may change to same team, which means score is not in a good state.
|
||||||
// thus we handle this manually.
|
// thus we handle this manually.
|
||||||
teamChanged(currentTeam.Value);
|
currentTeam.TriggerChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e)
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
@ -88,11 +90,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
|
|||||||
return base.OnMouseDown(e);
|
return base.OnMouseDown(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void teamChanged(TournamentTeam team)
|
private void teamChanged(ValueChangedEvent<TournamentTeam> team)
|
||||||
{
|
{
|
||||||
InternalChildren = new Drawable[]
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
teamDisplay = new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0),
|
teamDisplay = new TeamDisplay(team.NewValue, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,7 +21,7 @@ using osuTK.Graphics;
|
|||||||
|
|
||||||
namespace osu.Game.Tournament.Screens.Gameplay
|
namespace osu.Game.Tournament.Screens.Gameplay
|
||||||
{
|
{
|
||||||
public class GameplayScreen : BeatmapInfoScreen, IProvideVideo
|
public class GameplayScreen : BeatmapInfoScreen
|
||||||
{
|
{
|
||||||
private readonly BindableBool warmup = new BindableBool();
|
private readonly BindableBool warmup = new BindableBool();
|
||||||
|
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace osu.Game.Tournament.Screens
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Marker interface for a screen which provides its own local video background.
|
|
||||||
/// </summary>
|
|
||||||
public interface IProvideVideo
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
@ -53,6 +53,9 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
|
|||||||
|
|
||||||
editorInfo.Selected.ValueChanged += selection =>
|
editorInfo.Selected.ValueChanged += selection =>
|
||||||
{
|
{
|
||||||
|
// ensure any ongoing edits are committed out to the *current* selection before changing to a new one.
|
||||||
|
GetContainingInputManager().TriggerFocusContention(null);
|
||||||
|
|
||||||
roundDropdown.Current = selection.NewValue?.Round;
|
roundDropdown.Current = selection.NewValue?.Round;
|
||||||
losersCheckbox.Current = selection.NewValue?.Losers;
|
losersCheckbox.Current = selection.NewValue?.Losers;
|
||||||
dateTimeBox.Current = selection.NewValue?.Date;
|
dateTimeBox.Current = selection.NewValue?.Date;
|
||||||
|
@ -19,7 +19,7 @@ using osuTK.Graphics;
|
|||||||
|
|
||||||
namespace osu.Game.Tournament.Screens.Ladder
|
namespace osu.Game.Tournament.Screens.Ladder
|
||||||
{
|
{
|
||||||
public class LadderScreen : TournamentScreen, IProvideVideo
|
public class LadderScreen : TournamentScreen
|
||||||
{
|
{
|
||||||
protected Container<DrawableTournamentMatch> MatchesContainer;
|
protected Container<DrawableTournamentMatch> MatchesContainer;
|
||||||
private Container<Path> paths;
|
private Container<Path> paths;
|
||||||
|
@ -19,7 +19,7 @@ using osuTK.Graphics;
|
|||||||
|
|
||||||
namespace osu.Game.Tournament.Screens.Schedule
|
namespace osu.Game.Tournament.Screens.Schedule
|
||||||
{
|
{
|
||||||
public class ScheduleScreen : TournamentScreen // IProvidesVideo
|
public class ScheduleScreen : TournamentScreen
|
||||||
{
|
{
|
||||||
private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>();
|
private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>();
|
||||||
private Container mainContainer;
|
private Container mainContainer;
|
||||||
|
@ -9,6 +9,8 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Configuration;
|
using osu.Framework.Configuration;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.UserInterfaceV2;
|
using osu.Game.Graphics.UserInterfaceV2;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
@ -19,7 +21,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Tournament.Screens.Setup
|
namespace osu.Game.Tournament.Screens.Setup
|
||||||
{
|
{
|
||||||
public class SetupScreen : TournamentScreen, IProvideVideo
|
public class SetupScreen : TournamentScreen
|
||||||
{
|
{
|
||||||
private FillFlowContainer fillFlow;
|
private FillFlowContainer fillFlow;
|
||||||
|
|
||||||
@ -48,13 +50,21 @@ namespace osu.Game.Tournament.Screens.Setup
|
|||||||
{
|
{
|
||||||
windowSize = frameworkConfig.GetBindable<Size>(FrameworkSetting.WindowedSize);
|
windowSize = frameworkConfig.GetBindable<Size>(FrameworkSetting.WindowedSize);
|
||||||
|
|
||||||
InternalChild = fillFlow = new FillFlowContainer
|
InternalChildren = new Drawable[]
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
new Box
|
||||||
AutoSizeAxes = Axes.Y,
|
{
|
||||||
Direction = FillDirection.Vertical,
|
RelativeSizeAxes = Axes.Both,
|
||||||
Padding = new MarginPadding(10),
|
Colour = OsuColour.Gray(0.2f),
|
||||||
Spacing = new Vector2(10),
|
},
|
||||||
|
fillFlow = new FillFlowContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Direction = FillDirection.Vertical,
|
||||||
|
Padding = new MarginPadding(10),
|
||||||
|
Spacing = new Vector2(10),
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
api.LocalUser.BindValueChanged(_ => Schedule(reload));
|
api.LocalUser.BindValueChanged(_ => Schedule(reload));
|
||||||
@ -74,7 +84,8 @@ namespace osu.Game.Tournament.Screens.Setup
|
|||||||
Action = () => sceneManager?.SetScreen(new StablePathSelectScreen()),
|
Action = () => sceneManager?.SetScreen(new StablePathSelectScreen()),
|
||||||
Value = fileBasedIpc?.IPCStorage?.GetFullPath(string.Empty) ?? "Not found",
|
Value = fileBasedIpc?.IPCStorage?.GetFullPath(string.Empty) ?? "Not found",
|
||||||
Failing = fileBasedIpc?.IPCStorage == null,
|
Failing = fileBasedIpc?.IPCStorage == null,
|
||||||
Description = "The osu!stable installation which is currently being used as a data source. If a source is not found, make sure you have created an empty ipc.txt in your stable cutting-edge installation."
|
Description =
|
||||||
|
"The osu!stable installation which is currently being used as a data source. If a source is not found, make sure you have created an empty ipc.txt in your stable cutting-edge installation."
|
||||||
},
|
},
|
||||||
new ActionableInfo
|
new ActionableInfo
|
||||||
{
|
{
|
||||||
|
@ -14,7 +14,7 @@ using osuTK.Graphics;
|
|||||||
|
|
||||||
namespace osu.Game.Tournament.Screens.Showcase
|
namespace osu.Game.Tournament.Screens.Showcase
|
||||||
{
|
{
|
||||||
public class ShowcaseScreen : BeatmapInfoScreen // IProvideVideo
|
public class ShowcaseScreen : BeatmapInfoScreen
|
||||||
{
|
{
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
@ -19,7 +20,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Tournament.Screens.TeamIntro
|
namespace osu.Game.Tournament.Screens.TeamIntro
|
||||||
{
|
{
|
||||||
public class SeedingScreen : TournamentMatchScreen, IProvideVideo
|
public class SeedingScreen : TournamentMatchScreen
|
||||||
{
|
{
|
||||||
private Container mainContainer;
|
private Container mainContainer;
|
||||||
|
|
||||||
@ -69,7 +70,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
|
|||||||
currentTeam.BindValueChanged(teamChanged, true);
|
currentTeam.BindValueChanged(teamChanged, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void teamChanged(ValueChangedEvent<TournamentTeam> team)
|
private void teamChanged(ValueChangedEvent<TournamentTeam> team) => Scheduler.AddOnce(() =>
|
||||||
{
|
{
|
||||||
if (team.NewValue == null)
|
if (team.NewValue == null)
|
||||||
{
|
{
|
||||||
@ -78,7 +79,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
|
|||||||
}
|
}
|
||||||
|
|
||||||
showTeam(team.NewValue);
|
showTeam(team.NewValue);
|
||||||
}
|
});
|
||||||
|
|
||||||
protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match)
|
protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match)
|
||||||
{
|
{
|
||||||
@ -120,8 +121,14 @@ namespace osu.Game.Tournament.Screens.TeamIntro
|
|||||||
foreach (var seeding in team.SeedingResults)
|
foreach (var seeding in team.SeedingResults)
|
||||||
{
|
{
|
||||||
fill.Add(new ModRow(seeding.Mod.Value, seeding.Seed.Value));
|
fill.Add(new ModRow(seeding.Mod.Value, seeding.Seed.Value));
|
||||||
|
|
||||||
foreach (var beatmap in seeding.Beatmaps)
|
foreach (var beatmap in seeding.Beatmaps)
|
||||||
|
{
|
||||||
|
if (beatmap.Beatmap == null)
|
||||||
|
continue;
|
||||||
|
|
||||||
fill.Add(new BeatmapScoreRow(beatmap));
|
fill.Add(new BeatmapScoreRow(beatmap));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -129,6 +136,8 @@ namespace osu.Game.Tournament.Screens.TeamIntro
|
|||||||
{
|
{
|
||||||
public BeatmapScoreRow(SeedingBeatmap beatmap)
|
public BeatmapScoreRow(SeedingBeatmap beatmap)
|
||||||
{
|
{
|
||||||
|
Debug.Assert(beatmap.Beatmap != null);
|
||||||
|
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
AutoSizeAxes = Axes.Y;
|
AutoSizeAxes = Axes.Y;
|
||||||
|
|
||||||
@ -157,7 +166,8 @@ namespace osu.Game.Tournament.Screens.TeamIntro
|
|||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new TournamentSpriteText { Text = beatmap.Score.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Width = 80 },
|
new TournamentSpriteText { Text = beatmap.Score.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Width = 80 },
|
||||||
new TournamentSpriteText { Text = "#" + beatmap.Seed.Value.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) },
|
new TournamentSpriteText
|
||||||
|
{ Text = "#" + beatmap.Seed.Value.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) },
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -13,7 +13,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Tournament.Screens.TeamIntro
|
namespace osu.Game.Tournament.Screens.TeamIntro
|
||||||
{
|
{
|
||||||
public class TeamIntroScreen : TournamentMatchScreen, IProvideVideo
|
public class TeamIntroScreen : TournamentMatchScreen
|
||||||
{
|
{
|
||||||
private Container mainContainer;
|
private Container mainContainer;
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ using osuTK;
|
|||||||
|
|
||||||
namespace osu.Game.Tournament.Screens.TeamWin
|
namespace osu.Game.Tournament.Screens.TeamWin
|
||||||
{
|
{
|
||||||
public class TeamWinScreen : TournamentMatchScreen, IProvideVideo
|
public class TeamWinScreen : TournamentMatchScreen
|
||||||
{
|
{
|
||||||
private Container mainContainer;
|
private Container mainContainer;
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ namespace osu.Game.Tournament.Screens.TeamWin
|
|||||||
|
|
||||||
private bool firstDisplay = true;
|
private bool firstDisplay = true;
|
||||||
|
|
||||||
private void update() => Schedule(() =>
|
private void update() => Scheduler.AddOnce(() =>
|
||||||
{
|
{
|
||||||
var match = CurrentMatch.Value;
|
var match = CurrentMatch.Value;
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ using osu.Framework.Graphics;
|
|||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
using osu.Framework.Graphics.Shapes;
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Framework.Testing;
|
||||||
using osu.Framework.Threading;
|
using osu.Framework.Threading;
|
||||||
using osu.Game.Graphics;
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
@ -186,7 +187,7 @@ namespace osu.Game.Tournament
|
|||||||
var lastScreen = currentScreen;
|
var lastScreen = currentScreen;
|
||||||
currentScreen = target;
|
currentScreen = target;
|
||||||
|
|
||||||
if (currentScreen is IProvideVideo)
|
if (currentScreen.ChildrenOfType<TourneyVideo>().FirstOrDefault()?.VideoAvailable == true)
|
||||||
{
|
{
|
||||||
video.FadeOut(200);
|
video.FadeOut(200);
|
||||||
|
|
||||||
|
@ -31,12 +31,11 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
protected override string[] HashableFileTypes => new[] { ".osu" };
|
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)
|
: base(storage, realm)
|
||||||
{
|
{
|
||||||
this.beatmapUpdater = beatmapUpdater;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path).ToLowerInvariant() == ".osz";
|
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path).ToLowerInvariant() == ".osz";
|
||||||
@ -100,7 +99,7 @@ namespace osu.Game.Beatmaps
|
|||||||
{
|
{
|
||||||
base.PostImport(model, realm);
|
base.PostImport(model, realm);
|
||||||
|
|
||||||
beatmapUpdater?.Process(model);
|
ProcessBeatmap?.Invoke(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validateOnlineIds(BeatmapSetInfo beatmapSet, Realm realm)
|
private void validateOnlineIds(BeatmapSetInfo beatmapSet, Realm realm)
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
@ -110,6 +111,11 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
public bool SamplesMatchPlaybackRate { get; set; } = true;
|
public bool SamplesMatchPlaybackRate { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The time at which this beatmap was last played by the local user.
|
||||||
|
/// </summary>
|
||||||
|
public DateTimeOffset? LastPlayed { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The ratio of distance travelled per time unit.
|
/// The ratio of distance travelled per time unit.
|
||||||
/// Generally used to decouple the spacing between hit objects from the enforced "velocity" of the beatmap (see <see cref="DifficultyControlPoint.SliderVelocity"/>).
|
/// Generally used to decouple the spacing between hit objects from the enforced "velocity" of the beatmap (see <see cref="DifficultyControlPoint.SliderVelocity"/>).
|
||||||
@ -151,14 +157,23 @@ namespace osu.Game.Beatmaps
|
|||||||
public bool AudioEquals(BeatmapInfo? other) => other != null
|
public bool AudioEquals(BeatmapInfo? other) => other != null
|
||||||
&& BeatmapSet != null
|
&& BeatmapSet != null
|
||||||
&& other.BeatmapSet != null
|
&& other.BeatmapSet != null
|
||||||
&& BeatmapSet.Hash == other.BeatmapSet.Hash
|
&& compareFiles(this, other, m => m.AudioFile);
|
||||||
&& Metadata.AudioFile == other.Metadata.AudioFile;
|
|
||||||
|
|
||||||
public bool BackgroundEquals(BeatmapInfo? other) => other != null
|
public bool BackgroundEquals(BeatmapInfo? other) => other != null
|
||||||
&& BeatmapSet != null
|
&& BeatmapSet != null
|
||||||
&& other.BeatmapSet != null
|
&& other.BeatmapSet != null
|
||||||
&& BeatmapSet.Hash == other.BeatmapSet.Hash
|
&& compareFiles(this, other, m => m.BackgroundFile);
|
||||||
&& Metadata.BackgroundFile == other.Metadata.BackgroundFile;
|
|
||||||
|
private static bool compareFiles(BeatmapInfo x, BeatmapInfo y, Func<IBeatmapMetadataInfo, string> getFilename)
|
||||||
|
{
|
||||||
|
Debug.Assert(x.BeatmapSet != null);
|
||||||
|
Debug.Assert(y.BeatmapSet != null);
|
||||||
|
|
||||||
|
string? fileHashX = x.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(x.BeatmapSet.Metadata))?.File.Hash;
|
||||||
|
string? fileHashY = y.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(y.BeatmapSet.Metadata))?.File.Hash;
|
||||||
|
|
||||||
|
return fileHashX == fileHashY;
|
||||||
|
}
|
||||||
|
|
||||||
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
|
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
|
||||||
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
|
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
|
||||||
|
@ -34,14 +34,15 @@ namespace osu.Game.Beatmaps
|
|||||||
/// Handles general operations related to global beatmap management.
|
/// Handles general operations related to global beatmap management.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[ExcludeFromDynamicCompile]
|
[ExcludeFromDynamicCompile]
|
||||||
public class BeatmapManager : ModelManager<BeatmapSetInfo>, IModelImporter<BeatmapSetInfo>, IWorkingBeatmapCache, IDisposable
|
public class BeatmapManager : ModelManager<BeatmapSetInfo>, IModelImporter<BeatmapSetInfo>, IWorkingBeatmapCache
|
||||||
{
|
{
|
||||||
public ITrackStore BeatmapTrackStore { get; }
|
public ITrackStore BeatmapTrackStore { get; }
|
||||||
|
|
||||||
private readonly BeatmapImporter beatmapImporter;
|
private readonly BeatmapImporter beatmapImporter;
|
||||||
|
|
||||||
private readonly WorkingBeatmapCache workingBeatmapCache;
|
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,
|
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)
|
WorkingBeatmap? defaultBeatmap = null, BeatmapDifficultyCache? difficultyCache = null, bool performOnlineLookups = false)
|
||||||
@ -54,15 +55,14 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
if (difficultyCache == null)
|
if (difficultyCache == null)
|
||||||
throw new ArgumentNullException(nameof(difficultyCache), "Difficulty cache must be provided if online lookups are required.");
|
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;
|
var userResources = new RealmFileStore(realm, storage).Store;
|
||||||
|
|
||||||
BeatmapTrackStore = audioManager.GetTrackStore(userResources);
|
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);
|
beatmapImporter.PostNotification = obj => PostNotification?.Invoke(obj);
|
||||||
|
|
||||||
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host);
|
workingBeatmapCache = CreateWorkingBeatmapCache(audioManager, gameResources, userResources, defaultBeatmap, host);
|
||||||
@ -74,8 +74,7 @@ namespace osu.Game.Beatmaps
|
|||||||
return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host);
|
return new WorkingBeatmapCache(BeatmapTrackStore, audioManager, resources, storage, defaultBeatmap, host);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm, RulesetStore rulesets, BeatmapUpdater? beatmapUpdater) =>
|
protected virtual BeatmapImporter CreateBeatmapImporter(Storage storage, RealmAccess realm) => new BeatmapImporter(storage, realm);
|
||||||
new BeatmapImporter(storage, realm, beatmapUpdater);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create a new beatmap set, backed by a <see cref="BeatmapSetInfo"/> model,
|
/// 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));
|
AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));
|
||||||
|
|
||||||
|
setInfo.Hash = beatmapImporter.ComputeHash(setInfo);
|
||||||
|
|
||||||
Realm.Write(r =>
|
Realm.Write(r =>
|
||||||
{
|
{
|
||||||
var liveBeatmapSet = r.Find<BeatmapSetInfo>(setInfo.ID);
|
var liveBeatmapSet = r.Find<BeatmapSetInfo>(setInfo.ID);
|
||||||
|
|
||||||
setInfo.CopyChangesToRealm(liveBeatmapSet);
|
setInfo.CopyChangesToRealm(liveBeatmapSet);
|
||||||
|
|
||||||
beatmapUpdater?.Process(liveBeatmapSet, r);
|
ProcessBeatmap?.Invoke(liveBeatmapSet);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -468,15 +469,6 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Implementation of IDisposable
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
beatmapUpdater?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Implementation of IPostImports<out BeatmapSetInfo>
|
#region Implementation of IPostImports<out BeatmapSetInfo>
|
||||||
|
|
||||||
public Action<IEnumerable<Live<BeatmapSetInfo>>>? PresentImport
|
public Action<IEnumerable<Live<BeatmapSetInfo>>>? PresentImport
|
||||||
|
@ -10,7 +10,6 @@ using osu.Framework.Platform;
|
|||||||
using osu.Game.Database;
|
using osu.Game.Database;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using Realms;
|
|
||||||
|
|
||||||
namespace osu.Game.Beatmaps
|
namespace osu.Game.Beatmaps
|
||||||
{
|
{
|
||||||
@ -31,6 +30,14 @@ namespace osu.Game.Beatmaps
|
|||||||
onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
|
onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Queue a beatmap for background processing.
|
||||||
|
/// </summary>
|
||||||
|
public void Queue(int beatmapSetId)
|
||||||
|
{
|
||||||
|
// TODO: implement
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Queue a beatmap for background processing.
|
/// Queue a beatmap for background processing.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -44,9 +51,7 @@ namespace osu.Game.Beatmaps
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Run all processing on a beatmap immediately.
|
/// Run all processing on a beatmap immediately.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Process(BeatmapSetInfo beatmapSet) => beatmapSet.Realm.Write(r => Process(beatmapSet, r));
|
public void Process(BeatmapSetInfo beatmapSet) => beatmapSet.Realm.Write(r =>
|
||||||
|
|
||||||
public void Process(BeatmapSetInfo beatmapSet, Realm realm)
|
|
||||||
{
|
{
|
||||||
// Before we use below, we want to invalidate.
|
// Before we use below, we want to invalidate.
|
||||||
workingBeatmapCache.Invalidate(beatmapSet);
|
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.
|
// And invalidate again afterwards as re-fetching the most up-to-date database metadata will be required.
|
||||||
workingBeatmapCache.Invalidate(beatmapSet);
|
workingBeatmapCache.Invalidate(beatmapSet);
|
||||||
}
|
});
|
||||||
|
|
||||||
private double calculateLength(IBeatmap b)
|
private double calculateLength(IBeatmap b)
|
||||||
{
|
{
|
||||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps.Formats
|
|||||||
{
|
{
|
||||||
Section section = Section.General;
|
Section section = Section.General;
|
||||||
|
|
||||||
string line;
|
string? line;
|
||||||
|
|
||||||
while ((line = stream.ReadLine()) != null)
|
while ((line = stream.ReadLine()) != null)
|
||||||
{
|
{
|
||||||
|
@ -137,8 +137,17 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var stream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path))))
|
string fileStorePath = BeatmapSetInfo.GetPathForFile(BeatmapInfo.Path);
|
||||||
return Decoder.GetDecoder<Beatmap>(stream).Decode(stream);
|
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)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@ -154,7 +163,16 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
try
|
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)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@ -173,7 +191,16 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
try
|
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)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@ -192,8 +219,17 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var trackData = GetStream(BeatmapSetInfo.GetPathForFile(Metadata.AudioFile));
|
string fileStorePath = BeatmapSetInfo.GetPathForFile(Metadata.AudioFile);
|
||||||
return trackData == null ? null : new Waveform(trackData);
|
|
||||||
|
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)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@ -211,20 +247,38 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
try
|
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.
|
Stream storyboardFileStream = null;
|
||||||
if (string.IsNullOrEmpty(storyboardFilename))
|
|
||||||
storyboard = decoder.Decode(stream);
|
if (BeatmapSetInfo?.Files.FirstOrDefault(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename is string storyboardFilename)
|
||||||
else
|
|
||||||
{
|
{
|
||||||
using (var secondaryStream = new LineBufferedReader(GetStream(BeatmapSetInfo.GetPathForFile(storyboardFilename))))
|
string storyboardFileStorePath = BeatmapSetInfo?.GetPathForFile(storyboardFilename);
|
||||||
storyboard = decoder.Decode(stream, secondaryStream);
|
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)
|
catch (Exception e)
|
||||||
|
@ -167,6 +167,8 @@ namespace osu.Game.Configuration
|
|||||||
SetDefault(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full);
|
SetDefault(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full);
|
||||||
|
|
||||||
SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f);
|
SetDefault(OsuSetting.EditorWaveformOpacity, 0.25f);
|
||||||
|
|
||||||
|
SetDefault(OsuSetting.LastProcessedMetadataId, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IDictionary<OsuSetting, string> GetLoggableState() =>
|
public IDictionary<OsuSetting, string> GetLoggableState() =>
|
||||||
@ -363,5 +365,6 @@ namespace osu.Game.Configuration
|
|||||||
DiscordRichPresence,
|
DiscordRichPresence,
|
||||||
AutomaticallyDownloadWhenSpectating,
|
AutomaticallyDownloadWhenSpectating,
|
||||||
ShowOnlineExplicitContent,
|
ShowOnlineExplicitContent,
|
||||||
|
LastProcessedMetadataId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -132,11 +132,12 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
realm.CreateBackup(Path.Combine(backup_folder, $"client.{backupSuffix}.realm"), realmBlockOperations);
|
realm.CreateBackup(Path.Combine(backup_folder, $"client.{backupSuffix}.realm"));
|
||||||
}
|
}
|
||||||
finally
|
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.
|
// Clean up here so we don't accidentally dispose twice.
|
||||||
realmBlockOperations = null;
|
realmBlockOperations = null;
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,9 @@ using System.Collections.Concurrent;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
|
using osu.Framework.Extensions.TypeExtensions;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Statistics;
|
||||||
|
|
||||||
namespace osu.Game.Database
|
namespace osu.Game.Database
|
||||||
{
|
{
|
||||||
@ -20,8 +22,16 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
private readonly ConcurrentDictionary<TLookup, TValue> cache = new ConcurrentDictionary<TLookup, TValue>();
|
private readonly ConcurrentDictionary<TLookup, TValue> cache = new ConcurrentDictionary<TLookup, TValue>();
|
||||||
|
|
||||||
|
private readonly GlobalStatistic<MemoryCachingStatistics> statistics;
|
||||||
|
|
||||||
protected virtual bool CacheNullValues => true;
|
protected virtual bool CacheNullValues => true;
|
||||||
|
|
||||||
|
protected MemoryCachingComponent()
|
||||||
|
{
|
||||||
|
statistics = GlobalStatistics.Get<MemoryCachingStatistics>(nameof(MemoryCachingComponent<TLookup, TValue>), GetType().ReadableName());
|
||||||
|
statistics.Value = new MemoryCachingStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieve the cached value for the given lookup.
|
/// Retrieve the cached value for the given lookup.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -30,12 +40,20 @@ namespace osu.Game.Database
|
|||||||
protected async Task<TValue> GetAsync([NotNull] TLookup lookup, CancellationToken token = default)
|
protected async Task<TValue> GetAsync([NotNull] TLookup lookup, CancellationToken token = default)
|
||||||
{
|
{
|
||||||
if (CheckExists(lookup, out TValue performance))
|
if (CheckExists(lookup, out TValue performance))
|
||||||
|
{
|
||||||
|
statistics.Value.HitCount++;
|
||||||
return performance;
|
return performance;
|
||||||
|
}
|
||||||
|
|
||||||
var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false);
|
var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false);
|
||||||
|
|
||||||
|
statistics.Value.MissCount++;
|
||||||
|
|
||||||
if (computed != null || CacheNullValues)
|
if (computed != null || CacheNullValues)
|
||||||
|
{
|
||||||
cache[lookup] = computed;
|
cache[lookup] = computed;
|
||||||
|
statistics.Value.Usage = cache.Count;
|
||||||
|
}
|
||||||
|
|
||||||
return computed;
|
return computed;
|
||||||
}
|
}
|
||||||
@ -51,6 +69,8 @@ namespace osu.Game.Database
|
|||||||
if (matchKeyPredicate(kvp.Key))
|
if (matchKeyPredicate(kvp.Key))
|
||||||
cache.TryRemove(kvp.Key, out _);
|
cache.TryRemove(kvp.Key, out _);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
statistics.Value.Usage = cache.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected bool CheckExists([NotNull] TLookup lookup, out TValue value) =>
|
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>
|
/// <param name="token">An optional <see cref="CancellationToken"/> to cancel the operation.</param>
|
||||||
/// <returns>The computed value.</returns>
|
/// <returns>The computed value.</returns>
|
||||||
protected abstract Task<TValue> ComputeValueAsync(TLookup lookup, CancellationToken token = default);
|
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%}";
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -58,15 +58,19 @@ namespace osu.Game.Database
|
|||||||
/// 12 2021-11-24 Add Status to RealmBeatmapSet.
|
/// 12 2021-11-24 Add Status to RealmBeatmapSet.
|
||||||
/// 13 2022-01-13 Final migration of beatmaps and scores to realm (multiple new storage fields).
|
/// 13 2022-01-13 Final migration of beatmaps and scores to realm (multiple new storage fields).
|
||||||
/// 14 2022-03-01 Added BeatmapUserSettings to BeatmapInfo.
|
/// 14 2022-03-01 Added BeatmapUserSettings to BeatmapInfo.
|
||||||
|
/// 15 2022-07-13 Added LastPlayed to BeatmapInfo.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const int schema_version = 14;
|
private const int schema_version = 15;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private readonly SemaphoreSlim realmRetrievalLock = new SemaphoreSlim(1);
|
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>
|
/// <summary>
|
||||||
/// Holds a map of functions registered via <see cref="RegisterCustomSubscription"/> and <see cref="RegisterForNotifications{T}"/> and a coinciding action which when triggered,
|
/// Holds a map of functions registered via <see cref="RegisterCustomSubscription"/> and <see cref="RegisterForNotifications{T}"/> and a coinciding action which when triggered,
|
||||||
@ -184,14 +188,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 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))
|
if (!storage.Exists(newerVersionFilename))
|
||||||
CreateBackup(newerVersionFilename);
|
createBackup(newerVersionFilename);
|
||||||
|
|
||||||
storage.Delete(Filename);
|
storage.Delete(Filename);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Logger.Error(e, "Realm startup failed with unrecoverable error; starting with a fresh database. A backup of your database has been made.");
|
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);
|
storage.Delete(Filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,7 +240,7 @@ namespace osu.Game.Database
|
|||||||
}
|
}
|
||||||
|
|
||||||
// For extra safety, also store the temporarily-used database which we are about to replace.
|
// 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);
|
storage.Delete(Filename);
|
||||||
|
|
||||||
@ -584,10 +588,11 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
try
|
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();
|
realmRetrievalLock.Wait();
|
||||||
currentThreadCanCreateRealmInstances.Value = true;
|
currentThreadHasRealmRetrievalLock.Value = true;
|
||||||
tookSemaphoreLock = true;
|
tookSemaphoreLock = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -611,7 +616,7 @@ namespace osu.Game.Database
|
|||||||
if (tookSemaphoreLock)
|
if (tookSemaphoreLock)
|
||||||
{
|
{
|
||||||
realmRetrievalLock.Release();
|
realmRetrievalLock.Release();
|
||||||
currentThreadCanCreateRealmInstances.Value = false;
|
currentThreadHasRealmRetrievalLock.Value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -778,28 +783,37 @@ namespace osu.Game.Database
|
|||||||
private string? getRulesetShortNameFromLegacyID(long rulesetId) =>
|
private string? getRulesetShortNameFromLegacyID(long rulesetId) =>
|
||||||
efContextFactory?.Get().RulesetInfo.FirstOrDefault(r => r.ID == rulesetId)?.ShortName;
|
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);
|
try
|
||||||
|
|
||||||
int attempts = 10;
|
|
||||||
|
|
||||||
while (attempts-- > 0)
|
|
||||||
{
|
{
|
||||||
try
|
using (var source = storage.GetStream(Filename, mode: FileMode.Open))
|
||||||
{
|
using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew))
|
||||||
using (var source = storage.GetStream(Filename, mode: FileMode.Open))
|
source.CopyTo(destination);
|
||||||
using (var destination = storage.GetStream(backupFilename, FileAccess.Write, FileMode.CreateNew))
|
return;
|
||||||
source.CopyTo(destination);
|
}
|
||||||
return;
|
catch (IOException)
|
||||||
}
|
{
|
||||||
catch (IOException)
|
// file may be locked during use.
|
||||||
{
|
Thread.Sleep(500);
|
||||||
// file may be locked during use.
|
|
||||||
Thread.Sleep(500);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -907,16 +921,39 @@ namespace osu.Game.Database
|
|||||||
|
|
||||||
void restoreOperation()
|
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);
|
Logger.Log(@"Restoring realm operations.", LoggingTarget.Database);
|
||||||
realmRetrievalLock.Release();
|
realmRetrievalLock.Release();
|
||||||
|
|
||||||
|
if (syncContext == null) return;
|
||||||
|
|
||||||
|
ManualResetEventSlim updateRealmReestablished = new ManualResetEventSlim();
|
||||||
|
|
||||||
// Post back to the update thread to revive any subscriptions.
|
// 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.
|
// 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.
|
// This requirement is mostly due to test coverage, but shouldn't cause any harm.
|
||||||
if (ThreadSafety.IsUpdateThread)
|
if (ThreadSafety.IsUpdateThread)
|
||||||
syncContext?.Send(_ => ensureUpdateRealm(), null);
|
{
|
||||||
|
syncContext.Send(_ =>
|
||||||
|
{
|
||||||
|
ensureUpdateRealm();
|
||||||
|
updateRealmReestablished.Set();
|
||||||
|
}, null);
|
||||||
|
}
|
||||||
else
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -258,15 +258,13 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
bool checkedExisting = false;
|
TModel? existing;
|
||||||
TModel? existing = null;
|
|
||||||
|
|
||||||
if (batchImport && archive != null)
|
if (batchImport && archive != null)
|
||||||
{
|
{
|
||||||
// this is a fast bail condition to improve large import performance.
|
// this is a fast bail condition to improve large import performance.
|
||||||
item.Hash = computeHashFast(archive);
|
item.Hash = computeHashFast(archive);
|
||||||
|
|
||||||
checkedExisting = true;
|
|
||||||
existing = CheckForExisting(item, realm);
|
existing = CheckForExisting(item, realm);
|
||||||
|
|
||||||
if (existing != null)
|
if (existing != null)
|
||||||
@ -311,8 +309,12 @@ namespace osu.Game.Database
|
|||||||
// TODO: we may want to run this outside of the transaction.
|
// TODO: we may want to run this outside of the transaction.
|
||||||
Populate(item, archive, realm, cancellationToken);
|
Populate(item, archive, realm, cancellationToken);
|
||||||
|
|
||||||
if (!checkedExisting)
|
// Populate() may have adjusted file content (see SkinImporter.updateSkinIniMetadata), so regardless of whether a fast check was done earlier, let's
|
||||||
existing = CheckForExisting(item, realm);
|
// 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)
|
if (existing != null)
|
||||||
{
|
{
|
||||||
@ -336,11 +338,11 @@ namespace osu.Game.Database
|
|||||||
// import to store
|
// import to store
|
||||||
realm.Add(item);
|
realm.Add(item);
|
||||||
|
|
||||||
|
PostImport(item, realm);
|
||||||
|
|
||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
PostImport(item, realm);
|
|
||||||
|
|
||||||
LogForModel(item, @"Import successfully completed!");
|
LogForModel(item, @"Import successfully completed!");
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
@ -386,7 +388,7 @@ namespace osu.Game.Database
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// In the case of no matching files, a hash will be generated from the passed archive's <see cref="ArchiveReader.Name"/>.
|
/// In the case of no matching files, a hash will be generated from the passed archive's <see cref="ArchiveReader.Name"/>.
|
||||||
/// </remarks>
|
/// </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.
|
// for now, concatenate all hashable files in the set to create a unique hash.
|
||||||
MemoryStream hashable = new MemoryStream();
|
MemoryStream hashable = new MemoryStream();
|
||||||
@ -477,7 +479,7 @@ namespace osu.Game.Database
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
/// <param name="model">The model prepared for import.</param>
|
/// <param name="model">The model prepared for import.</param>
|
||||||
/// <param name="realm">The current realm context.</param>
|
/// <param name="realm">The current realm context.</param>
|
||||||
|
@ -8,19 +8,60 @@ namespace osu.Game.Database
|
|||||||
{
|
{
|
||||||
public static class RealmExtensions
|
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)
|
public static void Write(this Realm realm, Action<Realm> function)
|
||||||
{
|
{
|
||||||
using var transaction = realm.BeginWrite();
|
Transaction? transaction = null;
|
||||||
function(realm);
|
|
||||||
transaction.Commit();
|
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)
|
public static T Write<T>(this Realm realm, Func<Realm, T> function)
|
||||||
{
|
{
|
||||||
using var transaction = realm.BeginWrite();
|
Transaction? transaction = null;
|
||||||
var result = function(realm);
|
|
||||||
transaction.Commit();
|
try
|
||||||
return result;
|
{
|
||||||
|
if (!realm.IsInTransaction)
|
||||||
|
transaction = realm.BeginWrite();
|
||||||
|
|
||||||
|
var result = function(realm);
|
||||||
|
|
||||||
|
transaction?.Commit();
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
transaction?.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -38,15 +38,21 @@ namespace osu.Game.Graphics.Backgrounds
|
|||||||
private void load(OsuConfigManager config, SessionStatics sessionStatics)
|
private void load(OsuConfigManager config, SessionStatics sessionStatics)
|
||||||
{
|
{
|
||||||
seasonalBackgroundMode = config.GetBindable<SeasonalBackgroundMode>(OsuSetting.SeasonalBackgroundMode);
|
seasonalBackgroundMode = config.GetBindable<SeasonalBackgroundMode>(OsuSetting.SeasonalBackgroundMode);
|
||||||
seasonalBackgroundMode.BindValueChanged(_ => SeasonalBackgroundChanged?.Invoke());
|
seasonalBackgroundMode.BindValueChanged(_ => triggerSeasonalBackgroundChanged());
|
||||||
|
|
||||||
seasonalBackgrounds = sessionStatics.GetBindable<APISeasonalBackgrounds>(Static.SeasonalBackgrounds);
|
seasonalBackgrounds = sessionStatics.GetBindable<APISeasonalBackgrounds>(Static.SeasonalBackgrounds);
|
||||||
seasonalBackgrounds.BindValueChanged(_ => SeasonalBackgroundChanged?.Invoke());
|
seasonalBackgrounds.BindValueChanged(_ => triggerSeasonalBackgroundChanged());
|
||||||
|
|
||||||
apiState.BindTo(api.State);
|
apiState.BindTo(api.State);
|
||||||
apiState.BindValueChanged(fetchSeasonalBackgrounds, true);
|
apiState.BindValueChanged(fetchSeasonalBackgrounds, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void triggerSeasonalBackgroundChanged()
|
||||||
|
{
|
||||||
|
if (shouldShowSeasonal)
|
||||||
|
SeasonalBackgroundChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
private void fetchSeasonalBackgrounds(ValueChangedEvent<APIState> stateChanged)
|
private void fetchSeasonalBackgrounds(ValueChangedEvent<APIState> stateChanged)
|
||||||
{
|
{
|
||||||
if (seasonalBackgrounds.Value != null || stateChanged.NewValue != APIState.Online)
|
if (seasonalBackgrounds.Value != null || stateChanged.NewValue != APIState.Online)
|
||||||
@ -64,15 +70,10 @@ namespace osu.Game.Graphics.Backgrounds
|
|||||||
|
|
||||||
public SeasonalBackground LoadNextBackground()
|
public SeasonalBackground LoadNextBackground()
|
||||||
{
|
{
|
||||||
if (seasonalBackgroundMode.Value == SeasonalBackgroundMode.Never
|
if (!shouldShowSeasonal)
|
||||||
|| (seasonalBackgroundMode.Value == SeasonalBackgroundMode.Sometimes && !isInSeason))
|
|
||||||
{
|
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
var backgrounds = seasonalBackgrounds.Value?.Backgrounds;
|
var backgrounds = seasonalBackgrounds.Value.Backgrounds;
|
||||||
if (backgrounds == null || !backgrounds.Any())
|
|
||||||
return null;
|
|
||||||
|
|
||||||
current = (current + 1) % backgrounds.Count;
|
current = (current + 1) % backgrounds.Count;
|
||||||
string url = backgrounds[current].Url;
|
string url = backgrounds[current].Url;
|
||||||
@ -80,6 +81,20 @@ namespace osu.Game.Graphics.Backgrounds
|
|||||||
return new SeasonalBackground(url);
|
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;
|
private bool isInSeason => seasonalBackgrounds.Value != null && DateTimeOffset.Now < seasonalBackgrounds.Value.EndDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ namespace osu.Game.Graphics.Containers
|
|||||||
TimingControlPoint timingPoint;
|
TimingControlPoint timingPoint;
|
||||||
EffectControlPoint effectPoint;
|
EffectControlPoint effectPoint;
|
||||||
|
|
||||||
IsBeatSyncedWithTrack = BeatSyncSource.Clock?.IsRunning == true;
|
IsBeatSyncedWithTrack = BeatSyncSource.Clock?.IsRunning == true && BeatSyncSource.ControlPoints != null;
|
||||||
|
|
||||||
double currentTrackTime;
|
double currentTrackTime;
|
||||||
|
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
@ -17,34 +14,31 @@ namespace osu.Game.IO
|
|||||||
public class LineBufferedReader : IDisposable
|
public class LineBufferedReader : IDisposable
|
||||||
{
|
{
|
||||||
private readonly StreamReader streamReader;
|
private readonly StreamReader streamReader;
|
||||||
private readonly Queue<string> lineBuffer;
|
|
||||||
|
private string? peekedLine;
|
||||||
|
|
||||||
public LineBufferedReader(Stream stream, bool leaveOpen = false)
|
public LineBufferedReader(Stream stream, bool leaveOpen = false)
|
||||||
{
|
{
|
||||||
streamReader = new StreamReader(stream, Encoding.UTF8, true, 1024, leaveOpen);
|
streamReader = new StreamReader(stream, Encoding.UTF8, true, 1024, leaveOpen);
|
||||||
lineBuffer = new Queue<string>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads the next line from the stream without consuming it.
|
/// 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.
|
/// Subsequent calls to <see cref="PeekLine"/> without a <see cref="ReadLine"/> will return the same string.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string PeekLine()
|
public string? PeekLine() => peekedLine ??= streamReader.ReadLine();
|
||||||
{
|
|
||||||
if (lineBuffer.Count > 0)
|
|
||||||
return lineBuffer.Peek();
|
|
||||||
|
|
||||||
string line = streamReader.ReadLine();
|
|
||||||
if (line != null)
|
|
||||||
lineBuffer.Enqueue(line);
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads the next line from the stream and consumes it.
|
/// Reads the next line from the stream and consumes it.
|
||||||
/// If a line was peeked, that same line will then be consumed and returned.
|
/// If a line was peeked, that same line will then be consumed and returned.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string ReadLine() => lineBuffer.Count > 0 ? lineBuffer.Dequeue() : streamReader.ReadLine();
|
public string? ReadLine()
|
||||||
|
{
|
||||||
|
string? line = peekedLine ?? streamReader.ReadLine();
|
||||||
|
|
||||||
|
peekedLine = null;
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads the stream to its end and returns the text read.
|
/// Reads the stream to its end and returns the text read.
|
||||||
@ -53,14 +47,13 @@ namespace osu.Game.IO
|
|||||||
public string ReadToEnd()
|
public string ReadToEnd()
|
||||||
{
|
{
|
||||||
string remainingText = streamReader.ReadToEnd();
|
string remainingText = streamReader.ReadToEnd();
|
||||||
if (lineBuffer.Count == 0)
|
if (peekedLine == null)
|
||||||
return remainingText;
|
return remainingText;
|
||||||
|
|
||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
|
|
||||||
// this might not be completely correct due to varying platform line endings
|
// this might not be completely correct due to varying platform line endings
|
||||||
while (lineBuffer.Count > 0)
|
builder.AppendLine(peekedLine);
|
||||||
builder.AppendLine(lineBuffer.Dequeue());
|
|
||||||
builder.Append(remainingText);
|
builder.Append(remainingText);
|
||||||
|
|
||||||
return builder.ToString();
|
return builder.ToString();
|
||||||
@ -68,7 +61,7 @@ namespace osu.Game.IO
|
|||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
streamReader?.Dispose();
|
streamReader.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,6 +94,8 @@ namespace osu.Game.IO
|
|||||||
error = OsuStorageError.None;
|
error = OsuStorageError.None;
|
||||||
Storage lastStorage = UnderlyingStorage;
|
Storage lastStorage = UnderlyingStorage;
|
||||||
|
|
||||||
|
Logger.Log($"Attempting to use custom storage location {CustomStoragePath}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Storage userStorage = host.GetStorage(CustomStoragePath);
|
Storage userStorage = host.GetStorage(CustomStoragePath);
|
||||||
@ -102,6 +104,7 @@ namespace osu.Game.IO
|
|||||||
error = OsuStorageError.AccessibleButEmpty;
|
error = OsuStorageError.AccessibleButEmpty;
|
||||||
|
|
||||||
ChangeTargetStorage(userStorage);
|
ChangeTargetStorage(userStorage);
|
||||||
|
Logger.Log($"Storage successfully changed to {CustomStoragePath}.");
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@ -109,6 +112,9 @@ namespace osu.Game.IO
|
|||||||
ChangeTargetStorage(lastStorage);
|
ChangeTargetStorage(lastStorage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error != OsuStorageError.None)
|
||||||
|
Logger.Log($"Custom storage location could not be used ({error}).");
|
||||||
|
|
||||||
return error == OsuStorageError.None;
|
return error == OsuStorageError.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ namespace osu.Game.Online
|
|||||||
APIClientID = "5";
|
APIClientID = "5";
|
||||||
SpectatorEndpointUrl = $"{APIEndpointUrl}/spectator";
|
SpectatorEndpointUrl = $"{APIEndpointUrl}/spectator";
|
||||||
MultiplayerEndpointUrl = $"{APIEndpointUrl}/multiplayer";
|
MultiplayerEndpointUrl = $"{APIEndpointUrl}/multiplayer";
|
||||||
|
MetadataEndpointUrl = $"{APIEndpointUrl}/metadata";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,5 +39,10 @@ namespace osu.Game.Online
|
|||||||
/// The endpoint for the SignalR multiplayer server.
|
/// The endpoint for the SignalR multiplayer server.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string MultiplayerEndpointUrl { get; set; }
|
public string MultiplayerEndpointUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The endpoint for the SignalR metadata server.
|
||||||
|
/// </summary>
|
||||||
|
public string MetadataEndpointUrl { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,7 +144,7 @@ namespace osu.Game.Online
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task handleErrorAndDelay(Exception exception, CancellationToken cancellationToken)
|
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);
|
await Task.Delay(5000, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -426,10 +426,10 @@ namespace osu.Game.Online.Leaderboards
|
|||||||
items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods));
|
items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = Score.Mods));
|
||||||
|
|
||||||
if (Score.Files.Count > 0)
|
if (Score.Files.Count > 0)
|
||||||
|
{
|
||||||
items.Add(new OsuMenuItem("Export", MenuItemType.Standard, () => new LegacyScoreExporter(storage).Export(Score)));
|
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))));
|
items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(Score))));
|
||||||
|
}
|
||||||
|
|
||||||
return items.ToArray();
|
return items.ToArray();
|
||||||
}
|
}
|
||||||
|
28
osu.Game/Online/Metadata/BeatmapUpdates.cs
Normal file
28
osu.Game/Online/Metadata/BeatmapUpdates.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
osu.Game/Online/Metadata/IMetadataClient.cs
Normal file
12
osu.Game/Online/Metadata/IMetadataClient.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
21
osu.Game/Online/Metadata/IMetadataServer.cs
Normal file
21
osu.Game/Online/Metadata/IMetadataServer.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
15
osu.Game/Online/Metadata/MetadataClient.cs
Normal file
15
osu.Game/Online/Metadata/MetadataClient.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
134
osu.Game/Online/Metadata/OnlineMetadataClient.cs
Normal file
134
osu.Game/Online/Metadata/OnlineMetadataClient.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user