1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-13 10:33:07 +08:00

Merge branch 'master' into Save-Score-Failed

This commit is contained in:
Dean Herbert 2022-07-15 18:02:58 +09:00 committed by GitHub
commit ab6665d88c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
117 changed files with 2048 additions and 764 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ using osu.Framework.Platform;
using osu.Game;
using osu.Game.IPC;
using osu.Game.Tournament;
using SDL2;
using Squirrel;
namespace osu.Desktop
@ -29,7 +30,21 @@ namespace osu.Desktop
{
// run Squirrel first, as the app may exit after these run
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();
}
// Back up the cwd before DesktopGameHost changes it
string cwd = Environment.CurrentDirectory;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModRepel : OsuModTestScene
{
[TestCase(0.1f)]
[TestCase(0.5f)]
[TestCase(1)]
public void TestRepel(float strength)
{
CreateModTest(new ModTestData
{
Mod = new OsuModRepel
{
RepulsionStrength = { Value = strength },
},
PassCondition = () => true,
Autoplay = false,
});
}
}
}

View File

@ -0,0 +1,175 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModSingleTap : OsuModTestScene
{
[Test]
public void TestInputSingular() => CreateModTest(new ModTestData
{
Mod = new OsuModSingleTap(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 2,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 500,
Position = new Vector2(100),
},
new HitCircle
{
StartTime = 1000,
Position = new Vector2(200, 100),
},
new HitCircle
{
StartTime = 1500,
Position = new Vector2(300, 100),
},
new HitCircle
{
StartTime = 2000,
Position = new Vector2(400, 100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(100)),
new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.LeftButton),
}
});
[Test]
public void TestInputAlternating() => CreateModTest(new ModTestData
{
Mod = new OsuModSingleTap(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 1,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 500,
Position = new Vector2(100),
},
new HitCircle
{
StartTime = 1000,
Position = new Vector2(200, 100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(100)),
new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.RightButton),
new OsuReplayFrame(1001, new Vector2(200, 100)),
new OsuReplayFrame(1500, new Vector2(300, 100), OsuAction.LeftButton),
new OsuReplayFrame(1501, new Vector2(300, 100)),
new OsuReplayFrame(2000, new Vector2(400, 100), OsuAction.RightButton),
new OsuReplayFrame(2001, new Vector2(400, 100)),
}
});
/// <summary>
/// Ensures singletapping is reset before the first hitobject after intro.
/// </summary>
[Test]
public void TestInputAlternatingAtIntro() => CreateModTest(new ModTestData
{
Mod = new OsuModSingleTap(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 1000,
Position = new Vector2(100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
// first press during intro.
new OsuReplayFrame(500, new Vector2(200), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(200)),
// press different key at hitobject and ensure it has been hit.
new OsuReplayFrame(1000, new Vector2(100), OsuAction.RightButton),
}
});
/// <summary>
/// Ensures singletapping is reset before the first hitobject after a break.
/// </summary>
[Test]
public void TestInputAlternatingWithBreak() => CreateModTest(new ModTestData
{
Mod = new OsuModSingleTap(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 2,
Autoplay = false,
Beatmap = new Beatmap
{
Breaks = new List<BreakPeriod>
{
new BreakPeriod(500, 2000),
},
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 500,
Position = new Vector2(100),
},
new HitCircle
{
StartTime = 2500,
Position = new Vector2(500, 100),
},
new HitCircle
{
StartTime = 3000,
Position = new Vector2(500, 100),
},
}
},
ReplayFrames = new List<ReplayFrame>
{
// first press to start singletap lock.
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(100)),
// press different key after break but before hit object.
new OsuReplayFrame(2250, new Vector2(300, 100), OsuAction.RightButton),
new OsuReplayFrame(2251, new Vector2(300, 100)),
// press same key at second hitobject and ensure it has been hit.
new OsuReplayFrame(2500, new Vector2(500, 100), OsuAction.LeftButton),
new OsuReplayFrame(2501, new Vector2(500, 100)),
// press different key at third hitobject and ensure it has been missed.
new OsuReplayFrame(3000, new Vector2(500, 100), OsuAction.RightButton),
new OsuReplayFrame(3001, new Vector2(500, 100)),
}
});
}
}

View File

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

View File

@ -0,0 +1,114 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Mods
{
public abstract class InputBlockingMod : Mod, IApplicableToDrawableRuleset<OsuHitObject>
{
public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax), typeof(OsuModCinema) };
public override ModType Type => ModType.Conversion;
private const double flash_duration = 1000;
private DrawableRuleset<OsuHitObject> ruleset = null!;
protected OsuAction? LastAcceptedAction { get; private set; }
/// <summary>
/// A tracker for periods where alternate should not be forced (i.e. non-gameplay periods).
/// </summary>
/// <remarks>
/// This is different from <see cref="Player.IsBreakTime"/> in that the periods here end strictly at the first object after the break, rather than the break's end time.
/// </remarks>
private PeriodTracker nonGameplayPeriods = null!;
private IFrameStableClock gameplayClock = null!;
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
ruleset = drawableRuleset;
drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
var periods = new List<Period>();
if (drawableRuleset.Objects.Any())
{
periods.Add(new Period(int.MinValue, getValidJudgementTime(ruleset.Objects.First()) - 1));
foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1));
static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh);
}
nonGameplayPeriods = new PeriodTracker(periods);
gameplayClock = drawableRuleset.FrameStableClock;
}
protected abstract bool CheckValidNewAction(OsuAction action);
private bool checkCorrectAction(OsuAction action)
{
if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
{
LastAcceptedAction = null;
return true;
}
switch (action)
{
case OsuAction.LeftButton:
case OsuAction.RightButton:
break;
// Any action which is not left or right button should be ignored.
default:
return true;
}
if (CheckValidNewAction(action))
{
LastAcceptedAction = action;
return true;
}
ruleset.Cursor.FlashColour(Colour4.Red, flash_duration, Easing.OutQuint);
return false;
}
private class InputInterceptor : Component, IKeyBindingHandler<OsuAction>
{
private readonly InputBlockingMod mod;
public InputInterceptor(InputBlockingMod mod)
{
this.mod = mod;
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
// if the pressed action is incorrect, block it from reaching gameplay.
=> !mod.checkCorrectAction(e.Action);
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{
}
}
}
}

View File

@ -1,119 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAlternate : Mod, IApplicableToDrawableRuleset<OsuHitObject>
public class OsuModAlternate : InputBlockingMod
{
public override string Name => @"Alternate";
public override string Acronym => @"AL";
public override string Description => @"Don't use the same key twice in a row!";
public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModRelax) };
public override ModType Type => ModType.Conversion;
public override IconUsage? Icon => FontAwesome.Solid.Keyboard;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModSingleTap) }).ToArray();
private const double flash_duration = 1000;
/// <summary>
/// A tracker for periods where alternate should not be forced (i.e. non-gameplay periods).
/// </summary>
/// <remarks>
/// This is different from <see cref="Player.IsBreakTime"/> in that the periods here end strictly at the first object after the break, rather than the break's end time.
/// </remarks>
private PeriodTracker nonGameplayPeriods;
private OsuAction? lastActionPressed;
private DrawableRuleset<OsuHitObject> ruleset;
private IFrameStableClock gameplayClock;
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
ruleset = drawableRuleset;
drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
var periods = new List<Period>();
if (drawableRuleset.Objects.Any())
{
periods.Add(new Period(int.MinValue, getValidJudgementTime(ruleset.Objects.First()) - 1));
foreach (BreakPeriod b in drawableRuleset.Beatmap.Breaks)
periods.Add(new Period(b.StartTime, getValidJudgementTime(ruleset.Objects.First(h => h.StartTime >= b.EndTime)) - 1));
static double getValidJudgementTime(HitObject hitObject) => hitObject.StartTime - hitObject.HitWindows.WindowFor(HitResult.Meh);
}
nonGameplayPeriods = new PeriodTracker(periods);
gameplayClock = drawableRuleset.FrameStableClock;
}
private bool checkCorrectAction(OsuAction action)
{
if (nonGameplayPeriods.IsInAny(gameplayClock.CurrentTime))
{
lastActionPressed = null;
return true;
}
switch (action)
{
case OsuAction.LeftButton:
case OsuAction.RightButton:
break;
// Any action which is not left or right button should be ignored.
default:
return true;
}
if (lastActionPressed != action)
{
// User alternated correctly.
lastActionPressed = action;
return true;
}
ruleset.Cursor.FlashColour(Colour4.Red, flash_duration, Easing.OutQuint);
return false;
}
private class InputInterceptor : Component, IKeyBindingHandler<OsuAction>
{
private readonly OsuModAlternate mod;
public InputInterceptor(OsuModAlternate mod)
{
this.mod = mod;
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
// if the pressed action is incorrect, block it from reaching gameplay.
=> !mod.checkCorrectAction(e.Action);
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{
}
}
protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction != action;
}
}

View File

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

View File

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

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModCinema : ModCinema<OsuHitObject>
{
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(OsuModAutopilot), typeof(OsuModSpunOut), typeof(OsuModAlternate), typeof(OsuModSingleTap) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });

View File

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

View File

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

View File

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

View File

@ -0,0 +1,18 @@
// 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;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModSingleTap : InputBlockingMod
{
public override string Name => @"Single Tap";
public override string Acronym => @"ST";
public override string Description => @"You must only use one key!";
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAlternate) }).ToArray();
protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction == null || LastAcceptedAction == action;
}
}

View File

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

View File

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

View File

@ -319,13 +319,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
const float fade_out_time = 450;
// intentionally pile on an extra FadeOut to make it happen much faster.
Ball.FadeOut(fade_out_time / 4, Easing.Out);
switch (state)
{
case ArmedState.Hit:
Ball.ScaleTo(HitObject.Scale * 1.4f, fade_out_time, Easing.Out);
if (SliderBody?.SnakingOut.Value == true)
Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear.
break;

View File

@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour
{
public const float FOLLOW_AREA = 2.4f;
public Func<OsuAction?> GetInitialHitAction;
public Color4 AccentColour
@ -31,7 +33,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
set => ball.Colour = value;
}
private Drawable followCircle;
private Drawable followCircleReceptor;
private DrawableSlider drawableSlider;
private Drawable ball;
@ -47,12 +48,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Children = new[]
{
followCircle = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle())
new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle())
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Alpha = 0,
},
followCircleReceptor = new CircularContainer
{
@ -103,10 +103,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
tracking = value;
followCircleReceptor.Scale = new Vector2(tracking ? 2.4f : 1f);
followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint);
followCircle.FadeTo(tracking ? 1f : 0, 300, Easing.OutQuint);
followCircleReceptor.Scale = new Vector2(tracking ? FOLLOW_AREA : 1f);
}
}

View File

@ -172,7 +172,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModClassic(),
new OsuModRandom(),
new OsuModMirror(),
new OsuModAlternate(),
new MultiMod(new OsuModAlternate(), new OsuModSingleTap())
};
case ModType.Automation:
@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModApproachDifferent(),
new OsuModMuted(),
new OsuModNoScope(),
new OsuModMagnetised(),
new MultiMod(new OsuModMagnetised(), new OsuModRepel()),
new ModAdaptiveSpeed()
};

View File

@ -1,19 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public class DefaultFollowCircle : CompositeDrawable
public class DefaultFollowCircle : FollowCircle
{
public DefaultFollowCircle()
{
RelativeSizeAxes = Axes.Both;
InternalChild = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
@ -29,5 +29,22 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
}
};
}
protected override void OnTrackingChanged(ValueChangedEvent<bool> tracking)
{
const float scale_duration = 300f;
const float fade_duration = 300f;
this.ScaleTo(tracking.NewValue ? DrawableSliderBall.FOLLOW_AREA : 1f, scale_duration, Easing.OutQuint)
.FadeTo(tracking.NewValue ? 1f : 0, fade_duration, Easing.OutQuint);
}
protected override void OnSliderEnd()
{
const float fade_duration = 450f;
// intentionally pile on an extra FadeOut to make it happen much faster
this.FadeOut(fade_duration / 4, Easing.Out);
}
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -19,13 +17,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public class DefaultSliderBall : CompositeDrawable
{
private Box box;
private Box box = null!;
[Resolved(canBeNull: true)]
private DrawableHitObject? parentObject { get; set; }
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject, ISkinSource skin)
private void load(ISkinSource skin)
{
var slider = (DrawableSlider)drawableObject;
RelativeSizeAxes = Axes.Both;
float radius = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS;
@ -51,10 +50,62 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
}
};
slider.Tracking.BindValueChanged(trackingChanged, true);
if (parentObject != null)
{
var slider = (DrawableSlider)parentObject;
slider.Tracking.BindValueChanged(trackingChanged, true);
}
}
protected override void LoadComplete()
{
base.LoadComplete();
if (parentObject != null)
{
parentObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(parentObject, parentObject.State.Value);
}
}
private void trackingChanged(ValueChangedEvent<bool> tracking) =>
box.FadeTo(tracking.NewValue ? 0.3f : 0.05f, 200, Easing.OutQuint);
private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState state)
{
// Gets called by slider ticks, tails, etc., leading to duplicated
// animations which may negatively affect performance
if (drawableObject is not DrawableSlider)
return;
const float fade_duration = 450f;
using (BeginAbsoluteSequence(drawableObject.StateUpdateTime))
{
this.FadeIn()
.ScaleTo(1f);
}
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
{
// intentionally pile on an extra FadeOut to make it happen much faster
this.FadeOut(fade_duration / 4, Easing.Out);
switch (state)
{
case ArmedState.Hit:
this.ScaleTo(1.4f, fade_duration, Easing.Out);
break;
}
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (parentObject != null)
parentObject.ApplyCustomUpdateState -= updateStateTransforms;
}
}
}

View File

@ -0,0 +1,75 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
namespace osu.Game.Rulesets.Osu.Skinning
{
public abstract class FollowCircle : CompositeDrawable
{
[Resolved]
protected DrawableHitObject? ParentObject { get; private set; }
protected FollowCircle()
{
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load()
{
((DrawableSlider?)ParentObject)?.Tracking.BindValueChanged(OnTrackingChanged, true);
}
protected override void LoadComplete()
{
base.LoadComplete();
if (ParentObject != null)
{
ParentObject.HitObjectApplied += onHitObjectApplied;
onHitObjectApplied(ParentObject);
ParentObject.ApplyCustomUpdateState += updateStateTransforms;
updateStateTransforms(ParentObject, ParentObject.State.Value);
}
}
private void onHitObjectApplied(DrawableHitObject drawableObject)
{
this.ScaleTo(1f)
.FadeOut();
}
private void updateStateTransforms(DrawableHitObject drawableObject, ArmedState state)
{
// Gets called by slider ticks, tails, etc., leading to duplicated
// animations which may negatively affect performance
if (drawableObject is not DrawableSlider)
return;
using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
OnSliderEnd();
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (ParentObject != null)
{
ParentObject.HitObjectApplied -= onHitObjectApplied;
ParentObject.ApplyCustomUpdateState -= updateStateTransforms;
}
}
protected abstract void OnTrackingChanged(ValueChangedEvent<bool> tracking);
protected abstract void OnSliderEnd();
}
}

View File

@ -1,12 +1,14 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
public class LegacyFollowCircle : CompositeDrawable
public class LegacyFollowCircle : FollowCircle
{
public LegacyFollowCircle(Drawable animationContent)
{
@ -18,5 +20,36 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
RelativeSizeAxes = Axes.Both;
InternalChild = animationContent;
}
protected override void OnTrackingChanged(ValueChangedEvent<bool> tracking)
{
Debug.Assert(ParentObject != null);
if (ParentObject.Judged)
return;
double remainingTime = ParentObject.HitStateUpdateTime - Time.Current;
// Note that the scale adjust here is 2 instead of DrawableSliderBall.FOLLOW_AREA to match legacy behaviour.
// This means the actual tracking area for gameplay purposes is larger than the sprite (but skins may be accounting for this).
if (tracking.NewValue)
{
// TODO: Follow circle should bounce on each slider tick.
this.ScaleTo(0.5f).ScaleTo(2f, Math.Min(180f, remainingTime), Easing.Out)
.FadeTo(0).FadeTo(1f, Math.Min(60f, remainingTime));
}
else
{
// TODO: Should animate only at the next slider tick if we want to match stable perfectly.
this.ScaleTo(4f, 100)
.FadeTo(0f, 100);
}
}
protected override void OnSliderEnd()
{
this.ScaleTo(1.6f, 200, Easing.Out)
.FadeOut(200, Easing.In);
}
}
}

View File

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

View File

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

View File

@ -4,9 +4,12 @@
#nullable disable
using System;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Models;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.NonVisual
{
@ -23,6 +26,47 @@ namespace osu.Game.Tests.NonVisual
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]
public void TestDatabasedWithDatabased()
{

View File

@ -134,6 +134,7 @@ namespace osu.Game.Tests.Resources
DifficultyName = $"{version} {beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
StarRating = diff,
Length = length,
BeatmapSet = beatmapSet,
BPM = bpm,
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
Ruleset = rulesetInfo,

View File

@ -6,6 +6,7 @@ using System.IO;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Screens;
@ -103,6 +104,8 @@ namespace osu.Game.Tests.Visual.Editing
*/
public void TestAddAudioTrack()
{
AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual);
AddAssert("switch track to real track", () =>
{
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);
}

View File

@ -6,11 +6,13 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Overlays;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Screens.Select;
@ -23,7 +25,9 @@ namespace osu.Game.Tests.Visual.Editing
[Test]
public void TestCantExitWithoutSaving()
{
AddUntilStep("Wait for dialog overlay load", () => ((Drawable)Game.Dependencies.Get<IDialogOverlay>()).IsLoaded);
AddRepeatStep("Exit", () => InputManager.Key(Key.Escape), 10);
AddAssert("Sample playback disabled", () => Editor.SamplePlaybackDisabled.Value);
AddAssert("Editor is still active screen", () => Game.ScreenStack.CurrentScreen is Editor);
}

View File

@ -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);
}
}
}

View File

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

View File

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

View File

@ -18,6 +18,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Database;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Models;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
@ -195,12 +196,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestDownloadButtonHiddenWhenBeatmapExists()
{
var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo;
Live<BeatmapSetInfo> imported = null;
Debug.Assert(beatmap.BeatmapSet != null);
AddStep("import beatmap", () =>
{
var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo;
AddStep("import beatmap", () => imported = manager.Import(beatmap.BeatmapSet));
Debug.Assert(beatmap.BeatmapSet != null);
imported = manager.Import(beatmap.BeatmapSet);
});
createPlaylistWithBeatmaps(() => imported.PerformRead(s => s.Beatmaps.Detach()));
@ -245,40 +249,35 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestExpiredItems()
{
AddStep("create playlist", () =>
createPlaylist(p =>
{
Child = playlist = new TestPlaylist
p.Items.Clear();
p.Items.AddRange(new[]
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500, 300),
Items =
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
ID = 0,
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
Expired = true,
RequiredMods = new[]
{
ID = 0,
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
Expired = true,
RequiredMods = new[]
{
new APIMod(new OsuModHardRock()),
new APIMod(new OsuModDoubleTime()),
new APIMod(new OsuModAutoplay())
}
},
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
new APIMod(new OsuModHardRock()),
new APIMod(new OsuModDoubleTime()),
new APIMod(new OsuModAutoplay())
}
},
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
ID = 1,
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
RequiredMods = new[]
{
ID = 1,
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
RequiredMods = new[]
{
new APIMod(new OsuModHardRock()),
new APIMod(new OsuModDoubleTime()),
new APIMod(new OsuModAutoplay())
}
new APIMod(new OsuModHardRock()),
new APIMod(new OsuModDoubleTime()),
new APIMod(new OsuModAutoplay())
}
}
};
});
});
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
@ -321,19 +320,44 @@ namespace osu.Game.Tests.Visual.Multiplayer
=> AddAssert($"delete button {index} {(visible ? "is" : "is not")} visible",
() => (playlist.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistRemoveButton>().ElementAt(2 + index * 2).Alpha > 0) == visible);
private void createPlaylistWithBeatmaps(Func<IEnumerable<IBeatmapInfo>> beatmaps) => createPlaylist(p =>
{
int index = 0;
p.Items.Clear();
foreach (var b in beatmaps())
{
p.Items.Add(new PlaylistItem(b)
{
ID = index++,
OwnerID = 2,
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
RequiredMods = new[]
{
new APIMod(new OsuModHardRock()),
new APIMod(new OsuModDoubleTime()),
new APIMod(new OsuModAutoplay())
}
});
}
});
private void createPlaylist(Action<TestPlaylist> setupPlaylist = null)
{
AddStep("create playlist", () =>
{
Child = playlist = new TestPlaylist
Child = new OsuContextMenuContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500, 300)
RelativeSizeAxes = Axes.Both,
Child = playlist = new TestPlaylist
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500, 300)
}
};
setupPlaylist?.Invoke(playlist);
for (int i = 0; i < 20; i++)
{
playlist.Items.Add(new PlaylistItem(i % 2 == 1
@ -360,39 +384,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
}
});
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
}
private void createPlaylistWithBeatmaps(Func<IEnumerable<IBeatmapInfo>> beatmaps)
{
AddStep("create playlist", () =>
{
Child = playlist = new TestPlaylist
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500, 300)
};
int index = 0;
foreach (var b in beatmaps())
{
playlist.Items.Add(new PlaylistItem(b)
{
ID = index++,
OwnerID = 2,
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
RequiredMods = new[]
{
new APIMod(new OsuModHardRock()),
new APIMod(new OsuModDoubleTime()),
new APIMod(new OsuModAutoplay())
}
});
}
setupPlaylist?.Invoke(playlist);
});
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
@ -368,12 +369,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
ParticipantsList? participantsList = null;
AddStep("create new list", () => Child = participantsList = new ParticipantsList
AddStep("create new list", () => Child = new OsuContextMenuContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Size = new Vector2(380, 0.7f)
RelativeSizeAxes = Axes.Both,
Child = participantsList = new ParticipantsList
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Size = new Vector2(380, 0.7f)
}
});
AddUntilStep("wait for list to load", () => participantsList?.IsLoaded == true);

View File

@ -18,6 +18,7 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet.Scores;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Users;
using osuTK.Graphics;
@ -146,12 +147,12 @@ namespace osu.Game.Tests.Visual.Online
{
var scores = new APIScoresCollection
{
Scores = new List<APIScore>
Scores = new List<SoloScoreInfo>
{
new APIScore
new SoloScoreInfo
{
Date = DateTimeOffset.Now,
OnlineID = onlineID++,
EndedAt = DateTimeOffset.Now,
ID = onlineID++,
User = new APIUser
{
Id = 6602580,
@ -175,10 +176,10 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234567890,
Accuracy = 1,
},
new APIScore
new SoloScoreInfo
{
Date = DateTimeOffset.Now,
OnlineID = onlineID++,
EndedAt = DateTimeOffset.Now,
ID = onlineID++,
User = new APIUser
{
Id = 4608074,
@ -201,10 +202,10 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234789,
Accuracy = 0.9997,
},
new APIScore
new SoloScoreInfo
{
Date = DateTimeOffset.Now,
OnlineID = onlineID++,
EndedAt = DateTimeOffset.Now,
ID = onlineID++,
User = new APIUser
{
Id = 1014222,
@ -226,10 +227,10 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 12345678,
Accuracy = 0.9854,
},
new APIScore
new SoloScoreInfo
{
Date = DateTimeOffset.Now,
OnlineID = onlineID++,
EndedAt = DateTimeOffset.Now,
ID = onlineID++,
User = new APIUser
{
Id = 1541390,
@ -250,10 +251,10 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234567,
Accuracy = 0.8765,
},
new APIScore
new SoloScoreInfo
{
Date = DateTimeOffset.Now,
OnlineID = onlineID++,
EndedAt = DateTimeOffset.Now,
ID = onlineID++,
User = new APIUser
{
Id = 7151382,
@ -275,12 +276,12 @@ namespace osu.Game.Tests.Visual.Online
foreach (var s in scores.Scores)
{
s.Statistics = new Dictionary<string, int>
s.Statistics = new Dictionary<HitResult, int>
{
{ "count_300", RNG.Next(2000) },
{ "count_100", RNG.Next(2000) },
{ "count_50", RNG.Next(2000) },
{ "count_miss", RNG.Next(2000) }
{ HitResult.Great, RNG.Next(2000) },
{ HitResult.Ok, RNG.Next(2000) },
{ HitResult.Meh, RNG.Next(2000) },
{ HitResult.Miss, RNG.Next(2000) }
};
}
@ -289,10 +290,10 @@ namespace osu.Game.Tests.Visual.Online
private APIScoreWithPosition createUserBest() => new APIScoreWithPosition
{
Score = new APIScore
Score = new SoloScoreInfo
{
Date = DateTimeOffset.Now,
OnlineID = onlineID++,
EndedAt = DateTimeOffset.Now,
ID = onlineID++,
User = new APIUser
{
Id = 7151382,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,8 @@ namespace osu.Game.Tournament.Components
{
public class TournamentSpriteTextWithBackground : CompositeDrawable
{
protected readonly TournamentSpriteText Text;
public readonly TournamentSpriteText Text;
protected readonly Box Background;
public TournamentSpriteTextWithBackground(string text = "")

View File

@ -22,6 +22,8 @@ namespace osu.Game.Tournament.Components
private Video video;
private ManualClock manualClock;
public bool VideoAvailable => video != null;
public TourneyVideo(string filename, bool drawFallbackGradient = false)
{
this.filename = filename;

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using Newtonsoft.Json;
using osu.Framework.Bindables;
@ -13,7 +11,7 @@ namespace osu.Game.Tournament.Models
public int ID;
[JsonProperty("BeatmapInfo")]
public TournamentBeatmap Beatmap;
public TournamentBeatmap? Beatmap;
public long Score;

View File

@ -0,0 +1,101 @@
// 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.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Online.Multiplayer;
using osuTK;
namespace osu.Game.Tournament
{
internal class SaveChangesOverlay : CompositeDrawable
{
[Resolved]
private TournamentGame tournamentGame { get; set; } = null!;
private string? lastSerialisedLadder;
private readonly TourneyButton saveChangesButton;
public SaveChangesOverlay()
{
RelativeSizeAxes = Axes.Both;
InternalChild = new Container
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Position = new Vector2(5),
CornerRadius = 10,
Masking = true,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = OsuColour.Gray(0.2f),
RelativeSizeAxes = Axes.Both,
},
saveChangesButton = new TourneyButton
{
Text = "Save Changes",
Width = 140,
Height = 50,
Padding = new MarginPadding
{
Top = 10,
Left = 10,
},
Margin = new MarginPadding
{
Right = 10,
Bottom = 10,
},
Action = saveChanges,
// Enabled = { Value = false },
},
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
scheduleNextCheck();
}
private async Task checkForChanges()
{
string serialisedLadder = await Task.Run(() => tournamentGame.GetSerialisedLadder());
// If a save hasn't been triggered by the user yet, populate the initial value
lastSerialisedLadder ??= serialisedLadder;
if (lastSerialisedLadder != serialisedLadder && !saveChangesButton.Enabled.Value)
{
saveChangesButton.Enabled.Value = true;
saveChangesButton.Background
.FadeColour(saveChangesButton.BackgroundColour.Lighten(0.5f), 500, Easing.In).Then()
.FadeColour(saveChangesButton.BackgroundColour, 500, Easing.Out)
.Loop();
}
scheduleNextCheck();
}
private void scheduleNextCheck() => Scheduler.AddDelayed(() => checkForChanges().FireAndForget(), 1000);
private void saveChanges()
{
tournamentGame.SaveChanges();
lastSerialisedLadder = tournamentGame.GetSerialisedLadder();
saveChangesButton.Enabled.Value = false;
saveChangesButton.Background.FadeColour(saveChangesButton.BackgroundColour, 500);
}
}
}

View File

@ -12,8 +12,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Graphics;
@ -45,7 +43,7 @@ namespace osu.Game.Tournament.Screens.Drawings
public ITeamList TeamList;
[BackgroundDependencyLoader]
private void load(TextureStore textures, Storage storage)
private void load(Storage storage)
{
RelativeSizeAxes = Axes.Both;
@ -91,11 +89,10 @@ namespace osu.Game.Tournament.Screens.Drawings
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Sprite
new TourneyVideo("drawings")
{
Loop = true,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fill,
Texture = textures.Get(@"Backgrounds/Drawings/background.png")
},
// Visualiser
new VisualiserContainer

View File

@ -298,10 +298,10 @@ namespace osu.Game.Tournament.Screens.Editors
}, true);
}
private void updatePanel()
private void updatePanel() => Scheduler.AddOnce(() =>
{
drawableContainer.Child = new UserGridPanel(user.ToAPIUser()) { Width = 300 };
}
});
}
}
}

View File

@ -20,7 +20,7 @@ using osuTK;
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 TModel : class, new()
{

View File

@ -16,6 +16,10 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
{
private readonly TeamScore score;
private readonly TournamentSpriteTextWithBackground teamText;
private readonly Bindable<string> teamName = new Bindable<string>("???");
private 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),
Origin = anchor,
@ -113,6 +117,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
updateDisplay();
FinishTransforms(true);
if (Team != null)
teamName.BindTo(Team.FullName);
teamName.BindValueChanged(name => teamText.Text.Text = name.NewValue, true);
}
private void updateDisplay()

View File

@ -42,6 +42,8 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
currentMatch.BindTo(ladder.CurrentMatch);
currentMatch.BindValueChanged(matchChanged);
currentTeam.BindValueChanged(teamChanged);
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.
// thus we handle this manually.
teamChanged(currentTeam.Value);
currentTeam.TriggerChange();
}
protected override bool OnMouseDown(MouseDownEvent e)
@ -88,11 +90,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
return base.OnMouseDown(e);
}
private void teamChanged(TournamentTeam team)
private void teamChanged(ValueChangedEvent<TournamentTeam> team)
{
InternalChildren = new Drawable[]
{
teamDisplay = new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0),
teamDisplay = new TeamDisplay(team.NewValue, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0),
};
}
}

View File

@ -21,7 +21,7 @@ using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Gameplay
{
public class GameplayScreen : BeatmapInfoScreen, IProvideVideo
public class GameplayScreen : BeatmapInfoScreen
{
private readonly BindableBool warmup = new BindableBool();

View File

@ -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
{
}
}

View File

@ -53,6 +53,9 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
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;
losersCheckbox.Current = selection.NewValue?.Losers;
dateTimeBox.Current = selection.NewValue?.Date;

View File

@ -19,7 +19,7 @@ using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Ladder
{
public class LadderScreen : TournamentScreen, IProvideVideo
public class LadderScreen : TournamentScreen
{
protected Container<DrawableTournamentMatch> MatchesContainer;
private Container<Path> paths;

View File

@ -19,7 +19,7 @@ using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Schedule
{
public class ScheduleScreen : TournamentScreen // IProvidesVideo
public class ScheduleScreen : TournamentScreen
{
private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>();
private Container mainContainer;

View File

@ -9,6 +9,8 @@ using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.API;
using osu.Game.Overlays;
@ -19,7 +21,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.Setup
{
public class SetupScreen : TournamentScreen, IProvideVideo
public class SetupScreen : TournamentScreen
{
private FillFlowContainer fillFlow;
@ -48,13 +50,21 @@ namespace osu.Game.Tournament.Screens.Setup
{
windowSize = frameworkConfig.GetBindable<Size>(FrameworkSetting.WindowedSize);
InternalChild = fillFlow = new FillFlowContainer
InternalChildren = new Drawable[]
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Padding = new MarginPadding(10),
Spacing = new Vector2(10),
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(0.2f),
},
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));
@ -74,7 +84,8 @@ namespace osu.Game.Tournament.Screens.Setup
Action = () => sceneManager?.SetScreen(new StablePathSelectScreen()),
Value = fileBasedIpc?.IPCStorage?.GetFullPath(string.Empty) ?? "Not found",
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
{

View File

@ -14,7 +14,7 @@ using osuTK.Graphics;
namespace osu.Game.Tournament.Screens.Showcase
{
public class ShowcaseScreen : BeatmapInfoScreen // IProvideVideo
public class ShowcaseScreen : BeatmapInfoScreen
{
[BackgroundDependencyLoader]
private void load()

View File

@ -3,6 +3,7 @@
#nullable disable
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -19,7 +20,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.TeamIntro
{
public class SeedingScreen : TournamentMatchScreen, IProvideVideo
public class SeedingScreen : TournamentMatchScreen
{
private Container mainContainer;
@ -69,7 +70,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
currentTeam.BindValueChanged(teamChanged, true);
}
private void teamChanged(ValueChangedEvent<TournamentTeam> team)
private void teamChanged(ValueChangedEvent<TournamentTeam> team) => Scheduler.AddOnce(() =>
{
if (team.NewValue == null)
{
@ -78,7 +79,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
}
showTeam(team.NewValue);
}
});
protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match)
{
@ -120,8 +121,14 @@ namespace osu.Game.Tournament.Screens.TeamIntro
foreach (var seeding in team.SeedingResults)
{
fill.Add(new ModRow(seeding.Mod.Value, seeding.Seed.Value));
foreach (var beatmap in seeding.Beatmaps)
{
if (beatmap.Beatmap == null)
continue;
fill.Add(new BeatmapScoreRow(beatmap));
}
}
}
@ -129,6 +136,8 @@ namespace osu.Game.Tournament.Screens.TeamIntro
{
public BeatmapScoreRow(SeedingBeatmap beatmap)
{
Debug.Assert(beatmap.Beatmap != null);
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
@ -157,7 +166,8 @@ namespace osu.Game.Tournament.Screens.TeamIntro
Children = new Drawable[]
{
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) },
}
},
};

View File

@ -13,7 +13,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.TeamIntro
{
public class TeamIntroScreen : TournamentMatchScreen, IProvideVideo
public class TeamIntroScreen : TournamentMatchScreen
{
private Container mainContainer;

View File

@ -14,7 +14,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.TeamWin
{
public class TeamWinScreen : TournamentMatchScreen, IProvideVideo
public class TeamWinScreen : TournamentMatchScreen
{
private Container mainContainer;
@ -66,7 +66,7 @@ namespace osu.Game.Tournament.Screens.TeamWin
private bool firstDisplay = true;
private void update() => Schedule(() =>
private void update() => Scheduler.AddOnce(() =>
{
var match = CurrentMatch.Value;

View File

@ -11,8 +11,6 @@ using osu.Framework.Configuration;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Handlers.Mouse;
using osu.Framework.Logging;
using osu.Framework.Platform;
@ -20,11 +18,11 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Tournament.Models;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tournament
{
[Cached]
public class TournamentGame : TournamentGameBase
{
public static ColourInfo GetTeamColour(TeamColour teamColour) => teamColour == TeamColour.Red ? COLOUR_RED : COLOUR_BLUE;
@ -78,40 +76,9 @@ namespace osu.Game.Tournament
LoadComponentsAsync(new[]
{
new Container
new SaveChangesOverlay
{
CornerRadius = 10,
Depth = float.MinValue,
Position = new Vector2(5),
Masking = true,
AutoSizeAxes = Axes.Both,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Children = new Drawable[]
{
new Box
{
Colour = OsuColour.Gray(0.2f),
RelativeSizeAxes = Axes.Both,
},
new TourneyButton
{
Text = "Save Changes",
Width = 140,
Height = 50,
Padding = new MarginPadding
{
Top = 10,
Left = 10,
},
Margin = new MarginPadding
{
Right = 10,
Bottom = 10,
},
Action = SaveChanges,
},
}
},
heightWarning = new WarningBox("Please make the window wider")
{

View File

@ -295,7 +295,7 @@ namespace osu.Game.Tournament
}
}
protected virtual void SaveChanges()
public void SaveChanges()
{
if (!bracketLoadTaskCompletionSource.Task.IsCompletedSuccessfully)
{
@ -311,7 +311,16 @@ namespace osu.Game.Tournament
.ToList();
// Serialise before opening stream for writing, so if there's a failure it will leave the file in the previous state.
string serialisedLadder = JsonConvert.SerializeObject(ladder,
string serialisedLadder = GetSerialisedLadder();
using (var stream = storage.CreateFileSafely(BRACKET_FILENAME))
using (var sw = new StreamWriter(stream))
sw.Write(serialisedLadder);
}
public string GetSerialisedLadder()
{
return JsonConvert.SerializeObject(ladder,
new JsonSerializerSettings
{
Formatting = Formatting.Indented,
@ -319,10 +328,6 @@ namespace osu.Game.Tournament
DefaultValueHandling = DefaultValueHandling.Ignore,
Converters = new JsonConverter[] { new JsonPointConverter() }
});
using (var stream = storage.CreateFileSafely(BRACKET_FILENAME))
using (var sw = new StreamWriter(stream))
sw.Write(serialisedLadder);
}
protected override UserInputManager CreateUserInputManager() => new TournamentInputManager();

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Testing;
using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
@ -186,7 +187,7 @@ namespace osu.Game.Tournament
var lastScreen = currentScreen;
currentScreen = target;
if (currentScreen is IProvideVideo)
if (currentScreen.ChildrenOfType<TourneyVideo>().FirstOrDefault()?.VideoAvailable == true)
{
video.FadeOut(200);

View File

@ -3,12 +3,15 @@
#nullable disable
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Tournament
{
public class TourneyButton : OsuButton
{
public new Box Background => base.Background;
public TourneyButton()
: base(null)
{

View File

@ -80,9 +80,8 @@ namespace osu.Game.Beatmaps
if (beatmapSet.OnlineID > 0)
{
var existingSetWithSameOnlineID = realm.All<BeatmapSetInfo>().SingleOrDefault(b => b.OnlineID == beatmapSet.OnlineID);
if (existingSetWithSameOnlineID != null)
// OnlineID should really be unique, but to avoid catastrophic failure let's iterate just to be sure.
foreach (var existingSetWithSameOnlineID in realm.All<BeatmapSetInfo>().Where(b => b.OnlineID == beatmapSet.OnlineID))
{
existingSetWithSameOnlineID.DeletePending = true;
existingSetWithSameOnlineID.OnlineID = -1;
@ -90,7 +89,7 @@ namespace osu.Game.Beatmaps
foreach (var b in existingSetWithSameOnlineID.Beatmaps)
b.OnlineID = -1;
LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineID ({beatmapSet.OnlineID}). It will be deleted.");
LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineID ({beatmapSet.OnlineID}). It will be disassociated and marked for deletion.");
}
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json;
@ -110,6 +111,11 @@ namespace osu.Game.Beatmaps
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>
/// 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"/>).
@ -151,14 +157,23 @@ namespace osu.Game.Beatmaps
public bool AudioEquals(BeatmapInfo? other) => other != null
&& BeatmapSet != null
&& other.BeatmapSet != null
&& BeatmapSet.Hash == other.BeatmapSet.Hash
&& Metadata.AudioFile == other.Metadata.AudioFile;
&& compareFiles(this, other, m => m.AudioFile);
public bool BackgroundEquals(BeatmapInfo? other) => other != null
&& BeatmapSet != null
&& other.BeatmapSet != null
&& BeatmapSet.Hash == other.BeatmapSet.Hash
&& Metadata.BackgroundFile == other.Metadata.BackgroundFile;
&& compareFiles(this, other, m => m.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;
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;

View File

@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Collections
{
public class CollectionToggleMenuItem : ToggleMenuItem
{
public CollectionToggleMenuItem(BeatmapCollection collection, IBeatmapInfo beatmap)
: base(collection.Name.Value, MenuItemType.Standard, state =>
{
if (state)
collection.BeatmapHashes.Add(beatmap.MD5Hash);
else
collection.BeatmapHashes.Remove(beatmap.MD5Hash);
})
{
State.Value = collection.BeatmapHashes.Contains(beatmap.MD5Hash);
}
}
}

View File

@ -58,8 +58,9 @@ namespace osu.Game.Database
/// 12 2021-11-24 Add Status to RealmBeatmapSet.
/// 13 2022-01-13 Final migration of beatmaps and scores to realm (multiple new storage fields).
/// 14 2022-03-01 Added BeatmapUserSettings to BeatmapInfo.
/// 15 2022-07-13 Added LastPlayed to BeatmapInfo.
/// </summary>
private const int schema_version = 14;
private const int schema_version = 15;
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.

View File

@ -338,11 +338,11 @@ namespace osu.Game.Database
// import to store
realm.Add(item);
PostImport(item, realm);
transaction.Commit();
}
PostImport(item, realm);
LogForModel(item, @"Import successfully completed!");
}
catch (Exception e)
@ -479,7 +479,7 @@ namespace osu.Game.Database
}
/// <summary>
/// Perform any final actions after the import has been committed to the database.
/// Perform any final actions before the import has been committed to the database.
/// </summary>
/// <param name="model">The model prepared for import.</param>
/// <param name="realm">The current realm context.</param>

View File

@ -19,7 +19,7 @@ namespace osu.Game.IO
public LineBufferedReader(Stream stream, bool leaveOpen = false)
{
streamReader = new StreamReader(stream, Encoding.UTF8, true, -1, leaveOpen);
streamReader = new StreamReader(stream, Encoding.UTF8, true, 1024, leaveOpen);
}
/// <summary>

View File

@ -94,6 +94,8 @@ namespace osu.Game.IO
error = OsuStorageError.None;
Storage lastStorage = UnderlyingStorage;
Logger.Log($"Attempting to use custom storage location {CustomStoragePath}");
try
{
Storage userStorage = host.GetStorage(CustomStoragePath);
@ -102,6 +104,7 @@ namespace osu.Game.IO
error = OsuStorageError.AccessibleButEmpty;
ChangeTargetStorage(userStorage);
Logger.Log($"Storage successfully changed to {CustomStoragePath}.");
}
catch
{
@ -109,6 +112,9 @@ namespace osu.Game.IO
ChangeTargetStorage(lastStorage);
}
if (error != OsuStorageError.None)
Logger.Log($"Custom storage location could not be used ({error}).");
return error == OsuStorageError.None;
}

View File

@ -38,7 +38,7 @@ namespace osu.Game.Online.API
public string WebsiteRootUrl { get; }
public int APIVersion => 20220217; // We may want to pull this from the game version eventually.
public int APIVersion => 20220705; // We may want to pull this from the game version eventually.
public Exception LastLoginError { get; private set; }

View File

@ -16,11 +16,11 @@ namespace osu.Game.Online.API.Requests.Responses
public int? Position;
[JsonProperty(@"score")]
public APIScore Score;
public SoloScoreInfo Score;
public ScoreInfo CreateScoreInfo(RulesetStore rulesets, BeatmapInfo beatmap = null)
{
var score = Score.CreateScoreInfo(rulesets, beatmap);
var score = Score.ToScoreInfo(rulesets, beatmap);
score.Position = Position;
return score;
}

View File

@ -11,7 +11,7 @@ namespace osu.Game.Online.API.Requests.Responses
public class APIScoresCollection
{
[JsonProperty(@"scores")]
public List<APIScore> Scores;
public List<SoloScoreInfo> Scores;
[JsonProperty(@"userScore")]
public APIScoreWithPosition UserScore;

View File

@ -0,0 +1,143 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Online.API.Requests.Responses
{
[Serializable]
public class SoloScoreInfo : IHasOnlineID<long>
{
[JsonProperty("replay")]
public bool HasReplay { get; set; }
[JsonProperty("beatmap_id")]
public int BeatmapID { get; set; }
[JsonProperty("ruleset_id")]
public int RulesetID { get; set; }
[JsonProperty("build_id")]
public int? BuildID { get; set; }
[JsonProperty("passed")]
public bool Passed { get; set; }
[JsonProperty("total_score")]
public int TotalScore { get; set; }
[JsonProperty("accuracy")]
public double Accuracy { get; set; }
[JsonProperty("user_id")]
public int UserID { get; set; }
// TODO: probably want to update this column to match user stats (short)?
[JsonProperty("max_combo")]
public int MaxCombo { get; set; }
[JsonConverter(typeof(StringEnumConverter))]
[JsonProperty("rank")]
public ScoreRank Rank { get; set; }
[JsonProperty("started_at")]
public DateTimeOffset? StartedAt { get; set; }
[JsonProperty("ended_at")]
public DateTimeOffset? EndedAt { get; set; }
[JsonProperty("mods")]
public APIMod[] Mods { get; set; } = Array.Empty<APIMod>();
[JsonIgnore]
[JsonProperty("created_at")]
public DateTimeOffset CreatedAt { get; set; }
[JsonIgnore]
[JsonProperty("updated_at")]
public DateTimeOffset UpdatedAt { get; set; }
[JsonIgnore]
[JsonProperty("deleted_at")]
public DateTimeOffset? DeletedAt { get; set; }
[JsonProperty("statistics")]
public Dictionary<HitResult, int> Statistics { get; set; } = new Dictionary<HitResult, int>();
#region osu-web API additions (not stored to database).
[JsonProperty("id")]
public long? ID { get; set; }
[JsonProperty("user")]
public APIUser? User { get; set; }
[JsonProperty("pp")]
public double? PP { get; set; }
#endregion
public override string ToString() => $"score_id: {ID} user_id: {UserID}";
/// <summary>
/// Create a <see cref="ScoreInfo"/> from an API score instance.
/// </summary>
/// <param name="rulesets">A ruleset store, used to populate a ruleset instance in the returned score.</param>
/// <param name="beatmap">An optional beatmap, copied into the returned score (for cases where the API does not populate the beatmap).</param>
/// <returns></returns>
public ScoreInfo ToScoreInfo(RulesetStore rulesets, BeatmapInfo? beatmap = null)
{
var ruleset = rulesets.GetRuleset(RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {RulesetID} not found locally");
var rulesetInstance = ruleset.CreateInstance();
var mods = Mods.Select(apiMod => rulesetInstance.CreateModFromAcronym(apiMod.Acronym)).Where(m => m != null).ToArray();
// all API scores provided by this class are considered to be legacy.
mods = mods.Append(rulesetInstance.CreateMod<ModClassic>()).ToArray();
var scoreInfo = ToScoreInfo(mods);
scoreInfo.Ruleset = ruleset;
if (beatmap != null) scoreInfo.BeatmapInfo = beatmap;
return scoreInfo;
}
/// <summary>
/// Create a <see cref="ScoreInfo"/> from an API score instance.
/// </summary>
/// <param name="mods">The mod instances, resolved from a ruleset.</param>
/// <returns></returns>
public ScoreInfo ToScoreInfo(Mod[] mods) => new ScoreInfo
{
OnlineID = OnlineID,
User = User ?? new APIUser { Id = UserID },
BeatmapInfo = new BeatmapInfo { OnlineID = BeatmapID },
Ruleset = new RulesetInfo { OnlineID = RulesetID },
Passed = Passed,
TotalScore = TotalScore,
Accuracy = Accuracy,
MaxCombo = MaxCombo,
Rank = Rank,
Statistics = Statistics,
Date = EndedAt ?? DateTimeOffset.Now,
Hash = "online", // TODO: temporary?
HasReplay = HasReplay,
Mods = mods,
PP = PP,
};
public long OnlineID => ID ?? -1;
}
}

View File

@ -272,7 +272,7 @@ namespace osu.Game
dependencies.Cache(difficultyCache = new BeatmapDifficultyCache());
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, difficultyCache, LocalConfig));
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, API, difficultyCache, LocalConfig));
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, difficultyCache, performOnlineLookups: true));

View File

@ -87,7 +87,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
MD5Hash = apiBeatmap.MD5Hash
};
scoreManager.OrderByTotalScoreAsync(value.Scores.Select(s => s.CreateScoreInfo(rulesets, beatmapInfo)).ToArray(), loadCancellationSource.Token)
scoreManager.OrderByTotalScoreAsync(value.Scores.Select(s => s.ToScoreInfo(rulesets, beatmapInfo)).ToArray(), loadCancellationSource.Token)
.ContinueWith(task => Schedule(() =>
{
if (loadCancellationSource.IsCancellationRequested)
@ -101,7 +101,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
scoreTable.Show();
var userScore = value.UserScore;
var userScoreInfo = userScore?.Score.CreateScoreInfo(rulesets, beatmapInfo);
var userScoreInfo = userScore?.Score.ToScoreInfo(rulesets, beatmapInfo);
topScoresContainer.Add(new DrawableTopScore(topScore));

View File

@ -49,43 +49,54 @@ namespace osu.Game.Overlays
public void Push(PopupDialog dialog)
{
if (dialog == CurrentDialog || dialog.State.Value != Visibility.Visible) return;
var lastDialog = CurrentDialog;
if (dialog == CurrentDialog || dialog.State.Value == Visibility.Hidden) return;
// Immediately update the externally accessible property as this may be used for checks even before
// a DialogOverlay instance has finished loading.
var lastDialog = CurrentDialog;
CurrentDialog = dialog;
Scheduler.Add(() =>
Schedule(() =>
{
// if any existing dialog is being displayed, dismiss it before showing a new one.
lastDialog?.Hide();
dialog.State.ValueChanged += state => onDialogOnStateChanged(dialog, state.NewValue);
dialogContainer.Add(dialog);
// if the new dialog is hidden before added to the dialogContainer, bypass any further operations.
if (dialog.State.Value == Visibility.Hidden)
{
dismiss();
return;
}
dialogContainer.Add(dialog);
Show();
}, false);
dialog.State.BindValueChanged(state =>
{
if (state.NewValue != Visibility.Hidden) return;
// Trigger the demise of the dialog as soon as it hides.
dialog.Delay(PopupDialog.EXIT_DURATION).Expire();
dismiss();
});
});
void dismiss()
{
if (dialog != CurrentDialog) return;
// Handle the case where the dialog is the currently displayed dialog.
// In this scenario, the overlay itself should also be hidden.
Hide();
CurrentDialog = null;
}
}
public override bool IsPresent => Scheduler.HasPendingTasks || dialogContainer.Children.Count > 0;
protected override bool BlockNonPositionalInput => true;
private void onDialogOnStateChanged(VisibilityContainer dialog, Visibility v)
{
if (v != Visibility.Hidden) return;
// handle the dialog being dismissed.
dialog.Delay(PopupDialog.EXIT_DURATION).Expire();
if (dialog == CurrentDialog)
{
Hide();
CurrentDialog = null;
}
}
protected override void PopIn()
{
base.PopIn();
@ -97,7 +108,8 @@ namespace osu.Game.Overlays
base.PopOut();
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic);
if (CurrentDialog?.State.Value == Visibility.Visible)
// PopOut gets called initially, but we only want to hide dialog when we have been loaded and are present.
if (IsLoaded && CurrentDialog?.State.Value == Visibility.Visible)
CurrentDialog.Hide();
}

View File

@ -1,14 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Threading;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
using osuTK;
namespace osu.Game.Overlays.FirstRunSetup
{
@ -20,13 +30,175 @@ namespace osu.Game.Overlays.FirstRunSetup
{
Content.Children = new Drawable[]
{
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
RowDimensions = new[]
{
// Avoid height changes when changing language.
new Dimension(GridSizeMode.AutoSize, minSize: 100),
},
Content = new[]
{
new Drawable[]
{
new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{
Text = FirstRunSetupOverlayStrings.WelcomeDescription,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
},
},
}
},
new LanguageSelectionFlow
{
Text = FirstRunSetupOverlayStrings.WelcomeDescription,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
},
}
};
}
private class LanguageSelectionFlow : FillFlowContainer
{
private Bindable<string> frameworkLocale = null!;
private ScheduledDelegate? updateSelectedDelegate;
[BackgroundDependencyLoader]
private void load(FrameworkConfigManager frameworkConfig)
{
Direction = FillDirection.Full;
Spacing = new Vector2(5);
ChildrenEnumerable = Enum.GetValues(typeof(Language))
.Cast<Language>()
.Select(l => new LanguageButton(l)
{
Action = () => frameworkLocale.Value = l.ToCultureCode()
});
frameworkLocale = frameworkConfig.GetBindable<string>(FrameworkSetting.Locale);
frameworkLocale.BindValueChanged(locale =>
{
if (!LanguageExtensions.TryParseCultureCode(locale.NewValue, out var language))
language = Language.en;
// Changing language may cause a short period of blocking the UI thread while the new glyphs are loaded.
// Scheduling ensures the button animation plays smoothly after any blocking operation completes.
// Note that a delay is required (the alternative would be a double-schedule; delay feels better).
updateSelectedDelegate?.Cancel();
updateSelectedDelegate = Scheduler.AddDelayed(() => updateSelectedStates(language), 50);
}, true);
}
private void updateSelectedStates(Language language)
{
foreach (var c in Children.OfType<LanguageButton>())
c.Selected = c.Language == language;
}
private class LanguageButton : OsuClickableContainer
{
public readonly Language Language;
private Box backgroundBox = null!;
private OsuSpriteText text = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
private bool selected;
public bool Selected
{
get => selected;
set
{
if (selected == value)
return;
selected = value;
if (IsLoaded)
updateState();
}
}
public LanguageButton(Language language)
{
Language = language;
Size = new Vector2(160, 50);
Masking = true;
CornerRadius = 10;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
backgroundBox = new Box
{
Alpha = 0,
Colour = colourProvider.Background5,
RelativeSizeAxes = Axes.Both,
},
text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = colourProvider.Light1,
Text = Language.GetDescription(),
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
updateState();
}
protected override bool OnHover(HoverEvent e)
{
if (!selected)
updateState();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
if (!selected)
updateState();
base.OnHoverLost(e);
}
private void updateState()
{
if (selected)
{
const double selected_duration = 1000;
backgroundBox.FadeTo(1, selected_duration, Easing.OutQuint);
backgroundBox.FadeColour(colourProvider.Background2, selected_duration, Easing.OutQuint);
text.FadeColour(colourProvider.Content1, selected_duration, Easing.OutQuint);
text.ScaleTo(1.2f, selected_duration, Easing.OutQuint);
}
else
{
const double duration = 500;
backgroundBox.FadeTo(IsHovered ? 1 : 0, duration, Easing.OutQuint);
backgroundBox.FadeColour(colourProvider.Background5, duration, Easing.OutQuint);
text.FadeColour(colourProvider.Light1, duration, Easing.OutQuint);
text.ScaleTo(1, duration, Easing.OutQuint);
}
}
}
}
}
}

View File

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

View File

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

View File

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

View File

@ -186,7 +186,7 @@ namespace osu.Game.Screens.Edit
loadableBeatmap = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value);
// required so we can get the track length in EditorClock.
// this is safe as nothing has yet got a reference to this new beatmap.
// this is ONLY safe because the track being provided is a `TrackVirtual` which we don't really care about disposing.
loadableBeatmap.LoadTrack();
// this is a bit haphazard, but guards against setting the lease Beatmap bindable if
@ -921,7 +921,7 @@ namespace osu.Game.Screens.Edit
private void cancelExit()
{
samplePlaybackDisabled.Value = false;
updateSampleDisabledState();
loader?.CancelPendingDifficultySwitch();
}

View File

@ -35,7 +35,13 @@ namespace osu.Game.Screens.Edit.GameplayTest
ScoreProcessor.HasCompleted.BindValueChanged(completed =>
{
if (completed.NewValue)
Scheduler.AddDelayed(this.Exit, RESULTS_DISPLAY_DELAY);
{
Scheduler.AddDelayed(() =>
{
if (this.IsCurrentScreen())
this.Exit();
}, RESULTS_DISPLAY_DELAY);
}
});
}

View File

@ -72,9 +72,17 @@ namespace osu.Game.Screens.Menu
RelativeSizeAxes = Axes.Both,
Clock = decoupledClock,
LoadMenu = LoadMenu
}, t =>
}, _ =>
{
AddInternal(t);
AddInternal(intro);
// There is a chance that the intro timed out before being displayed, and this scheduled callback could
// happen during the outro rather than intro.
// In such a scenario, we don't want to play the intro sample, nor attempt to start the intro track
// (that may have already been since disposed by MusicController).
if (DidLoadMenu)
return;
if (!UsingThemedIntro)
welcome?.Play();

View File

@ -9,17 +9,20 @@ using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Collections;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@ -27,6 +30,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.Chat;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets;
@ -38,7 +42,7 @@ using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay
{
public class DrawableRoomPlaylistItem : OsuRearrangeableListItem<PlaylistItem>
public class DrawableRoomPlaylistItem : OsuRearrangeableListItem<PlaylistItem>, IHasContextMenu
{
public const float HEIGHT = 50;
@ -93,6 +97,9 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved]
private RulesetStore rulesets { get; set; }
[Resolved]
private BeatmapManager beatmaps { get; set; }
[Resolved]
private OsuColour colours { get; set; }
@ -102,6 +109,15 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved]
private BeatmapLookupCache beatmapLookupCache { get; set; }
[Resolved(CanBeNull = true)]
private BeatmapSetOverlay beatmapOverlay { get; set; }
[Resolved(CanBeNull = true)]
private CollectionManager collectionManager { get; set; }
[Resolved(CanBeNull = true)]
private ManageCollectionsDialog manageCollectionsDialog { get; set; }
protected override bool ShouldBeConsideredForInput(Drawable child) => AllowReordering || AllowDeletion || !AllowSelection || SelectedItem.Value == Model;
public DrawableRoomPlaylistItem(PlaylistItem item)
@ -433,7 +449,7 @@ namespace osu.Game.Screens.OnlinePlay
}
}
},
}
},
};
}
@ -470,6 +486,31 @@ namespace osu.Game.Screens.OnlinePlay
return true;
}
public MenuItem[] ContextMenuItems
{
get
{
List<MenuItem> items = new List<MenuItem>();
if (beatmapOverlay != null)
items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(Item.Beatmap.OnlineID)));
if (collectionManager != null && beatmap != null)
{
if (beatmaps.QueryBeatmap(b => b.OnlineID == beatmap.OnlineID) is BeatmapInfo local && !local.BeatmapSet.AsNonNull().DeletePending)
{
var collectionItems = collectionManager.Collections.Select(c => new CollectionToggleMenuItem(c, beatmap)).Cast<OsuMenuItem>().ToList();
if (manageCollectionsDialog != null)
collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show));
items.Add(new OsuMenuItem("Collections") { Items = collectionItems });
}
}
return items.ToArray();
}
}
public class PlaylistEditButton : GrayButton
{
public PlaylistEditButton()

View File

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

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