1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-13 17:53:21 +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> <GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ using osu.Framework.Platform;
using osu.Game; using osu.Game;
using osu.Game.IPC; using osu.Game.IPC;
using osu.Game.Tournament; using osu.Game.Tournament;
using SDL2;
using Squirrel; using Squirrel;
namespace osu.Desktop namespace osu.Desktop
@ -29,7 +30,21 @@ namespace osu.Desktop
{ {
// run Squirrel first, as the app may exit after these run // run Squirrel first, as the app may exit after these run
if (OperatingSystem.IsWindows()) if (OperatingSystem.IsWindows())
{
var windowsVersion = Environment.OSVersion.Version;
// While .NET 6 still supports Windows 7 and above, we are limited by realm currently, as they choose to only support 8.1 and higher.
// See https://www.mongodb.com/docs/realm/sdk/dotnet/#supported-platforms
if (windowsVersion.Major < 6 || (windowsVersion.Major == 6 && windowsVersion.Minor <= 2))
{
SDL.SDL_ShowSimpleMessageBox(SDL.SDL_MessageBoxFlags.SDL_MESSAGEBOX_ERROR,
"Your operating system is too old to run osu!",
"This version of osu! requires at least Windows 8.1 to run.\nPlease upgrade your operating system or consider using an older version of osu!.", IntPtr.Zero);
return;
}
setupSquirrel(); setupSquirrel();
}
// Back up the cwd before DesktopGameHost changes it // Back up the cwd before DesktopGameHost changes it
string cwd = Environment.CurrentDirectory; string cwd = Environment.CurrentDirectory;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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"> <Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" /> <Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.17.2" /> <PackageReference Include="Moq" Version="4.18.1" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" /> <PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" /> <PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

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. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites; 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 namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModAlternate : Mod, IApplicableToDrawableRuleset<OsuHitObject> public class OsuModAlternate : InputBlockingMod
{ {
public override string Name => @"Alternate"; public override string Name => @"Alternate";
public override string Acronym => @"AL"; public override string Acronym => @"AL";
public override string Description => @"Don't use the same key twice in a row!"; 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 IconUsage? Icon => FontAwesome.Solid.Keyboard;
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModSingleTap) }).ToArray();
private const double flash_duration = 1000; protected override bool CheckValidNewAction(OsuAction action) => LastAcceptedAction != action;
/// <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)
{
}
}
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer 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 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> /// <summary>
/// How early before a hitobject's start time to trigger a hit. /// 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 ModType Type => ModType.Fun;
public override string Description => "Everything rotates. EVERYTHING."; public override string Description => "Everything rotates. EVERYTHING.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised), typeof(OsuModRepel) };
private float theta; private float theta;

View File

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

View File

@ -319,13 +319,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
const float fade_out_time = 450; 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) switch (state)
{ {
case ArmedState.Hit: case ArmedState.Hit:
Ball.ScaleTo(HitObject.Scale * 1.4f, fade_out_time, Easing.Out);
if (SliderBody?.SnakingOut.Value == true) if (SliderBody?.SnakingOut.Value == true)
Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear. Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear.
break; break;

View File

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

View File

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

View File

@ -1,19 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Default namespace osu.Game.Rulesets.Osu.Skinning.Default
{ {
public class DefaultFollowCircle : CompositeDrawable public class DefaultFollowCircle : FollowCircle
{ {
public DefaultFollowCircle() public DefaultFollowCircle()
{ {
RelativeSizeAxes = Axes.Both;
InternalChild = new CircularContainer InternalChild = new CircularContainer
{ {
RelativeSizeAxes = Axes.Both, 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. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -19,13 +17,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{ {
public class DefaultSliderBall : CompositeDrawable public class DefaultSliderBall : CompositeDrawable
{ {
private Box box; private Box box = null!;
[Resolved(canBeNull: true)]
private DrawableHitObject? parentObject { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject, ISkinSource skin) private void load(ISkinSource skin)
{ {
var slider = (DrawableSlider)drawableObject;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
float radius = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS; float radius = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS;
@ -51,10 +50,62 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
} }
}; };
if (parentObject != null)
{
var slider = (DrawableSlider)parentObject;
slider.Tracking.BindValueChanged(trackingChanged, true); 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) => private void trackingChanged(ValueChangedEvent<bool> tracking) =>
box.FadeTo(tracking.NewValue ? 0.3f : 0.05f, 200, Easing.OutQuint); 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. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
public class LegacyFollowCircle : CompositeDrawable public class LegacyFollowCircle : FollowCircle
{ {
public LegacyFollowCircle(Drawable animationContent) public LegacyFollowCircle(Drawable animationContent)
{ {
@ -18,5 +20,36 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
InternalChild = animationContent; 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) private static Vector2 clampSliderToPlayfield(WorkingObject workingObject)
{ {
var slider = (Slider)workingObject.HitObject; var slider = (Slider)workingObject.HitObject;
var possibleMovementBounds = calculatePossibleMovementBounds(slider); var possibleMovementBounds = CalculatePossibleMovementBounds(slider);
// The slider rotation applied in computeModifiedPosition might make it impossible to fit the slider into the playfield
// For example, a long horizontal slider will be off-screen when rotated by 90 degrees
// In this case, limit the rotation to either 0 or 180 degrees
if (possibleMovementBounds.Width < 0 || possibleMovementBounds.Height < 0)
{
float currentRotation = getSliderRotation(slider);
float diff1 = getAngleDifference(workingObject.RotationOriginal, currentRotation);
float diff2 = getAngleDifference(workingObject.RotationOriginal + MathF.PI, currentRotation);
if (diff1 < diff2)
{
RotateSlider(slider, workingObject.RotationOriginal - getSliderRotation(slider));
}
else
{
RotateSlider(slider, workingObject.RotationOriginal + MathF.PI - getSliderRotation(slider));
}
possibleMovementBounds = CalculatePossibleMovementBounds(slider);
}
var previousPosition = workingObject.PositionModified; var previousPosition = workingObject.PositionModified;
@ -239,10 +260,12 @@ namespace osu.Game.Rulesets.Osu.Utils
/// Calculates a <see cref="RectangleF"/> which contains all of the possible movements of the slider (in relative X/Y coordinates) /// Calculates a <see cref="RectangleF"/> which contains all of the possible movements of the slider (in relative X/Y coordinates)
/// such that the entire slider is inside the playfield. /// such that the entire slider is inside the playfield.
/// </summary> /// </summary>
/// <param name="slider">The <see cref="Slider"/> for which to calculate a movement bounding box.</param>
/// <returns>A <see cref="RectangleF"/> which contains all of the possible movements of the slider such that the entire slider is inside the playfield.</returns>
/// <remarks> /// <remarks>
/// If the slider is larger than the playfield, the returned <see cref="RectangleF"/> may have negative width/height. /// If the slider is larger than the playfield, the returned <see cref="RectangleF"/> may have negative width/height.
/// </remarks> /// </remarks>
private static RectangleF calculatePossibleMovementBounds(Slider slider) public static RectangleF CalculatePossibleMovementBounds(Slider slider)
{ {
var pathPositions = new List<Vector2>(); var pathPositions = new List<Vector2>();
slider.Path.GetPathToProgress(pathPositions, 0, 1); slider.Path.GetPathToProgress(pathPositions, 0, 1);
@ -353,6 +376,18 @@ namespace osu.Game.Rulesets.Osu.Utils
return MathF.Atan2(endPositionVector.Y, endPositionVector.X); return MathF.Atan2(endPositionVector.Y, endPositionVector.X);
} }
/// <summary>
/// Get the absolute difference between 2 angles measured in Radians.
/// </summary>
/// <param name="angle1">The first angle</param>
/// <param name="angle2">The second angle</param>
/// <returns>The absolute difference with interval <c>[0, MathF.PI)</c></returns>
private static float getAngleDifference(float angle1, float angle2)
{
float diff = MathF.Abs(angle1 - angle2) % (MathF.PI * 2);
return MathF.Min(diff, MathF.PI * 2 - diff);
}
public class ObjectPositionInfo public class ObjectPositionInfo
{ {
/// <summary> /// <summary>
@ -395,6 +430,7 @@ namespace osu.Game.Rulesets.Osu.Utils
private class WorkingObject private class WorkingObject
{ {
public float RotationOriginal { get; }
public Vector2 PositionOriginal { get; } public Vector2 PositionOriginal { get; }
public Vector2 PositionModified { get; set; } public Vector2 PositionModified { get; set; }
public Vector2 EndPositionModified { get; set; } public Vector2 EndPositionModified { get; set; }
@ -405,6 +441,7 @@ namespace osu.Game.Rulesets.Osu.Utils
public WorkingObject(ObjectPositionInfo positionInfo) public WorkingObject(ObjectPositionInfo positionInfo)
{ {
PositionInfo = positionInfo; PositionInfo = positionInfo;
RotationOriginal = HitObject is Slider slider ? getSliderRotation(slider) : 0;
PositionModified = PositionOriginal = HitObject.Position; PositionModified = PositionOriginal = HitObject.Position;
EndPositionModified = HitObject.EndPosition; EndPositionModified = HitObject.EndPosition;
} }

View File

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

View File

@ -4,9 +4,12 @@
#nullable disable #nullable disable
using System; using System;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Models;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.NonVisual namespace osu.Game.Tests.NonVisual
{ {
@ -23,6 +26,47 @@ namespace osu.Game.Tests.NonVisual
Assert.IsTrue(ourInfo.MatchesOnlineID(otherInfo)); Assert.IsTrue(ourInfo.MatchesOnlineID(otherInfo));
} }
[Test]
public void TestAudioEqualityNoFile()
{
var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
Assert.AreNotEqual(beatmapSetA, beatmapSetB);
Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
}
[Test]
public void TestAudioEqualitySameHash()
{
var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
addAudioFile(beatmapSetA, "abc");
addAudioFile(beatmapSetB, "abc");
Assert.AreNotEqual(beatmapSetA, beatmapSetB);
Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
}
[Test]
public void TestAudioEqualityDifferentHash()
{
var beatmapSetA = TestResources.CreateTestBeatmapSetInfo(1);
var beatmapSetB = TestResources.CreateTestBeatmapSetInfo(1);
addAudioFile(beatmapSetA);
addAudioFile(beatmapSetB);
Assert.AreNotEqual(beatmapSetA, beatmapSetB);
Assert.IsTrue(beatmapSetA.Beatmaps.Single().AudioEquals(beatmapSetB.Beatmaps.Single()));
}
private static void addAudioFile(BeatmapSetInfo beatmapSetInfo, string hash = null)
{
beatmapSetInfo.Files.Add(new RealmNamedFileUsage(new RealmFile { Hash = hash ?? Guid.NewGuid().ToString() }, "audio.mp3"));
}
[Test] [Test]
public void TestDatabasedWithDatabased() public void TestDatabasedWithDatabased()
{ {

View File

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

View File

@ -6,6 +6,7 @@ using System.IO;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Screens; using osu.Framework.Screens;
@ -103,6 +104,8 @@ namespace osu.Game.Tests.Visual.Editing
*/ */
public void TestAddAudioTrack() public void TestAddAudioTrack()
{ {
AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual);
AddAssert("switch track to real track", () => AddAssert("switch track to real track", () =>
{ {
var setup = Editor.ChildrenOfType<SetupScreen>().First(); var setup = Editor.ChildrenOfType<SetupScreen>().First();
@ -131,6 +134,7 @@ namespace osu.Game.Tests.Visual.Editing
} }
}); });
AddAssert("track is not virtual", () => Beatmap.Value.Track is not TrackVirtual);
AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000); AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000);
} }

View File

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

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

View File

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

View File

@ -18,6 +18,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Models; using osu.Game.Models;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
@ -195,12 +196,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestDownloadButtonHiddenWhenBeatmapExists() public void TestDownloadButtonHiddenWhenBeatmapExists()
{ {
var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo;
Live<BeatmapSetInfo> imported = null; 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())); createPlaylistWithBeatmaps(() => imported.PerformRead(s => s.Beatmaps.Detach()));
@ -245,14 +249,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestExpiredItems() 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)
{ {
@ -277,8 +277,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
new APIMod(new OsuModAutoplay()) new APIMod(new OsuModAutoplay())
} }
} }
} });
};
}); });
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded)); 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", => AddAssert($"delete button {index} {(visible ? "is" : "is not")} visible",
() => (playlist.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistRemoveButton>().ElementAt(2 + index * 2).Alpha > 0) == 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) private void createPlaylist(Action<TestPlaylist> setupPlaylist = null)
{ {
AddStep("create playlist", () => AddStep("create playlist", () =>
{ {
Child = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = playlist = new TestPlaylist Child = playlist = new TestPlaylist
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(500, 300) Size = new Vector2(500, 300)
}
}; };
setupPlaylist?.Invoke(playlist);
for (int i = 0; i < 20; i++) for (int i = 0; i < 20; i++)
{ {
playlist.Items.Add(new PlaylistItem(i % 2 == 1 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)); setupPlaylist?.Invoke(playlist);
}
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())
}
});
}
}); });
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded)); 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.Graphics.Sprites;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online; using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
@ -368,12 +369,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
ParticipantsList? participantsList = null; ParticipantsList? participantsList = null;
AddStep("create new list", () => Child = participantsList = new ParticipantsList AddStep("create new list", () => Child = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = participantsList = new ParticipantsList
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Size = new Vector2(380, 0.7f) Size = new Vector2(380, 0.7f)
}
}); });
AddUntilStep("wait for list to load", () => participantsList?.IsLoaded == true); 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;
using osu.Game.Overlays.BeatmapSet.Scores; using osu.Game.Overlays.BeatmapSet.Scores;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users; using osu.Game.Users;
using osuTK.Graphics; using osuTK.Graphics;
@ -146,12 +147,12 @@ namespace osu.Game.Tests.Visual.Online
{ {
var scores = new APIScoresCollection var scores = new APIScoresCollection
{ {
Scores = new List<APIScore> Scores = new List<SoloScoreInfo>
{ {
new APIScore new SoloScoreInfo
{ {
Date = DateTimeOffset.Now, EndedAt = DateTimeOffset.Now,
OnlineID = onlineID++, ID = onlineID++,
User = new APIUser User = new APIUser
{ {
Id = 6602580, Id = 6602580,
@ -175,10 +176,10 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234567890, TotalScore = 1234567890,
Accuracy = 1, Accuracy = 1,
}, },
new APIScore new SoloScoreInfo
{ {
Date = DateTimeOffset.Now, EndedAt = DateTimeOffset.Now,
OnlineID = onlineID++, ID = onlineID++,
User = new APIUser User = new APIUser
{ {
Id = 4608074, Id = 4608074,
@ -201,10 +202,10 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234789, TotalScore = 1234789,
Accuracy = 0.9997, Accuracy = 0.9997,
}, },
new APIScore new SoloScoreInfo
{ {
Date = DateTimeOffset.Now, EndedAt = DateTimeOffset.Now,
OnlineID = onlineID++, ID = onlineID++,
User = new APIUser User = new APIUser
{ {
Id = 1014222, Id = 1014222,
@ -226,10 +227,10 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 12345678, TotalScore = 12345678,
Accuracy = 0.9854, Accuracy = 0.9854,
}, },
new APIScore new SoloScoreInfo
{ {
Date = DateTimeOffset.Now, EndedAt = DateTimeOffset.Now,
OnlineID = onlineID++, ID = onlineID++,
User = new APIUser User = new APIUser
{ {
Id = 1541390, Id = 1541390,
@ -250,10 +251,10 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234567, TotalScore = 1234567,
Accuracy = 0.8765, Accuracy = 0.8765,
}, },
new APIScore new SoloScoreInfo
{ {
Date = DateTimeOffset.Now, EndedAt = DateTimeOffset.Now,
OnlineID = onlineID++, ID = onlineID++,
User = new APIUser User = new APIUser
{ {
Id = 7151382, Id = 7151382,
@ -275,12 +276,12 @@ namespace osu.Game.Tests.Visual.Online
foreach (var s in scores.Scores) foreach (var s in scores.Scores)
{ {
s.Statistics = new Dictionary<string, int> s.Statistics = new Dictionary<HitResult, int>
{ {
{ "count_300", RNG.Next(2000) }, { HitResult.Great, RNG.Next(2000) },
{ "count_100", RNG.Next(2000) }, { HitResult.Ok, RNG.Next(2000) },
{ "count_50", RNG.Next(2000) }, { HitResult.Meh, RNG.Next(2000) },
{ "count_miss", RNG.Next(2000) } { HitResult.Miss, RNG.Next(2000) }
}; };
} }
@ -289,10 +290,10 @@ namespace osu.Game.Tests.Visual.Online
private APIScoreWithPosition createUserBest() => new APIScoreWithPosition private APIScoreWithPosition createUserBest() => new APIScoreWithPosition
{ {
Score = new APIScore Score = new SoloScoreInfo
{ {
Date = DateTimeOffset.Now, EndedAt = DateTimeOffset.Now,
OnlineID = onlineID++, ID = onlineID++,
User = new APIUser User = new APIUser
{ {
Id = 7151382, Id = 7151382,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@ -20,7 +20,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.Editors namespace osu.Game.Tournament.Screens.Editors
{ {
public abstract class TournamentEditorScreen<TDrawable, TModel> : TournamentScreen, IProvideVideo public abstract class TournamentEditorScreen<TDrawable, TModel> : TournamentScreen
where TDrawable : Drawable, IModelBacked<TModel> where TDrawable : Drawable, IModelBacked<TModel>
where TModel : class, new() where TModel : class, new()
{ {

View File

@ -16,6 +16,10 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
{ {
private readonly TeamScore score; private readonly TeamScore score;
private readonly TournamentSpriteTextWithBackground teamText;
private readonly Bindable<string> teamName = new Bindable<string>("???");
private bool showScore; private bool showScore;
public bool ShowScore public bool ShowScore
@ -93,7 +97,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
} }
} }
}, },
new TournamentSpriteTextWithBackground(team?.FullName.Value ?? "???") teamText = new TournamentSpriteTextWithBackground
{ {
Scale = new Vector2(0.5f), Scale = new Vector2(0.5f),
Origin = anchor, Origin = anchor,
@ -113,6 +117,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
updateDisplay(); updateDisplay();
FinishTransforms(true); FinishTransforms(true);
if (Team != null)
teamName.BindTo(Team.FullName);
teamName.BindValueChanged(name => teamText.Text.Text = name.NewValue, true);
} }
private void updateDisplay() private void updateDisplay()

View File

@ -42,6 +42,8 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
currentMatch.BindTo(ladder.CurrentMatch); currentMatch.BindTo(ladder.CurrentMatch);
currentMatch.BindValueChanged(matchChanged); currentMatch.BindValueChanged(matchChanged);
currentTeam.BindValueChanged(teamChanged);
updateMatch(); updateMatch();
} }
@ -67,7 +69,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
// team may change to same team, which means score is not in a good state. // team may change to same team, which means score is not in a good state.
// thus we handle this manually. // thus we handle this manually.
teamChanged(currentTeam.Value); currentTeam.TriggerChange();
} }
protected override bool OnMouseDown(MouseDownEvent e) protected override bool OnMouseDown(MouseDownEvent e)
@ -88,11 +90,11 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components
return base.OnMouseDown(e); return base.OnMouseDown(e);
} }
private void teamChanged(TournamentTeam team) private void teamChanged(ValueChangedEvent<TournamentTeam> team)
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
teamDisplay = new TeamDisplay(team, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0), teamDisplay = new TeamDisplay(team.NewValue, teamColour, currentTeamScore, currentMatch.Value?.PointsToWin ?? 0),
}; };
} }
} }

View File

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

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 => editorInfo.Selected.ValueChanged += selection =>
{ {
// ensure any ongoing edits are committed out to the *current* selection before changing to a new one.
GetContainingInputManager().TriggerFocusContention(null);
roundDropdown.Current = selection.NewValue?.Round; roundDropdown.Current = selection.NewValue?.Round;
losersCheckbox.Current = selection.NewValue?.Losers; losersCheckbox.Current = selection.NewValue?.Losers;
dateTimeBox.Current = selection.NewValue?.Date; dateTimeBox.Current = selection.NewValue?.Date;

View File

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

View File

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

View File

@ -9,6 +9,8 @@ using osu.Framework.Bindables;
using osu.Framework.Configuration; using osu.Framework.Configuration;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -19,7 +21,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.Setup namespace osu.Game.Tournament.Screens.Setup
{ {
public class SetupScreen : TournamentScreen, IProvideVideo public class SetupScreen : TournamentScreen
{ {
private FillFlowContainer fillFlow; private FillFlowContainer fillFlow;
@ -48,13 +50,21 @@ namespace osu.Game.Tournament.Screens.Setup
{ {
windowSize = frameworkConfig.GetBindable<Size>(FrameworkSetting.WindowedSize); windowSize = frameworkConfig.GetBindable<Size>(FrameworkSetting.WindowedSize);
InternalChild = fillFlow = new FillFlowContainer InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(0.2f),
},
fillFlow = new FillFlowContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Padding = new MarginPadding(10), Padding = new MarginPadding(10),
Spacing = new Vector2(10), Spacing = new Vector2(10),
}
}; };
api.LocalUser.BindValueChanged(_ => Schedule(reload)); api.LocalUser.BindValueChanged(_ => Schedule(reload));
@ -74,7 +84,8 @@ namespace osu.Game.Tournament.Screens.Setup
Action = () => sceneManager?.SetScreen(new StablePathSelectScreen()), Action = () => sceneManager?.SetScreen(new StablePathSelectScreen()),
Value = fileBasedIpc?.IPCStorage?.GetFullPath(string.Empty) ?? "Not found", Value = fileBasedIpc?.IPCStorage?.GetFullPath(string.Empty) ?? "Not found",
Failing = fileBasedIpc?.IPCStorage == null, Failing = fileBasedIpc?.IPCStorage == null,
Description = "The osu!stable installation which is currently being used as a data source. If a source is not found, make sure you have created an empty ipc.txt in your stable cutting-edge installation." Description =
"The osu!stable installation which is currently being used as a data source. If a source is not found, make sure you have created an empty ipc.txt in your stable cutting-edge installation."
}, },
new ActionableInfo new ActionableInfo
{ {

View File

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

View File

@ -3,6 +3,7 @@
#nullable disable #nullable disable
using System.Diagnostics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -19,7 +20,7 @@ using osuTK;
namespace osu.Game.Tournament.Screens.TeamIntro namespace osu.Game.Tournament.Screens.TeamIntro
{ {
public class SeedingScreen : TournamentMatchScreen, IProvideVideo public class SeedingScreen : TournamentMatchScreen
{ {
private Container mainContainer; private Container mainContainer;
@ -69,7 +70,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
currentTeam.BindValueChanged(teamChanged, true); currentTeam.BindValueChanged(teamChanged, true);
} }
private void teamChanged(ValueChangedEvent<TournamentTeam> team) private void teamChanged(ValueChangedEvent<TournamentTeam> team) => Scheduler.AddOnce(() =>
{ {
if (team.NewValue == null) if (team.NewValue == null)
{ {
@ -78,7 +79,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro
} }
showTeam(team.NewValue); showTeam(team.NewValue);
} });
protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match) protected override void CurrentMatchChanged(ValueChangedEvent<TournamentMatch> match)
{ {
@ -120,15 +121,23 @@ namespace osu.Game.Tournament.Screens.TeamIntro
foreach (var seeding in team.SeedingResults) foreach (var seeding in team.SeedingResults)
{ {
fill.Add(new ModRow(seeding.Mod.Value, seeding.Seed.Value)); fill.Add(new ModRow(seeding.Mod.Value, seeding.Seed.Value));
foreach (var beatmap in seeding.Beatmaps) foreach (var beatmap in seeding.Beatmaps)
{
if (beatmap.Beatmap == null)
continue;
fill.Add(new BeatmapScoreRow(beatmap)); fill.Add(new BeatmapScoreRow(beatmap));
} }
} }
}
private class BeatmapScoreRow : CompositeDrawable private class BeatmapScoreRow : CompositeDrawable
{ {
public BeatmapScoreRow(SeedingBeatmap beatmap) public BeatmapScoreRow(SeedingBeatmap beatmap)
{ {
Debug.Assert(beatmap.Beatmap != null);
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
@ -157,7 +166,8 @@ namespace osu.Game.Tournament.Screens.TeamIntro
Children = new Drawable[] Children = new Drawable[]
{ {
new TournamentSpriteText { Text = beatmap.Score.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Width = 80 }, new TournamentSpriteText { Text = beatmap.Score.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Width = 80 },
new TournamentSpriteText { Text = "#" + beatmap.Seed.Value.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) }, new TournamentSpriteText
{ Text = "#" + beatmap.Seed.Value.ToString("#,0"), Colour = TournamentGame.TEXT_COLOUR, Font = OsuFont.Torus.With(weight: FontWeight.Regular) },
} }
}, },
}; };

View File

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

View File

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

View File

@ -11,8 +11,6 @@ using osu.Framework.Configuration;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Handlers.Mouse; using osu.Framework.Input.Handlers.Mouse;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
@ -20,11 +18,11 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Tournament namespace osu.Game.Tournament
{ {
[Cached]
public class TournamentGame : TournamentGameBase public class TournamentGame : TournamentGameBase
{ {
public static ColourInfo GetTeamColour(TeamColour teamColour) => teamColour == TeamColour.Red ? COLOUR_RED : COLOUR_BLUE; public static ColourInfo GetTeamColour(TeamColour teamColour) => teamColour == TeamColour.Red ? COLOUR_RED : COLOUR_BLUE;
@ -78,40 +76,9 @@ namespace osu.Game.Tournament
LoadComponentsAsync(new[] LoadComponentsAsync(new[]
{ {
new Container new SaveChangesOverlay
{ {
CornerRadius = 10,
Depth = float.MinValue, 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") 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) if (!bracketLoadTaskCompletionSource.Task.IsCompletedSuccessfully)
{ {
@ -311,7 +311,16 @@ namespace osu.Game.Tournament
.ToList(); .ToList();
// Serialise before opening stream for writing, so if there's a failure it will leave the file in the previous state. // 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 new JsonSerializerSettings
{ {
Formatting = Formatting.Indented, Formatting = Formatting.Indented,
@ -319,10 +328,6 @@ namespace osu.Game.Tournament
DefaultValueHandling = DefaultValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore,
Converters = new JsonConverter[] { new JsonPointConverter() } 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(); 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.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Testing;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -186,7 +187,7 @@ namespace osu.Game.Tournament
var lastScreen = currentScreen; var lastScreen = currentScreen;
currentScreen = target; currentScreen = target;
if (currentScreen is IProvideVideo) if (currentScreen.ChildrenOfType<TourneyVideo>().FirstOrDefault()?.VideoAvailable == true)
{ {
video.FadeOut(200); video.FadeOut(200);

View File

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

View File

@ -80,9 +80,8 @@ namespace osu.Game.Beatmaps
if (beatmapSet.OnlineID > 0) if (beatmapSet.OnlineID > 0)
{ {
var existingSetWithSameOnlineID = realm.All<BeatmapSetInfo>().SingleOrDefault(b => b.OnlineID == beatmapSet.OnlineID); // 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))
if (existingSetWithSameOnlineID != null)
{ {
existingSetWithSameOnlineID.DeletePending = true; existingSetWithSameOnlineID.DeletePending = true;
existingSetWithSameOnlineID.OnlineID = -1; existingSetWithSameOnlineID.OnlineID = -1;
@ -90,7 +89,7 @@ namespace osu.Game.Beatmaps
foreach (var b in existingSetWithSameOnlineID.Beatmaps) foreach (var b in existingSetWithSameOnlineID.Beatmaps)
b.OnlineID = -1; 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. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Diagnostics;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -110,6 +111,11 @@ namespace osu.Game.Beatmaps
public bool SamplesMatchPlaybackRate { get; set; } = true; public bool SamplesMatchPlaybackRate { get; set; } = true;
/// <summary>
/// The time at which this beatmap was last played by the local user.
/// </summary>
public DateTimeOffset? LastPlayed { get; set; }
/// <summary> /// <summary>
/// The ratio of distance travelled per time unit. /// The ratio of distance travelled per time unit.
/// Generally used to decouple the spacing between hit objects from the enforced "velocity" of the beatmap (see <see cref="DifficultyControlPoint.SliderVelocity"/>). /// Generally used to decouple the spacing between hit objects from the enforced "velocity" of the beatmap (see <see cref="DifficultyControlPoint.SliderVelocity"/>).
@ -151,14 +157,23 @@ namespace osu.Game.Beatmaps
public bool AudioEquals(BeatmapInfo? other) => other != null public bool AudioEquals(BeatmapInfo? other) => other != null
&& BeatmapSet != null && BeatmapSet != null
&& other.BeatmapSet != null && other.BeatmapSet != null
&& BeatmapSet.Hash == other.BeatmapSet.Hash && compareFiles(this, other, m => m.AudioFile);
&& Metadata.AudioFile == other.Metadata.AudioFile;
public bool BackgroundEquals(BeatmapInfo? other) => other != null public bool BackgroundEquals(BeatmapInfo? other) => other != null
&& BeatmapSet != null && BeatmapSet != null
&& other.BeatmapSet != null && other.BeatmapSet != null
&& BeatmapSet.Hash == other.BeatmapSet.Hash && compareFiles(this, other, m => m.BackgroundFile);
&& Metadata.BackgroundFile == other.Metadata.BackgroundFile;
private static bool compareFiles(BeatmapInfo x, BeatmapInfo y, Func<IBeatmapMetadataInfo, string> getFilename)
{
Debug.Assert(x.BeatmapSet != null);
Debug.Assert(y.BeatmapSet != null);
string? fileHashX = x.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(x.BeatmapSet.Metadata))?.File.Hash;
string? fileHashY = y.BeatmapSet.Files.FirstOrDefault(f => f.Filename == getFilename(y.BeatmapSet.Metadata))?.File.Hash;
return fileHashX == fileHashY;
}
IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata; IBeatmapMetadataInfo IBeatmapInfo.Metadata => Metadata;
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet; IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;

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

View File

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

View File

@ -19,7 +19,7 @@ namespace osu.Game.IO
public LineBufferedReader(Stream stream, bool leaveOpen = false) 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> /// <summary>

View File

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

View File

@ -38,7 +38,7 @@ namespace osu.Game.Online.API
public string WebsiteRootUrl { get; } 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; } public Exception LastLoginError { get; private set; }

View File

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

View File

@ -11,7 +11,7 @@ namespace osu.Game.Online.API.Requests.Responses
public class APIScoresCollection public class APIScoresCollection
{ {
[JsonProperty(@"scores")] [JsonProperty(@"scores")]
public List<APIScore> Scores; public List<SoloScoreInfo> Scores;
[JsonProperty(@"userScore")] [JsonProperty(@"userScore")]
public APIScoreWithPosition 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()); dependencies.Cache(difficultyCache = new BeatmapDifficultyCache());
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, difficultyCache, LocalConfig)); dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, realm, Scheduler, API, difficultyCache, LocalConfig));
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, realm, RulesetStore, API, Audio, Resources, Host, defaultBeatmap, difficultyCache, performOnlineLookups: true)); dependencies.Cache(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 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(() => .ContinueWith(task => Schedule(() =>
{ {
if (loadCancellationSource.IsCancellationRequested) if (loadCancellationSource.IsCancellationRequested)
@ -101,7 +101,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
scoreTable.Show(); scoreTable.Show();
var userScore = value.UserScore; var userScore = value.UserScore;
var userScoreInfo = userScore?.Score.CreateScoreInfo(rulesets, beatmapInfo); var userScoreInfo = userScore?.Score.ToScoreInfo(rulesets, beatmapInfo);
topScoresContainer.Add(new DrawableTopScore(topScore)); topScoresContainer.Add(new DrawableTopScore(topScore));

View File

@ -49,43 +49,54 @@ namespace osu.Game.Overlays
public void Push(PopupDialog dialog) public void Push(PopupDialog dialog)
{ {
if (dialog == CurrentDialog || dialog.State.Value != Visibility.Visible) return; if (dialog == CurrentDialog || dialog.State.Value == Visibility.Hidden) return;
var lastDialog = CurrentDialog;
// Immediately update the externally accessible property as this may be used for checks even before // Immediately update the externally accessible property as this may be used for checks even before
// a DialogOverlay instance has finished loading. // a DialogOverlay instance has finished loading.
var lastDialog = CurrentDialog;
CurrentDialog = dialog; CurrentDialog = dialog;
Scheduler.Add(() => Schedule(() =>
{ {
// if any existing dialog is being displayed, dismiss it before showing a new one. // if any existing dialog is being displayed, dismiss it before showing a new one.
lastDialog?.Hide(); 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(); 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; public override bool IsPresent => Scheduler.HasPendingTasks || dialogContainer.Children.Count > 0;
protected override bool BlockNonPositionalInput => true; 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() protected override void PopIn()
{ {
base.PopIn(); base.PopIn();
@ -97,7 +108,8 @@ namespace osu.Game.Overlays
base.PopOut(); base.PopOut();
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF, 100, Easing.InCubic); 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(); CurrentDialog.Hide();
} }

View File

@ -1,14 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable using System;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Configuration;
using osu.Framework.Extensions;
using osu.Framework.Graphics; 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.Localisation;
using osu.Framework.Threading;
using osu.Game.Extensions;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Localisation; using osu.Game.Localisation;
using osuTK;
namespace osu.Game.Overlays.FirstRunSetup namespace osu.Game.Overlays.FirstRunSetup
{ {
@ -19,6 +29,19 @@ namespace osu.Game.Overlays.FirstRunSetup
private void load() private void load()
{ {
Content.Children = new Drawable[] Content.Children = new Drawable[]
{
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)) new OsuTextFlowContainer(cp => cp.Font = OsuFont.Default.With(size: CONTENT_FONT_SIZE))
{ {
@ -26,7 +49,156 @@ namespace osu.Game.Overlays.FirstRunSetup
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y AutoSizeAxes = Axes.Y
}, },
},
}
},
new LanguageSelectionFlow
{
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 osu.Game.Rulesets.Mods;
using osuTK; using osuTK;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Localisation; using osu.Framework.Localisation;
namespace osu.Game.Rulesets.UI namespace osu.Game.Rulesets.UI
@ -53,7 +54,6 @@ namespace osu.Game.Rulesets.UI
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
private Color4 backgroundColour; private Color4 backgroundColour;
private Color4 highlightedColour;
/// <summary> /// <summary>
/// Construct a new instance. /// Construct a new instance.
@ -123,47 +123,13 @@ namespace osu.Game.Rulesets.UI
modAcronym.FadeOut(); modAcronym.FadeOut();
} }
switch (value.Type) backgroundColour = colours.ForModType(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;
}
updateColour(); updateColour();
} }
private void 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.IO.Archives;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Scoring.Legacy; 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; using Realms;
namespace osu.Game.Scoring namespace osu.Game.Scoring
@ -26,11 +29,14 @@ namespace osu.Game.Scoring
private readonly RulesetStore rulesets; private readonly RulesetStore rulesets;
private readonly Func<BeatmapManager> beatmaps; 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) : base(storage, realm)
{ {
this.rulesets = rulesets; this.rulesets = rulesets;
this.beatmaps = beatmaps; this.beatmaps = beatmaps;
this.api = api;
} }
protected override ScoreInfo? CreateModel(ArchiveReader archive) protected override ScoreInfo? CreateModel(ArchiveReader archive)
@ -68,5 +74,17 @@ namespace osu.Game.Scoring
if (string.IsNullOrEmpty(model.StatisticsJson)) if (string.IsNullOrEmpty(model.StatisticsJson))
model.StatisticsJson = JsonConvert.SerializeObject(model.Statistics); 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.Overlays.Notifications;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Online.API;
namespace osu.Game.Scoring namespace osu.Game.Scoring
{ {
@ -31,7 +32,7 @@ namespace osu.Game.Scoring
private readonly OsuConfigManager configManager; private readonly OsuConfigManager configManager;
private readonly ScoreImporter scoreImporter; 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) BeatmapDifficultyCache difficultyCache = null, OsuConfigManager configManager = null)
: base(storage, realm) : base(storage, realm)
{ {
@ -39,7 +40,7 @@ namespace osu.Game.Scoring
this.difficultyCache = difficultyCache; this.difficultyCache = difficultyCache;
this.configManager = configManager; this.configManager = configManager;
scoreImporter = new ScoreImporter(rulesets, beatmaps, storage, realm) scoreImporter = new ScoreImporter(rulesets, beatmaps, storage, realm, api)
{ {
PostNotification = obj => PostNotification?.Invoke(obj) PostNotification = obj => PostNotification?.Invoke(obj)
}; };

View File

@ -186,7 +186,7 @@ namespace osu.Game.Screens.Edit
loadableBeatmap = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value); loadableBeatmap = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value);
// required so we can get the track length in EditorClock. // 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(); loadableBeatmap.LoadTrack();
// this is a bit haphazard, but guards against setting the lease Beatmap bindable if // 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() private void cancelExit()
{ {
samplePlaybackDisabled.Value = false; updateSampleDisabledState();
loader?.CancelPendingDifficultySwitch(); loader?.CancelPendingDifficultySwitch();
} }

View File

@ -35,7 +35,13 @@ namespace osu.Game.Screens.Edit.GameplayTest
ScoreProcessor.HasCompleted.BindValueChanged(completed => ScoreProcessor.HasCompleted.BindValueChanged(completed =>
{ {
if (completed.NewValue) 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, RelativeSizeAxes = Axes.Both,
Clock = decoupledClock, Clock = decoupledClock,
LoadMenu = LoadMenu 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) if (!UsingThemedIntro)
welcome?.Play(); welcome?.Play();

View File

@ -9,17 +9,20 @@ using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Collections;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
@ -27,6 +30,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online; using osu.Game.Online;
using osu.Game.Online.Chat; using osu.Game.Online.Chat;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet; using osu.Game.Overlays.BeatmapSet;
using osu.Game.Resources.Localisation.Web; using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -38,7 +42,7 @@ using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay namespace osu.Game.Screens.OnlinePlay
{ {
public class DrawableRoomPlaylistItem : OsuRearrangeableListItem<PlaylistItem> public class DrawableRoomPlaylistItem : OsuRearrangeableListItem<PlaylistItem>, IHasContextMenu
{ {
public const float HEIGHT = 50; public const float HEIGHT = 50;
@ -93,6 +97,9 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved] [Resolved]
private RulesetStore rulesets { get; set; } private RulesetStore rulesets { get; set; }
[Resolved]
private BeatmapManager beatmaps { get; set; }
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
@ -102,6 +109,15 @@ namespace osu.Game.Screens.OnlinePlay
[Resolved] [Resolved]
private BeatmapLookupCache beatmapLookupCache { get; set; } 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; protected override bool ShouldBeConsideredForInput(Drawable child) => AllowReordering || AllowDeletion || !AllowSelection || SelectedItem.Value == Model;
public DrawableRoomPlaylistItem(PlaylistItem item) public DrawableRoomPlaylistItem(PlaylistItem item)
@ -433,7 +449,7 @@ namespace osu.Game.Screens.OnlinePlay
} }
} }
}, },
} },
}; };
} }
@ -470,6 +486,31 @@ namespace osu.Game.Screens.OnlinePlay
return true; 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 class PlaylistEditButton : GrayButton
{ {
public PlaylistEditButton() public PlaylistEditButton()

View File

@ -314,6 +314,9 @@ namespace osu.Game.Screens.OnlinePlay.Match
public override void OnSuspending(ScreenTransitionEvent e) 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(); onLeaving();
base.OnSuspending(e); base.OnSuspending(e);
} }

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