1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 12:17:26 +08:00

Merge branch 'master' into fix-mania-long-note-regression

This commit is contained in:
Bartłomiej Dach 2023-02-14 21:25:31 +01:00 committed by GitHub
commit 99b78c63a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
60 changed files with 1393 additions and 413 deletions

View File

@ -17,7 +17,7 @@
<EmbeddedResource Include="Resources\**\*.*" />
</ItemGroup>
<ItemGroup Label="Code Analysis">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.3" PrivateAssets="All" />
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" PrivateAssets="All" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\BannedSymbols.txt" />
</ItemGroup>
<PropertyGroup Label="Code Analysis">

View File

@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.EmptyFreeform\osu.Game.Rulesets.EmptyFreeform.csproj" />

View File

@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />

View File

@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.EmptyScrolling\osu.Game.Rulesets.EmptyScrolling.csproj" />

View File

@ -9,9 +9,9 @@
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\osu.Game.Rulesets.Pippidon\osu.Game.Rulesets.Pippidon.csproj" />

View File

@ -98,7 +98,7 @@ namespace osu.Desktop
if (status.Value is UserStatusOnline && activity.Value != null)
{
presence.State = truncate(activity.Value.Status);
presence.State = truncate(activity.Value.GetStatus(privacyMode.Value == DiscordRichPresenceMode.Limited));
presence.Details = truncate(getDetails(activity.Value));
if (getBeatmap(activity.Value) is IBeatmapInfo beatmap && beatmap.OnlineID > 0)
@ -169,7 +169,7 @@ namespace osu.Desktop
case UserActivity.InGame game:
return game.BeatmapInfo;
case UserActivity.Editing edit:
case UserActivity.EditingBeatmap edit:
return edit.BeatmapInfo;
}
@ -183,9 +183,12 @@ namespace osu.Desktop
case UserActivity.InGame game:
return game.BeatmapInfo.ToString() ?? string.Empty;
case UserActivity.Editing edit:
case UserActivity.EditingBeatmap edit:
return edit.BeatmapInfo.ToString() ?? string.Empty;
case UserActivity.WatchingReplay watching:
return watching.BeatmapInfo.ToString();
case UserActivity.InLobby lobby:
return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value;
}

View File

@ -26,8 +26,8 @@
<ItemGroup Label="Package References">
<PackageReference Include="Clowd.Squirrel" Version="2.9.42" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="System.IO.Packaging" Version="6.0.0" />
<PackageReference Include="DiscordRichPresence" Version="1.1.1.14" />
<PackageReference Include="System.IO.Packaging" Version="7.0.0" />
<PackageReference Include="DiscordRichPresence" Version="1.1.3.18" />
</ItemGroup>
<ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" />

View File

@ -7,9 +7,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.4" />
<PackageReference Include="nunit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup>
<ItemGroup>

View File

@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Moq" Version="4.18.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="Moq" Version="4.18.4" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -25,6 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
[TestCase("slider-conversion-v6")]
[TestCase("slider-conversion-v14")]
[TestCase("slider-generating-drumroll-2")]
[TestCase("file-hitsamples")]
public void Test(string name) => base.Test(name);
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)

View File

@ -1,9 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -2,13 +2,15 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset<TaikoHitObject>
public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset<TaikoHitObject>, IApplicableToDrawableHitObject
{
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
{
@ -18,5 +20,11 @@ namespace osu.Game.Rulesets.Taiko.Mods
var playfield = (TaikoPlayfield)drawableRuleset.Playfield;
playfield.ClassicHitTargetPosition.Value = true;
}
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{
if (drawable is DrawableTaikoHitObject hit)
hit.SnapJudgementLocation = true;
}
}
}

View File

@ -207,6 +207,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
const float gravity_time = 300;
const float gravity_travel_height = 200;
if (SnapJudgementLocation)
MainPiece.MoveToX(-X);
this.ScaleTo(0.8f, gravity_time * 2, Easing.OutQuad);
this.MoveToY(-gravity_travel_height, gravity_time, Easing.Out)

View File

@ -25,6 +25,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
private readonly Container nonProxiedContent;
/// <summary>
/// Whether the location of the hit should be snapped to the hit target before animating.
/// </summary>
/// <remarks>
/// This is how osu-stable worked, but notably is not how TnT works.
/// Not snapping results in less visual feedback on hit accuracy.
/// </remarks>
public bool SnapJudgementLocation { get; set; }
protected DrawableTaikoHitObject([CanBeNull] TaikoHitObject hitObject)
: base(hitObject)
{

View File

@ -0,0 +1 @@
{"Mappings":[{"StartTime":500.0,"Objects":[{"StartTime":500.0,"EndTime":500.0,"IsRim":false,"IsCentre":true,"IsDrumRoll":false,"IsSwell":false,"IsStrong":false}]},{"StartTime":1000.0,"Objects":[{"StartTime":1000.0,"EndTime":1000.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":false}]},{"StartTime":1500.0,"Objects":[{"StartTime":1500.0,"EndTime":1500.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":false}]},{"StartTime":2000.0,"Objects":[{"StartTime":2000.0,"EndTime":2000.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":false}]},{"StartTime":2500.0,"Objects":[{"StartTime":2500.0,"EndTime":2500.0,"IsRim":false,"IsCentre":true,"IsDrumRoll":false,"IsSwell":false,"IsStrong":true}]},{"StartTime":3000.0,"Objects":[{"StartTime":3000.0,"EndTime":3000.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":true}]},{"StartTime":3500.0,"Objects":[{"StartTime":3500.0,"EndTime":3500.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":true}]},{"StartTime":4000.0,"Objects":[{"StartTime":4000.0,"EndTime":4000.0,"IsRim":true,"IsCentre":false,"IsDrumRoll":false,"IsSwell":false,"IsStrong":true}]}]}

View File

@ -0,0 +1,22 @@
osu file format v14
[Difficulty]
HPDrainRate:5
CircleSize:7
OverallDifficulty:6.5
ApproachRate:10
SliderMultiplier:1.9
SliderTickRate:1
[TimingPoints]
500,500,4,2,1,50,1,0
[HitObjects]
256,192,500,1,0,0:0:0:0:sample.ogg
256,192,1000,1,8,0:0:0:0:sample.ogg
256,192,1500,1,2,0:0:0:0:sample.ogg
256,192,2000,1,10,0:0:0:0:sample.ogg
256,192,2500,1,4,0:0:0:0:sample.ogg
256,192,3000,1,12,0:0:0:0:sample.ogg
256,192,3500,1,6,0:0:0:0:sample.ogg
256,192,4000,1,14,0:0:0:0:sample.ogg

View File

@ -209,9 +209,8 @@ namespace osu.Game.Rulesets.Taiko
HitResult.Great,
HitResult.Ok,
HitResult.SmallTickHit,
HitResult.SmallBonus,
HitResult.LargeBonus,
};
}
@ -220,6 +219,9 @@ namespace osu.Game.Rulesets.Taiko
switch (result)
{
case HitResult.SmallBonus:
return "drum tick";
case HitResult.LargeBonus:
return "bonus";
}

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Screens.Play.Break;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneLetterboxOverlay : OsuTestScene
{
public TestSceneLetterboxOverlay()
{
AddRange(new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both
},
new LetterboxOverlay()
});
}
}
}

View File

@ -1,32 +1,64 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Linq;
using NUnit.Framework;
using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Play;
using osu.Game.Tests.Beatmaps;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
public partial class TestSceneReplayPlayer : RateAdjustedBeatmapTestScene
{
protected TestReplayPlayer Player;
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("Initialise player", () => Player = CreatePlayer(new OsuRuleset()));
AddStep("Load player", () => LoadScreen(Player));
AddUntilStep("player loaded", () => Player.IsLoaded);
}
protected TestReplayPlayer Player = null!;
[Test]
public void TestPauseViaSpace()
{
loadPlayerWithBeatmap();
double? lastTime = null;
AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
AddStep("Pause playback with space", () => InputManager.Key(Key.Space));
AddAssert("player not exited", () => Player.IsCurrentScreen());
AddUntilStep("Time stopped progressing", () =>
{
double current = Player.GameplayClockContainer.CurrentTime;
bool changed = lastTime != current;
lastTime = current;
return !changed;
});
AddWaitStep("wait some", 10);
AddAssert("Time still stopped", () => lastTime == Player.GameplayClockContainer.CurrentTime);
}
[Test]
public void TestPauseViaSpaceWithSkip()
{
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo)
{
BeatmapInfo = { AudioLeadIn = 60000 }
});
AddUntilStep("wait for skip overlay", () => Player.ChildrenOfType<SkipOverlay>().First().IsButtonVisible);
AddStep("Skip with space", () => InputManager.Key(Key.Space));
AddAssert("Player not paused", () => !Player.DrawableRuleset.IsPaused.Value);
double? lastTime = null;
AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
@ -52,6 +84,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestPauseViaMiddleMouse()
{
loadPlayerWithBeatmap();
double? lastTime = null;
AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
@ -77,6 +111,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestSeekBackwards()
{
loadPlayerWithBeatmap();
double? lastTime = null;
AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
@ -93,6 +129,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestSeekForwards()
{
loadPlayerWithBeatmap();
double? lastTime = null;
AddUntilStep("wait for first hit", () => Player.ScoreProcessor.TotalScore.Value > 0);
@ -106,12 +144,26 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("Jumped forwards", () => Player.GameplayClockContainer.CurrentTime - lastTime > 500);
}
protected TestReplayPlayer CreatePlayer(Ruleset ruleset)
private void loadPlayerWithBeatmap(IBeatmap? beatmap = null)
{
Beatmap.Value = CreateWorkingBeatmap(ruleset.RulesetInfo);
AddStep("create player", () =>
{
CreatePlayer(new OsuRuleset(), beatmap);
});
AddStep("Load player", () => LoadScreen(Player));
AddUntilStep("player loaded", () => Player.IsLoaded);
}
protected void CreatePlayer(Ruleset ruleset, IBeatmap? beatmap = null)
{
Beatmap.Value = beatmap != null
? CreateWorkingBeatmap(beatmap)
: CreateWorkingBeatmap(ruleset.RulesetInfo);
SelectedMods.Value = new[] { ruleset.GetAutoplayMod() };
return new TestReplayPlayer(false);
Player = new TestReplayPlayer(false);
}
}
}

View File

@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestEditActivity()
{
AddStep("Set activity", () => api.Activity.Value = new UserActivity.Editing(new BeatmapInfo()));
AddStep("Set activity", () => api.Activity.Value = new UserActivity.EditingBeatmap(new BeatmapInfo()));
AddStep("Run command", () => Add(new NowPlayingCommand(new Channel())));

View File

@ -11,6 +11,8 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osu.Game.Tests.Beatmaps;
using osu.Game.Users;
using osuTK;
@ -107,14 +109,16 @@ namespace osu.Game.Tests.Visual.Online
AddStep("set online status", () => status.Value = new UserStatusOnline());
AddStep("idle", () => activity.Value = null);
AddStep("spectating", () => activity.Value = new UserActivity.Spectating());
AddStep("watching replay", () => activity.Value = new UserActivity.WatchingReplay(createScore(@"nats")));
AddStep("spectating user", () => activity.Value = new UserActivity.SpectatingUser(createScore(@"mrekk")));
AddStep("solo (osu!)", () => activity.Value = soloGameStatusForRuleset(0));
AddStep("solo (osu!taiko)", () => activity.Value = soloGameStatusForRuleset(1));
AddStep("solo (osu!catch)", () => activity.Value = soloGameStatusForRuleset(2));
AddStep("solo (osu!mania)", () => activity.Value = soloGameStatusForRuleset(3));
AddStep("choosing", () => activity.Value = new UserActivity.ChoosingBeatmap());
AddStep("editing", () => activity.Value = new UserActivity.Editing(null));
AddStep("modding", () => activity.Value = new UserActivity.Modding());
AddStep("editing beatmap", () => activity.Value = new UserActivity.EditingBeatmap(null));
AddStep("modding beatmap", () => activity.Value = new UserActivity.ModdingBeatmap(null));
AddStep("testing beatmap", () => activity.Value = new UserActivity.TestingBeatmap(null, null));
}
[Test]
@ -132,6 +136,14 @@ namespace osu.Game.Tests.Visual.Online
private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(null, rulesetStore.GetRuleset(rulesetId));
private ScoreInfo createScore(string name) => new ScoreInfo(new TestBeatmap(Ruleset.Value).BeatmapInfo)
{
User = new APIUser
{
Username = name,
}
};
private partial class TestUserListPanel : UserListPanel
{
public TestUserListPanel(APIUser user)

View File

@ -24,17 +24,26 @@ namespace osu.Game.Tests.Visual.Ranking
{
public partial class TestSceneAccuracyCircle : OsuTestScene
{
[TestCase(0.2, ScoreRank.D)]
[TestCase(0.5, ScoreRank.D)]
[TestCase(0.75, ScoreRank.C)]
[TestCase(0.85, ScoreRank.B)]
[TestCase(0.925, ScoreRank.A)]
[TestCase(0.975, ScoreRank.S)]
[TestCase(0.9999, ScoreRank.S)]
[TestCase(1, ScoreRank.X)]
public void TestRank(double accuracy, ScoreRank rank)
[TestCase(0)]
[TestCase(0.2)]
[TestCase(0.5)]
[TestCase(0.6999)]
[TestCase(0.7)]
[TestCase(0.75)]
[TestCase(0.7999)]
[TestCase(0.8)]
[TestCase(0.85)]
[TestCase(0.8999)]
[TestCase(0.9)]
[TestCase(0.925)]
[TestCase(0.9499)]
[TestCase(0.95)]
[TestCase(0.975)]
[TestCase(0.9999)]
[TestCase(1)]
public void TestRank(double accuracy)
{
var score = createScore(accuracy, rank);
var score = createScore(accuracy, ScoreProcessor.RankFromAccuracy(accuracy));
addCircleStep(score);
}

View File

@ -0,0 +1,146 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Screens.Select.FooterV2;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect
{
public partial class TestSceneSongSelectFooterV2 : OsuManualInputManagerTestScene
{
private FooterButtonRandomV2 randomButton = null!;
private FooterButtonModsV2 modsButton = null!;
private bool nextRandomCalled;
private bool previousRandomCalled;
private DummyOverlay overlay = null!;
[Cached]
private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
[SetUp]
public void SetUp() => Schedule(() =>
{
nextRandomCalled = false;
previousRandomCalled = false;
FooterV2 footer;
Children = new Drawable[]
{
footer = new FooterV2
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
},
overlay = new DummyOverlay()
};
footer.AddButton(modsButton = new FooterButtonModsV2(), overlay);
footer.AddButton(randomButton = new FooterButtonRandomV2
{
NextRandom = () => nextRandomCalled = true,
PreviousRandom = () => previousRandomCalled = true
});
footer.AddButton(new FooterButtonOptionsV2());
overlay.Hide();
});
[Test]
public void TestState()
{
AddToggleStep("set options enabled state", state => this.ChildrenOfType<FooterButtonV2>().Last().Enabled.Value = state);
}
[Test]
public void TestFooterRandom()
{
AddStep("press F2", () => InputManager.Key(Key.F2));
AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled);
}
[Test]
public void TestFooterRandomViaMouse()
{
AddStep("click button", () =>
{
InputManager.MoveMouseTo(randomButton);
InputManager.Click(MouseButton.Left);
});
AddAssert("next random invoked", () => nextRandomCalled && !previousRandomCalled);
}
[Test]
public void TestFooterRewind()
{
AddStep("press Shift+F2", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.PressKey(Key.F2);
InputManager.ReleaseKey(Key.F2);
InputManager.ReleaseKey(Key.LShift);
});
AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
}
[Test]
public void TestFooterRewindViaShiftMouseLeft()
{
AddStep("shift + click button", () =>
{
InputManager.PressKey(Key.LShift);
InputManager.MoveMouseTo(randomButton);
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.LShift);
});
AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
}
[Test]
public void TestFooterRewindViaMouseRight()
{
AddStep("right click button", () =>
{
InputManager.MoveMouseTo(randomButton);
InputManager.Click(MouseButton.Right);
});
AddAssert("previous random invoked", () => previousRandomCalled && !nextRandomCalled);
}
[Test]
public void TestOverlayPresent()
{
AddStep("Press F1", () =>
{
InputManager.MoveMouseTo(modsButton);
InputManager.Click(MouseButton.Left);
});
AddAssert("Overlay visible", () => overlay.State.Value == Visibility.Visible);
AddStep("Hide", () => overlay.Hide());
}
private partial class DummyOverlay : ShearedOverlayContainer
{
public DummyOverlay()
: base(OverlayColourScheme.Green)
{
}
[BackgroundDependencyLoader]
private void load()
{
Header.Title = "An overlay";
}
}
}
}

View File

@ -2,11 +2,11 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="DeepEqual" Version="4.2.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="Nito.AsyncEx" Version="5.1.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="Moq" Version="4.18.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
<PackageReference Include="Moq" Version="4.18.4" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -4,9 +4,9 @@
<StartupObject>osu.Game.Tournament.Tests.TournamentTestRunner</StartupObject>
</PropertyGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.1" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.0" />
<PackageReference Include="NUnit3TestAdapter" Version="4.3.1" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -98,6 +98,9 @@ namespace osu.Game.Audio
Track.Stop();
// Ensure the track is reset immediately on stopping, so the next time it is started it has a correct time value.
Track.Seek(0);
Stopped?.Invoke();
}

View File

@ -100,9 +100,9 @@ namespace osu.Game.Graphics.Containers
/// <summary>
/// Abort any ongoing confirmation. Should be called when the container's interaction is no longer valid (ie. the user releases a key).
/// </summary>
protected void AbortConfirm()
protected virtual void AbortConfirm()
{
if (!AllowMultipleFires && Fired) return;
if (!confirming || (!AllowMultipleFires && Fired)) return;
confirming = false;
Fired = false;

View File

@ -46,8 +46,8 @@ namespace osu.Game.Graphics.Containers
AddRangeInternal(new Drawable[]
{
CreateHoverSounds(sampleSet),
content,
CreateHoverSounds(sampleSet)
});
}

View File

@ -35,8 +35,8 @@ namespace osu.Game.Input.Bindings
// It is used to decide the order of precedence, with the earlier items having higher precedence.
public override IEnumerable<IKeyBinding> DefaultKeyBindings => GlobalKeyBindings
.Concat(EditorKeyBindings)
.Concat(ReplayKeyBindings)
.Concat(InGameKeyBindings)
.Concat(ReplayKeyBindings)
.Concat(SongSelectKeyBindings)
.Concat(AudioControlKeyBindings)
// Overlay bindings may conflict with more local cases like the editor so they are checked last.

View File

@ -95,7 +95,7 @@ namespace osu.Game.Online.Chat
{
connector.ChannelJoined += ch => Schedule(() => joinChannel(ch));
connector.ChannelParted += ch => Schedule(() => LeaveChannel(getChannel(ch)));
connector.ChannelParted += ch => Schedule(() => leaveChannel(getChannel(ch), false));
connector.NewMessages += msgs => Schedule(() => addMessages(msgs));
@ -558,7 +558,9 @@ namespace osu.Game.Online.Chat
/// Leave the specified channel. Can be called from any thread.
/// </summary>
/// <param name="channel">The channel to leave.</param>
public void LeaveChannel(Channel channel) => Schedule(() =>
public void LeaveChannel(Channel channel) => Schedule(() => leaveChannel(channel, true));
private void leaveChannel(Channel channel, bool sendLeaveRequest)
{
if (channel == null) return;
@ -581,10 +583,11 @@ namespace osu.Game.Online.Chat
if (channel.Joined.Value)
{
api.Queue(new LeaveChannelRequest(channel));
if (sendLeaveRequest)
api.Queue(new LeaveChannelRequest(channel));
channel.Joined.Value = false;
}
});
}
/// <summary>
/// Opens the most recently closed channel that has not already been reopened,

View File

@ -61,7 +61,7 @@ namespace osu.Game.Online.Chat
beatmapInfo = game.BeatmapInfo;
break;
case UserActivity.Editing edit:
case UserActivity.EditingBeatmap edit:
verb = "editing";
beatmapInfo = edit.BeatmapInfo;
break;

View File

@ -60,6 +60,7 @@ using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Skinning;
using osu.Game.Updater;
using osu.Game.Users;
using osu.Game.Utils;
@ -501,6 +502,23 @@ namespace osu.Game
/// <param name="version">The build version of the update stream</param>
public void ShowChangelogBuild(string updateStream, string version) => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowBuild(updateStream, version));
/// <summary>
/// Present a skin select immediately.
/// </summary>
/// <param name="skin">The skin to select.</param>
public void PresentSkin(SkinInfo skin)
{
var databasedSkin = SkinManager.Query(s => s.ID == skin.ID);
if (databasedSkin == null)
{
Logger.Log("The requested skin could not be loaded.", LoggingTarget.Information);
return;
}
SkinManager.CurrentSkinInfo.Value = databasedSkin;
}
/// <summary>
/// Present a beatmap at song select immediately.
/// The user should have already requested this interactively.
@ -777,6 +795,7 @@ namespace osu.Game
// todo: all archive managers should be able to be looped here.
SkinManager.PostNotification = n => Notifications.Post(n);
SkinManager.PresentImport = items => PresentSkin(items.First().Value);
BeatmapManager.PostNotification = n => Notifications.Post(n);
BeatmapManager.PresentImport = items => PresentBeatmap(items.First().Value);

View File

@ -57,6 +57,7 @@ namespace osu.Game.Overlays.Dialog
private Sample confirmSample;
private double lastTickPlaybackTime;
private AudioFilter lowPassFilter = null!;
private bool mouseDown;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
@ -73,6 +74,12 @@ namespace osu.Game.Overlays.Dialog
Progress.BindValueChanged(progressChanged);
}
protected override void AbortConfirm()
{
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
base.AbortConfirm();
}
protected override void Confirm()
{
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
@ -83,6 +90,7 @@ namespace osu.Game.Overlays.Dialog
protected override bool OnMouseDown(MouseDownEvent e)
{
BeginConfirm();
mouseDown = true;
return true;
}
@ -90,11 +98,28 @@ namespace osu.Game.Overlays.Dialog
{
if (!e.HasAnyButtonPressed)
{
lowPassFilter.CutoffTo(AudioFilter.MAX_LOWPASS_CUTOFF);
AbortConfirm();
mouseDown = false;
}
}
protected override bool OnHover(HoverEvent e)
{
if (mouseDown)
BeginConfirm();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
if (!mouseDown) return;
AbortConfirm();
}
private void progressChanged(ValueChangedEvent<double> progress)
{
if (progress.NewValue < progress.OldValue) return;

View File

@ -148,9 +148,9 @@ namespace osu.Game.Overlays.SkinEditor
component.Origin = Anchor.Centre;
}
protected override void Update()
protected override void UpdateAfterChildren()
{
base.Update();
base.UpdateAfterChildren();
if (component.DrawSize != Vector2.Zero)
{

View File

@ -439,19 +439,20 @@ namespace osu.Game.Rulesets.Objects.Legacy
private List<HitSampleInfo> convertSoundType(LegacyHitSoundType type, SampleBankInfo bankInfo)
{
// Todo: This should return the normal SampleInfos if the specified sample file isn't found, but that's a pretty edge-case scenario
if (!string.IsNullOrEmpty(bankInfo.Filename))
{
return new List<HitSampleInfo> { new FileHitSampleInfo(bankInfo.Filename, bankInfo.Volume) };
}
var soundTypes = new List<HitSampleInfo>();
var soundTypes = new List<HitSampleInfo>
if (string.IsNullOrEmpty(bankInfo.Filename))
{
new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.BankForNormal, bankInfo.Volume, bankInfo.CustomSampleBank,
// if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample.
// None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds
type != LegacyHitSoundType.None && !type.HasFlagFast(LegacyHitSoundType.Normal))
};
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.BankForNormal, bankInfo.Volume, bankInfo.CustomSampleBank,
// if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample.
// None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds
type != LegacyHitSoundType.None && !type.HasFlagFast(LegacyHitSoundType.Normal)));
}
else
{
// Todo: This should set the normal SampleInfo if the specified sample file isn't found, but that's a pretty edge-case scenario
soundTypes.Add(new FileHitSampleInfo(bankInfo.Filename, bankInfo.Volume));
}
if (type.HasFlagFast(LegacyHitSoundType.Finish))
soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_FINISH, bankInfo.BankForAdditions, bankInfo.Volume, bankInfo.CustomSampleBank));

View File

@ -7,21 +7,28 @@ using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Extensions;
using osu.Game.Localisation;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays;
using osu.Game.Scoring;
using osu.Framework.Localisation;
using osu.Game.Localisation;
namespace osu.Game.Rulesets.Scoring
{
public partial class ScoreProcessor : JudgementProcessor
{
private const double accuracy_cutoff_x = 1;
private const double accuracy_cutoff_s = 0.95;
private const double accuracy_cutoff_a = 0.9;
private const double accuracy_cutoff_b = 0.8;
private const double accuracy_cutoff_c = 0.7;
private const double accuracy_cutoff_d = 0;
private const double max_score = 1000000;
/// <summary>
@ -160,7 +167,7 @@ namespace osu.Game.Rulesets.Scoring
Combo.ValueChanged += combo => HighestCombo.Value = Math.Max(HighestCombo.Value, combo.NewValue);
Accuracy.ValueChanged += accuracy =>
{
Rank.Value = rankFrom(accuracy.NewValue);
Rank.Value = RankFromAccuracy(accuracy.NewValue);
foreach (var mod in Mods.Value.OfType<IApplicableToScoreProcessor>())
Rank.Value = mod.AdjustRank(Rank.Value, accuracy.NewValue);
};
@ -369,22 +376,6 @@ namespace osu.Game.Rulesets.Scoring
}
}
private ScoreRank rankFrom(double acc)
{
if (acc == 1)
return ScoreRank.X;
if (acc >= 0.95)
return ScoreRank.S;
if (acc >= 0.9)
return ScoreRank.A;
if (acc >= 0.8)
return ScoreRank.B;
if (acc >= 0.7)
return ScoreRank.C;
return ScoreRank.D;
}
/// <summary>
/// Resets this ScoreProcessor to a default state.
/// </summary>
@ -583,6 +574,62 @@ namespace osu.Game.Rulesets.Scoring
hitEvents.Clear();
}
#region Static helper methods
/// <summary>
/// Given an accuracy (0..1), return the correct <see cref="ScoreRank"/>.
/// </summary>
public static ScoreRank RankFromAccuracy(double accuracy)
{
if (accuracy == accuracy_cutoff_x)
return ScoreRank.X;
if (accuracy >= accuracy_cutoff_s)
return ScoreRank.S;
if (accuracy >= accuracy_cutoff_a)
return ScoreRank.A;
if (accuracy >= accuracy_cutoff_b)
return ScoreRank.B;
if (accuracy >= accuracy_cutoff_c)
return ScoreRank.C;
return ScoreRank.D;
}
/// <summary>
/// Given a <see cref="ScoreRank"/>, return the cutoff accuracy (0..1).
/// Accuracy must be greater than or equal to the cutoff to qualify for the provided rank.
/// </summary>
public static double AccuracyCutoffFromRank(ScoreRank rank)
{
switch (rank)
{
case ScoreRank.X:
case ScoreRank.XH:
return accuracy_cutoff_x;
case ScoreRank.S:
case ScoreRank.SH:
return accuracy_cutoff_s;
case ScoreRank.A:
return accuracy_cutoff_a;
case ScoreRank.B:
return accuracy_cutoff_b;
case ScoreRank.C:
return accuracy_cutoff_c;
case ScoreRank.D:
return accuracy_cutoff_d;
default:
throw new ArgumentOutOfRangeException(nameof(rank), rank, null);
}
}
#endregion
/// <summary>
/// Stores the required scoring data that fulfils the minimum requirements for a <see cref="ScoreProcessor"/> to calculate score.
/// </summary>

View File

@ -157,7 +157,16 @@ namespace osu.Game.Screens.Edit
private bool isNewBeatmap;
protected override UserActivity InitialActivity => new UserActivity.Editing(Beatmap.Value.BeatmapInfo);
protected override UserActivity InitialActivity
{
get
{
if (Beatmap.Value.Metadata.Author.OnlineID == api.LocalUser.Value.OnlineID)
return new UserActivity.EditingBeatmap(Beatmap.Value.BeatmapInfo);
return new UserActivity.ModdingBeatmap(Beatmap.Value.BeatmapInfo);
}
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
=> dependencies = new DependencyContainer(base.CreateChildDependencies(parent));

View File

@ -7,6 +7,7 @@ using osu.Framework.Screens;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Screens.Play;
using osu.Game.Users;
namespace osu.Game.Screens.Edit.GameplayTest
{
@ -15,6 +16,8 @@ namespace osu.Game.Screens.Edit.GameplayTest
private readonly Editor editor;
private readonly EditorState editorState;
protected override UserActivity InitialActivity => new UserActivity.TestingBeatmap(Beatmap.Value.BeatmapInfo, Ruleset.Value);
[Resolved]
private MusicController musicController { get; set; } = null!;

View File

@ -0,0 +1,209 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Screens.Edit.Timing
{
public partial class ControlPointList : CompositeDrawable
{
private OsuButton deleteButton = null!;
private ControlPointTable table = null!;
private OsuScrollContainer scroll = null!;
private RoundedButton addButton = null!;
private readonly IBindableList<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>();
[Resolved]
private EditorClock clock { get; set; } = null!;
[Resolved]
protected EditorBeatmap Beatmap { get; private set; } = null!;
[Resolved]
private Bindable<ControlPointGroup?> selectedGroup { get; set; } = null!;
[Resolved]
private IEditorChangeHandler? changeHandler { get; set; }
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colours)
{
RelativeSizeAxes = Axes.Both;
const float margins = 10;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colours.Background4,
RelativeSizeAxes = Axes.Both,
},
new Box
{
Colour = colours.Background3,
RelativeSizeAxes = Axes.Y,
Width = ControlPointTable.TIMING_COLUMN_WIDTH + margins,
},
scroll = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = table = new ControlPointTable(),
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Direction = FillDirection.Horizontal,
Margin = new MarginPadding(margins),
Spacing = new Vector2(5),
Children = new Drawable[]
{
deleteButton = new RoundedButton
{
Text = "-",
Size = new Vector2(30, 30),
Action = delete,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
},
addButton = new RoundedButton
{
Action = addNew,
Size = new Vector2(160, 30),
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
},
}
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
selectedGroup.BindValueChanged(selected =>
{
deleteButton.Enabled.Value = selected.NewValue != null;
addButton.Text = selected.NewValue != null
? "+ Clone to current time"
: "+ Add at current time";
}, true);
controlPointGroups.BindTo(Beatmap.ControlPointInfo.Groups);
controlPointGroups.BindCollectionChanged((_, _) =>
{
table.ControlGroups = controlPointGroups;
changeHandler?.SaveState();
}, true);
table.OnRowSelected += drawable => scroll.ScrollIntoView(drawable);
}
protected override bool OnClick(ClickEvent e)
{
selectedGroup.Value = null;
return true;
}
protected override void Update()
{
base.Update();
trackActivePoint();
addButton.Enabled.Value = clock.CurrentTimeAccurate != selectedGroup.Value?.Time;
}
private Type? trackedType;
/// <summary>
/// Given the user has selected a control point group, we want to track any group which is
/// active at the current point in time which matches the type the user has selected.
///
/// So if the user is currently looking at a timing point and seeks into the future, a
/// future timing point would be automatically selected if it is now the new "current" point.
/// </summary>
private void trackActivePoint()
{
// For simplicity only match on the first type of the active control point.
if (selectedGroup.Value == null)
trackedType = null;
else
{
// If the selected group only has one control point, update the tracking type.
if (selectedGroup.Value.ControlPoints.Count == 1)
trackedType = selectedGroup.Value?.ControlPoints.Single().GetType();
// If the selected group has more than one control point, choose the first as the tracking type
// if we don't already have a singular tracked type.
else if (trackedType == null)
trackedType = selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType();
}
if (trackedType != null)
{
// We don't have an efficient way of looking up groups currently, only individual point types.
// To improve the efficiency of this in the future, we should reconsider the overall structure of ControlPointInfo.
// Find the next group which has the same type as the selected one.
var found = Beatmap.ControlPointInfo.Groups
.Where(g => g.ControlPoints.Any(cp => cp.GetType() == trackedType))
.LastOrDefault(g => g.Time <= clock.CurrentTimeAccurate);
if (found != null)
selectedGroup.Value = found;
}
}
private void delete()
{
if (selectedGroup.Value == null)
return;
Beatmap.ControlPointInfo.RemoveGroup(selectedGroup.Value);
selectedGroup.Value = Beatmap.ControlPointInfo.Groups.FirstOrDefault(g => g.Time >= clock.CurrentTime);
}
private void addNew()
{
bool isFirstControlPoint = !Beatmap.ControlPointInfo.TimingPoints.Any();
var group = Beatmap.ControlPointInfo.GroupAt(clock.CurrentTime, true);
if (isFirstControlPoint)
group.Add(new TimingControlPoint());
else
{
// Try and create matching types from the currently selected control point.
var selected = selectedGroup.Value;
if (selected != null && !ReferenceEquals(selected, group))
{
foreach (var controlPoint in selected.ControlPoints)
{
group.Add(controlPoint.DeepClone());
}
}
}
selectedGroup.Value = group;
}
}
}

View File

@ -1,20 +1,11 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Screens.Edit.Timing
{
@ -23,6 +14,9 @@ namespace osu.Game.Screens.Edit.Timing
[Cached]
public readonly Bindable<ControlPointGroup> SelectedGroup = new Bindable<ControlPointGroup>();
[Resolved]
private EditorClock? editorClock { get; set; }
public TimingScreen()
: base(EditorScreenMode.Timing)
{
@ -46,192 +40,17 @@ namespace osu.Game.Screens.Edit.Timing
}
};
public partial class ControlPointList : CompositeDrawable
protected override void LoadComplete()
{
private OsuButton deleteButton = null!;
private ControlPointTable table = null!;
private OsuScrollContainer scroll = null!;
private RoundedButton addButton = null!;
base.LoadComplete();
private readonly IBindableList<ControlPointGroup> controlPointGroups = new BindableList<ControlPointGroup>();
[Resolved]
private EditorClock clock { get; set; } = null!;
[Resolved]
protected EditorBeatmap Beatmap { get; private set; } = null!;
[Resolved]
private Bindable<ControlPointGroup?> selectedGroup { get; set; } = null!;
[Resolved]
private IEditorChangeHandler? changeHandler { get; set; }
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colours)
if (editorClock != null)
{
RelativeSizeAxes = Axes.Both;
const float margins = 10;
InternalChildren = new Drawable[]
{
new Box
{
Colour = colours.Background4,
RelativeSizeAxes = Axes.Both,
},
new Box
{
Colour = colours.Background3,
RelativeSizeAxes = Axes.Y,
Width = ControlPointTable.TIMING_COLUMN_WIDTH + margins,
},
scroll = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = table = new ControlPointTable(),
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Direction = FillDirection.Horizontal,
Margin = new MarginPadding(margins),
Spacing = new Vector2(5),
Children = new Drawable[]
{
deleteButton = new RoundedButton
{
Text = "-",
Size = new Vector2(30, 30),
Action = delete,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
},
addButton = new RoundedButton
{
Action = addNew,
Size = new Vector2(160, 30),
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
},
}
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
selectedGroup.BindValueChanged(selected =>
{
deleteButton.Enabled.Value = selected.NewValue != null;
addButton.Text = selected.NewValue != null
? "+ Clone to current time"
: "+ Add at current time";
}, true);
controlPointGroups.BindTo(Beatmap.ControlPointInfo.Groups);
controlPointGroups.BindCollectionChanged((_, _) =>
{
table.ControlGroups = controlPointGroups;
changeHandler?.SaveState();
}, true);
table.OnRowSelected += drawable => scroll.ScrollIntoView(drawable);
}
protected override bool OnClick(ClickEvent e)
{
selectedGroup.Value = null;
return true;
}
protected override void Update()
{
base.Update();
trackActivePoint();
addButton.Enabled.Value = clock.CurrentTimeAccurate != selectedGroup.Value?.Time;
}
private Type? trackedType;
/// <summary>
/// Given the user has selected a control point group, we want to track any group which is
/// active at the current point in time which matches the type the user has selected.
///
/// So if the user is currently looking at a timing point and seeks into the future, a
/// future timing point would be automatically selected if it is now the new "current" point.
/// </summary>
private void trackActivePoint()
{
// For simplicity only match on the first type of the active control point.
if (selectedGroup.Value == null)
trackedType = null;
else
{
// If the selected group only has one control point, update the tracking type.
if (selectedGroup.Value.ControlPoints.Count == 1)
trackedType = selectedGroup.Value?.ControlPoints.Single().GetType();
// If the selected group has more than one control point, choose the first as the tracking type
// if we don't already have a singular tracked type.
else if (trackedType == null)
trackedType = selectedGroup.Value?.ControlPoints.FirstOrDefault()?.GetType();
}
if (trackedType != null)
{
// We don't have an efficient way of looking up groups currently, only individual point types.
// To improve the efficiency of this in the future, we should reconsider the overall structure of ControlPointInfo.
// Find the next group which has the same type as the selected one.
var found = Beatmap.ControlPointInfo.Groups
.Where(g => g.ControlPoints.Any(cp => cp.GetType() == trackedType))
.LastOrDefault(g => g.Time <= clock.CurrentTimeAccurate);
if (found != null)
selectedGroup.Value = found;
}
}
private void delete()
{
if (selectedGroup.Value == null)
return;
Beatmap.ControlPointInfo.RemoveGroup(selectedGroup.Value);
selectedGroup.Value = Beatmap.ControlPointInfo.Groups.FirstOrDefault(g => g.Time >= clock.CurrentTime);
}
private void addNew()
{
bool isFirstControlPoint = !Beatmap.ControlPointInfo.TimingPoints.Any();
var group = Beatmap.ControlPointInfo.GroupAt(clock.CurrentTime, true);
if (isFirstControlPoint)
group.Add(new TimingControlPoint());
else
{
// Try and create matching types from the currently selected control point.
var selected = selectedGroup.Value;
if (selected != null && !ReferenceEquals(selected, group))
{
foreach (var controlPoint in selected.ControlPoints)
{
group.Add(controlPoint.DeepClone());
}
}
}
selectedGroup.Value = group;
// When entering the timing screen, let's choose the closest valid timing point.
// This will emulate the osu-stable behaviour where a metronome and timing information
// are presented on entering the screen.
var nearestTimingPoint = EditorBeatmap.ControlPointInfo.TimingPointAt(editorClock.CurrentTime);
SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(nearestTimingPoint.Time);
}
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@ -22,29 +20,21 @@ namespace osu.Game.Screens.Play.Break
RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new Container
new Box
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
RelativeSizeAxes = Axes.X,
Height = height,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(Color4.Black, transparent_black),
}
Colour = ColourInfo.GradientVertical(Color4.Black, transparent_black),
},
new Container
new Box
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.X,
Height = height,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourInfo.GradientVertical(transparent_black, Color4.Black),
}
Colour = ColourInfo.GradientVertical(transparent_black, Color4.Black),
}
};
}

View File

@ -121,8 +121,8 @@ namespace osu.Game.Screens.Play.HUD
AutoSizeAxes = Axes.Both,
Child = new UprightAspectMaintainingContainer
{
Origin = Anchor.CentreRight,
Anchor = Anchor.CentreRight,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
AutoSizeAxes = Axes.Both,
Scaling = ScaleMode.Vertical,
ScalingFactor = 0.5f,

View File

@ -15,6 +15,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Ranking;
using osu.Game.Users;
namespace osu.Game.Screens.Play
{
@ -24,6 +25,8 @@ namespace osu.Game.Screens.Play
private readonly bool replayIsFailedScore;
protected override UserActivity InitialActivity => new UserActivity.WatchingReplay(Score.ScoreInfo);
// Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108)
protected override bool CheckModsAllowFailure()
{

View File

@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Game.Online.Spectator;
using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Screens.Play
{
@ -14,6 +15,8 @@ namespace osu.Game.Screens.Play
{
private readonly Score score;
protected override UserActivity InitialActivity => new UserActivity.SpectatingUser(Score.ScoreInfo);
public SoloSpectatorPlayer(Score score, PlayerConfiguration configuration = null)
: base(score, configuration)
{

View File

@ -17,6 +17,7 @@ using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Skinning;
using osuTK;
@ -28,6 +29,13 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
/// </summary>
public partial class AccuracyCircle : CompositeDrawable
{
private static readonly double accuracy_x = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.X);
private static readonly double accuracy_s = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.S);
private static readonly double accuracy_a = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.A);
private static readonly double accuracy_b = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.B);
private static readonly double accuracy_c = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.C);
private static readonly double accuracy_d = ScoreProcessor.AccuracyCutoffFromRank(ScoreRank.D);
/// <summary>
/// Duration for the transforms causing this component to appear.
/// </summary>
@ -73,6 +81,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
/// </summary>
private const double virtual_ss_percentage = 0.01;
/// <summary>
/// The width of a <see cref="RankNotch"/> in terms of accuracy.
/// </summary>
public const double NOTCH_WIDTH_PERCENTAGE = 1.0 / 360;
/// <summary>
/// The easing for the circle filling transforms.
/// </summary>
@ -145,49 +158,49 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.X),
InnerRadius = RANK_CIRCLE_RADIUS,
Current = { Value = 1 }
Current = { Value = accuracy_x }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.S),
InnerRadius = RANK_CIRCLE_RADIUS,
Current = { Value = 1 - virtual_ss_percentage }
Current = { Value = accuracy_x - virtual_ss_percentage }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.A),
InnerRadius = RANK_CIRCLE_RADIUS,
Current = { Value = 0.95f }
Current = { Value = accuracy_s }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.B),
InnerRadius = RANK_CIRCLE_RADIUS,
Current = { Value = 0.9f }
Current = { Value = accuracy_a }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.C),
InnerRadius = RANK_CIRCLE_RADIUS,
Current = { Value = 0.8f }
Current = { Value = accuracy_b }
},
new CircularProgress
{
RelativeSizeAxes = Axes.Both,
Colour = OsuColour.ForRank(ScoreRank.D),
InnerRadius = RANK_CIRCLE_RADIUS,
Current = { Value = 0.7f }
Current = { Value = accuracy_c }
},
new RankNotch(0),
new RankNotch((float)(1 - virtual_ss_percentage)),
new RankNotch(0.95f),
new RankNotch(0.9f),
new RankNotch(0.8f),
new RankNotch(0.7f),
new RankNotch((float)accuracy_x),
new RankNotch((float)(accuracy_x - virtual_ss_percentage)),
new RankNotch((float)accuracy_s),
new RankNotch((float)accuracy_a),
new RankNotch((float)accuracy_b),
new RankNotch((float)accuracy_c),
new BufferedContainer
{
Name = "Graded circle mask",
@ -215,12 +228,13 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
Padding = new MarginPadding { Vertical = -15, Horizontal = -20 },
Children = new[]
{
new RankBadge(1, getRank(ScoreRank.X)),
new RankBadge(0.95, getRank(ScoreRank.S)),
new RankBadge(0.9, getRank(ScoreRank.A)),
new RankBadge(0.8, getRank(ScoreRank.B)),
new RankBadge(0.7, getRank(ScoreRank.C)),
new RankBadge(0.35, getRank(ScoreRank.D)),
// The S and A badges are moved down slightly to prevent collision with the SS badge.
new RankBadge(accuracy_x, accuracy_x, getRank(ScoreRank.X)),
new RankBadge(accuracy_s, Interpolation.Lerp(accuracy_s, (accuracy_x - virtual_ss_percentage), 0.25), getRank(ScoreRank.S)),
new RankBadge(accuracy_a, Interpolation.Lerp(accuracy_a, accuracy_s, 0.25), getRank(ScoreRank.A)),
new RankBadge(accuracy_b, Interpolation.Lerp(accuracy_b, accuracy_a, 0.5), getRank(ScoreRank.B)),
new RankBadge(accuracy_c, Interpolation.Lerp(accuracy_c, accuracy_b, 0.5), getRank(ScoreRank.C)),
new RankBadge(accuracy_d, Interpolation.Lerp(accuracy_d, accuracy_c, 0.5), getRank(ScoreRank.D)),
}
},
rankText = new RankText(score.Rank)
@ -263,7 +277,39 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
using (BeginDelayedSequence(ACCURACY_TRANSFORM_DELAY))
{
double targetAccuracy = score.Rank == ScoreRank.X || score.Rank == ScoreRank.XH ? 1 : Math.Min(1 - virtual_ss_percentage, score.Accuracy);
double targetAccuracy = score.Accuracy;
double[] notchPercentages =
{
accuracy_s,
accuracy_a,
accuracy_b,
accuracy_c,
};
// Ensure the gauge overshoots or undershoots a bit so it doesn't land in the gaps of the inner graded circle (caused by `RankNotch`es),
// to prevent ambiguity on what grade it's pointing at.
foreach (double p in notchPercentages)
{
if (Precision.AlmostEquals(p, targetAccuracy, NOTCH_WIDTH_PERCENTAGE / 2))
{
int tippingDirection = targetAccuracy - p >= 0 ? 1 : -1; // We "round up" here to match rank criteria
targetAccuracy = p + tippingDirection * (NOTCH_WIDTH_PERCENTAGE / 2);
break;
}
}
// The final gap between 99.999...% (S) and 100% (SS) is exaggerated by `virtual_ss_percentage`. We don't want to land there either.
if (score.Rank == ScoreRank.X || score.Rank == ScoreRank.XH)
targetAccuracy = 1;
else
targetAccuracy = Math.Min(accuracy_x - virtual_ss_percentage - NOTCH_WIDTH_PERCENTAGE / 2, targetAccuracy);
// The accuracy circle gauge visually fills up a bit too much.
// This wouldn't normally matter but we want it to align properly with the inner graded circle in the above cases.
const double visual_alignment_offset = 0.001;
if (targetAccuracy < 1 && targetAccuracy >= visual_alignment_offset)
targetAccuracy -= visual_alignment_offset;
accuracyCircle.FillTo(targetAccuracy, ACCURACY_TRANSFORM_DURATION, ACCURACY_TRANSFORM_EASING);
@ -293,7 +339,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
if (badge.Accuracy > score.Accuracy)
continue;
using (BeginDelayedSequence(inverseEasing(ACCURACY_TRANSFORM_EASING, Math.Min(1 - virtual_ss_percentage, badge.Accuracy) / targetAccuracy) * ACCURACY_TRANSFORM_DURATION))
using (BeginDelayedSequence(inverseEasing(ACCURACY_TRANSFORM_EASING, Math.Min(accuracy_x - virtual_ss_percentage, badge.Accuracy) / targetAccuracy) * ACCURACY_TRANSFORM_DURATION))
{
badge.Appear();

View File

@ -27,6 +27,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
/// </summary>
public readonly double Accuracy;
/// <summary>
/// The position around the <see cref="AccuracyCircle"/> to display this badge.
/// </summary>
private readonly double displayPosition;
private readonly ScoreRank rank;
private Drawable rankContainer;
@ -36,10 +41,12 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
/// Creates a new <see cref="RankBadge"/>.
/// </summary>
/// <param name="accuracy">The accuracy value corresponding to <paramref name="rank"/>.</param>
/// <param name="position">The position around the <see cref="AccuracyCircle"/> to display this badge.</param>
/// <param name="rank">The <see cref="ScoreRank"/> to be displayed in this <see cref="RankBadge"/>.</param>
public RankBadge(double accuracy, ScoreRank rank)
public RankBadge(double accuracy, double position, ScoreRank rank)
{
Accuracy = accuracy;
displayPosition = position;
this.rank = rank;
RelativeSizeAxes = Axes.Both;
@ -92,7 +99,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
base.Update();
// Starts at -90deg (top) and moves counter-clockwise by the accuracy
rankContainer.Position = circlePosition(-MathF.PI / 2 - (1 - (float)Accuracy) * MathF.PI * 2);
rankContainer.Position = circlePosition(-MathF.PI / 2 - (1 - (float)displayPosition) * MathF.PI * 2);
}
private Vector2 circlePosition(float t)

View File

@ -41,7 +41,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y,
Height = AccuracyCircle.RANK_CIRCLE_RADIUS,
Width = 1f,
Width = (float)AccuracyCircle.NOTCH_WIDTH_PERCENTAGE * 360f,
Colour = OsuColour.Gray(0.3f),
EdgeSmoothness = new Vector2(1f)
}

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
namespace osu.Game.Screens.Select.FooterV2
{
public partial class FooterButtonModsV2 : FooterButtonV2
{
[BackgroundDependencyLoader]
private void load(OsuColour colour)
{
Text = "Mods";
Icon = FontAwesome.Solid.ExchangeAlt;
AccentColour = colour.Lime1;
}
}
}

View File

@ -0,0 +1,22 @@
// 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.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Input.Bindings;
namespace osu.Game.Screens.Select.FooterV2
{
public partial class FooterButtonOptionsV2 : FooterButtonV2
{
[BackgroundDependencyLoader]
private void load(OsuColour colour)
{
Text = "Options";
Icon = FontAwesome.Solid.Cog;
AccentColour = colour.Purple1;
Hotkey = GlobalAction.ToggleBeatmapOptions;
}
}
}

View File

@ -0,0 +1,161 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Input.Bindings;
using osuTK;
using osuTK.Input;
namespace osu.Game.Screens.Select.FooterV2
{
public partial class FooterButtonRandomV2 : FooterButtonV2
{
public Action? NextRandom { get; set; }
public Action? PreviousRandom { get; set; }
private Container persistentText = null!;
private OsuSpriteText randomSpriteText = null!;
private OsuSpriteText rewindSpriteText = null!;
private bool rewindSearch;
[BackgroundDependencyLoader]
private void load(OsuColour colour)
{
//TODO: use https://fontawesome.com/icons/shuffle?s=solid&f=classic when local Fontawesome is updated
Icon = FontAwesome.Solid.Random;
AccentColour = colour.Blue1;
TextContainer.Add(persistentText = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AlwaysPresent = true,
AutoSizeAxes = Axes.Both,
Children = new[]
{
randomSpriteText = new OsuSpriteText
{
Font = OsuFont.TorusAlternate.With(size: 19),
AlwaysPresent = true,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = "Random",
},
rewindSpriteText = new OsuSpriteText
{
Font = OsuFont.TorusAlternate.With(size: 19),
AlwaysPresent = true,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = "Rewind",
Alpha = 0f,
}
}
});
Action = () =>
{
if (rewindSearch)
{
const double fade_time = 500;
OsuSpriteText fallingRewind;
TextContainer.Add(fallingRewind = new OsuSpriteText
{
Alpha = 0,
Text = rewindSpriteText.Text,
AlwaysPresent = true, // make sure the button is sized large enough to always show this
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Font = OsuFont.TorusAlternate.With(size: 19),
});
fallingRewind.FadeOutFromOne(fade_time, Easing.In);
fallingRewind.MoveTo(Vector2.Zero).MoveTo(new Vector2(0, 10), fade_time, Easing.In);
fallingRewind.Expire();
persistentText.FadeInFromZero(fade_time, Easing.In);
PreviousRandom?.Invoke();
}
else
{
NextRandom?.Invoke();
}
};
}
protected override bool OnKeyDown(KeyDownEvent e)
{
updateText(e.ShiftPressed);
return base.OnKeyDown(e);
}
protected override void OnKeyUp(KeyUpEvent e)
{
updateText(e.ShiftPressed);
base.OnKeyUp(e);
}
protected override bool OnClick(ClickEvent e)
{
try
{
// this uses OR to handle rewinding when clicks are triggered by other sources (i.e. right button in OnMouseUp).
rewindSearch |= e.ShiftPressed;
return base.OnClick(e);
}
finally
{
rewindSearch = false;
}
}
protected override void OnMouseUp(MouseUpEvent e)
{
if (e.Button == MouseButton.Right)
{
rewindSearch = true;
TriggerClick();
return;
}
base.OnMouseUp(e);
}
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
rewindSearch = e.Action == GlobalAction.SelectPreviousRandom;
if (e.Action != GlobalAction.SelectNextRandom && e.Action != GlobalAction.SelectPreviousRandom)
{
return false;
}
if (!e.Repeat)
TriggerClick();
return true;
}
public override void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
if (e.Action == GlobalAction.SelectPreviousRandom)
{
rewindSearch = false;
}
}
private void updateText(bool rewind = false)
{
randomSpriteText.Alpha = rewind ? 0 : 1;
rewindSpriteText.Alpha = rewind ? 1 : 0;
}
}
}

View File

@ -0,0 +1,211 @@
// 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.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.Select.FooterV2
{
public partial class FooterButtonV2 : OsuClickableContainer, IKeyBindingHandler<GlobalAction>
{
private const int button_height = 90;
private const int button_width = 140;
private const int corner_radius = 10;
private const int transition_length = 500;
// This should be 12 by design, but an extra allowance is added due to the corner radius specification.
public const float SHEAR_WIDTH = 13.5f;
public Bindable<Visibility> OverlayState = new Bindable<Visibility>();
protected static readonly Vector2 SHEAR = new Vector2(SHEAR_WIDTH / button_height, 0);
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
private Colour4 buttonAccentColour;
protected Colour4 AccentColour
{
set
{
buttonAccentColour = value;
bar.Colour = buttonAccentColour;
icon.Colour = buttonAccentColour;
}
}
protected IconUsage Icon
{
set => icon.Icon = value;
}
protected LocalisableString Text
{
set => text.Text = value;
}
private readonly SpriteText text;
private readonly SpriteIcon icon;
protected Container TextContainer;
private readonly Box bar;
private readonly Box backgroundBox;
public FooterButtonV2()
{
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Radius = 4,
// Figma says 50% opacity, but it does not match up visually if taken at face value, and looks bad.
Colour = Colour4.Black.Opacity(0.25f),
Offset = new Vector2(0, 2),
};
Shear = SHEAR;
Size = new Vector2(button_width, button_height);
Masking = true;
CornerRadius = corner_radius;
Children = new Drawable[]
{
backgroundBox = new Box
{
RelativeSizeAxes = Axes.Both
},
// For elements that should not be sheared.
new Container
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Shear = -SHEAR,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
TextContainer = new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Y = 42,
AutoSizeAxes = Axes.Both,
Child = text = new OsuSpriteText
{
// figma design says the size is 16, but due to the issues with font sizes 19 matches better
Font = OsuFont.TorusAlternate.With(size: 19),
AlwaysPresent = true
}
},
icon = new SpriteIcon
{
Y = 12,
Size = new Vector2(20),
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
},
}
},
new Container
{
Shear = -SHEAR,
Anchor = Anchor.BottomCentre,
Origin = Anchor.Centre,
Y = -corner_radius,
Size = new Vector2(120, 6),
Masking = true,
CornerRadius = 3,
Child = bar = new Box
{
RelativeSizeAxes = Axes.Both,
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
OverlayState.BindValueChanged(_ => updateDisplay());
Enabled.BindValueChanged(_ => updateDisplay(), true);
FinishTransforms(true);
}
public GlobalAction? Hotkey;
private bool handlingMouse;
protected override bool OnHover(HoverEvent e)
{
updateDisplay();
return true;
}
protected override bool OnMouseDown(MouseDownEvent e)
{
handlingMouse = true;
updateDisplay();
return base.OnMouseDown(e);
}
protected override void OnMouseUp(MouseUpEvent e)
{
handlingMouse = false;
updateDisplay();
base.OnMouseUp(e);
}
protected override void OnHoverLost(HoverLostEvent e) => updateDisplay();
public virtual bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Action != Hotkey || e.Repeat) return false;
TriggerClick();
return true;
}
public virtual void OnReleased(KeyBindingReleaseEvent<GlobalAction> e) { }
private void updateDisplay()
{
Color4 backgroundColour = colourProvider.Background3;
if (!Enabled.Value)
{
backgroundColour = colourProvider.Background3.Darken(0.4f);
}
else
{
if (OverlayState.Value == Visibility.Visible)
backgroundColour = buttonAccentColour.Darken(0.5f);
if (IsHovered)
{
backgroundColour = backgroundColour.Lighten(0.3f);
if (handlingMouse)
backgroundColour = backgroundColour.Lighten(0.3f);
}
}
backgroundBox.FadeColour(backgroundColour, transition_length, Easing.OutQuint);
}
}
}

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 System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Screens.Select.FooterV2
{
public partial class FooterV2 : InputBlockingContainer
{
//Should be 60, setting to 50 for now for the sake of matching the current BackButton height.
private const int height = 50;
private const int padding = 80;
private readonly List<OverlayContainer> overlays = new List<OverlayContainer>();
/// <param name="button">The button to be added.</param>
/// <param name="overlay">The <see cref="OverlayContainer"/> to be toggled by this button.</param>
public void AddButton(FooterButtonV2 button, OverlayContainer? overlay = null)
{
if (overlay != null)
{
overlays.Add(overlay);
button.Action = () => showOverlay(overlay);
button.OverlayState.BindTo(overlay.State);
}
buttons.Add(button);
}
private void showOverlay(OverlayContainer overlay)
{
foreach (var o in overlays)
{
if (o == overlay)
o.ToggleVisibility();
else
o.Hide();
}
}
private FillFlowContainer<FooterButtonV2> buttons = null!;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.X;
Height = height;
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5
},
buttons = new FillFlowContainer<FooterButtonV2>
{
Position = new Vector2(TwoLayerButton.SIZE_EXTENDED.X + padding, 10),
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(-FooterButtonV2.SHEAR_WIDTH + 7, 0),
AutoSizeAxes = Axes.Both
}
};
}
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
namespace osu.Game.Tests.Visual
{
/// <summary>

View File

@ -1,11 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.Containers;
@ -13,56 +10,49 @@ using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Users.Drawables
{
public partial class ClickableAvatar : Container
public partial class ClickableAvatar : OsuClickableContainer
{
private const string default_tooltip_text = "view profile";
/// <summary>
/// Whether to open the user's profile when clicked.
/// </summary>
public bool OpenOnClick
public override LocalisableString TooltipText
{
set => clickableArea.Enabled.Value = clickableArea.Action != null && value;
get
{
if (!Enabled.Value)
return string.Empty;
return ShowUsernameTooltip ? (user?.Username ?? string.Empty) : default_tooltip_text;
}
set => throw new NotSupportedException();
}
/// <summary>
/// By default, the tooltip will show "view profile" as avatars are usually displayed next to a username.
/// Setting this to <c>true</c> exposes the username via tooltip for special cases where this is not true.
/// </summary>
public bool ShowUsernameTooltip
{
set => clickableArea.TooltipText = value ? (user?.Username ?? string.Empty) : default_tooltip_text;
}
public bool ShowUsernameTooltip { get; set; }
private readonly APIUser user;
private readonly APIUser? user;
[Resolved(CanBeNull = true)]
private OsuGame game { get; set; }
private readonly ClickableArea clickableArea;
[Resolved]
private OsuGame? game { get; set; }
/// <summary>
/// A clickable avatar for the specified user, with UI sounds included.
/// If <see cref="OpenOnClick"/> is <c>true</c>, clicking will open the user's profile.
/// </summary>
/// <param name="user">The user. A null value will get a placeholder avatar.</param>
public ClickableAvatar(APIUser user = null)
public ClickableAvatar(APIUser? user = null)
{
this.user = user;
Add(clickableArea = new ClickableArea
{
RelativeSizeAxes = Axes.Both,
});
if (user?.Id != APIUser.SYSTEM_USER_ID)
clickableArea.Action = openProfile;
Action = openProfile;
}
[BackgroundDependencyLoader]
private void load()
{
LoadComponentAsync(new DrawableAvatar(user), clickableArea.Add);
LoadComponentAsync(new DrawableAvatar(user), Add);
}
private void openProfile()
@ -71,23 +61,12 @@ namespace osu.Game.Users.Drawables
game?.ShowUser(user);
}
private partial class ClickableArea : OsuClickableContainer
protected override bool OnClick(ClickEvent e)
{
private LocalisableString tooltip = default_tooltip_text;
if (!Enabled.Value)
return false;
public override LocalisableString TooltipText
{
get => Enabled.Value ? tooltip : default;
set => tooltip = value;
}
protected override bool OnClick(ClickEvent e)
{
if (!Enabled.Value)
return false;
return base.OnClick(e);
}
return base.OnClick(e);
}
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
@ -13,9 +11,9 @@ namespace osu.Game.Users.Drawables
/// <summary>
/// An avatar which can update to a new user when needed.
/// </summary>
public partial class UpdateableAvatar : ModelBackedDrawable<APIUser>
public partial class UpdateableAvatar : ModelBackedDrawable<APIUser?>
{
public APIUser User
public APIUser? User
{
get => Model;
set => Model = value;
@ -58,7 +56,7 @@ namespace osu.Game.Users.Drawables
/// <param name="isInteractive">If set to true, hover/click sounds will play and clicking the avatar will open the user's profile.</param>
/// <param name="showUsernameTooltip">Whether to show the username rather than "view profile" on the tooltip. (note: this only applies if <paramref name="isInteractive"/> is also true)</param>
/// <param name="showGuestOnNull">Whether to show a default guest representation on null user (as opposed to nothing).</param>
public UpdateableAvatar(APIUser user = null, bool isInteractive = true, bool showUsernameTooltip = false, bool showGuestOnNull = true)
public UpdateableAvatar(APIUser? user = null, bool isInteractive = true, bool showUsernameTooltip = false, bool showGuestOnNull = true)
{
this.isInteractive = isInteractive;
this.showUsernameTooltip = showUsernameTooltip;
@ -67,7 +65,7 @@ namespace osu.Game.Users.Drawables
User = user;
}
protected override Drawable CreateDrawable(APIUser user)
protected override Drawable? CreateDrawable(APIUser? user)
{
if (user == null && !showGuestOnNull)
return null;
@ -76,7 +74,6 @@ namespace osu.Game.Users.Drawables
{
return new ClickableAvatar(user)
{
OpenOnClick = true,
ShowUsernameTooltip = showUsernameTooltip,
RelativeSizeAxes = Axes.Both,
};

View File

@ -106,7 +106,7 @@ namespace osu.Game.Users
// Set status message based on activity (if we have one) and status is not offline
if (activity != null && !(status is UserStatusOffline))
{
statusMessage.Text = activity.Status;
statusMessage.Text = activity.GetStatus();
statusIcon.FadeColour(activity.GetAppropriateColour(Colours), 500, Easing.OutQuint);
return;
}

View File

@ -7,24 +7,31 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Scoring;
using osuTK.Graphics;
namespace osu.Game.Users
{
public abstract class UserActivity
{
public abstract string Status { get; }
public abstract string GetStatus(bool hideIdentifiableInformation = false);
public virtual Color4 GetAppropriateColour(OsuColour colours) => colours.GreenDarker;
public class Modding : UserActivity
public class ModdingBeatmap : EditingBeatmap
{
public override string Status => "Modding a map";
public override string GetStatus(bool hideIdentifiableInformation = false) => "Modding a beatmap";
public override Color4 GetAppropriateColour(OsuColour colours) => colours.PurpleDark;
public ModdingBeatmap(IBeatmapInfo info)
: base(info)
{
}
}
public class ChoosingBeatmap : UserActivity
{
public override string Status => "Choosing a beatmap";
public override string GetStatus(bool hideIdentifiableInformation = false) => "Choosing a beatmap";
}
public abstract class InGame : UserActivity
@ -39,7 +46,7 @@ namespace osu.Game.Users
Ruleset = ruleset;
}
public override string Status => Ruleset.CreateInstance().PlayingVerb;
public override string GetStatus(bool hideIdentifiableInformation = false) => Ruleset.CreateInstance().PlayingVerb;
}
public class InMultiplayerGame : InGame
@ -49,7 +56,7 @@ namespace osu.Game.Users
{
}
public override string Status => $@"{base.Status} with others";
public override string GetStatus(bool hideIdentifiableInformation = false) => $@"{base.GetStatus(hideIdentifiableInformation)} with others";
}
public class SpectatingMultiplayerGame : InGame
@ -59,7 +66,7 @@ namespace osu.Game.Users
{
}
public override string Status => $"Watching others {base.Status.ToLowerInvariant()}";
public override string GetStatus(bool hideIdentifiableInformation = false) => $"Watching others {base.GetStatus(hideIdentifiableInformation).ToLowerInvariant()}";
}
public class InPlaylistGame : InGame
@ -78,31 +85,62 @@ namespace osu.Game.Users
}
}
public class Editing : UserActivity
public class TestingBeatmap : InGame
{
public override string GetStatus(bool hideIdentifiableInformation = false) => "Testing a beatmap";
public TestingBeatmap(IBeatmapInfo beatmapInfo, IRulesetInfo ruleset)
: base(beatmapInfo, ruleset)
{
}
}
public class EditingBeatmap : UserActivity
{
public IBeatmapInfo BeatmapInfo { get; }
public Editing(IBeatmapInfo info)
public EditingBeatmap(IBeatmapInfo info)
{
BeatmapInfo = info;
}
public override string Status => @"Editing a beatmap";
public override string GetStatus(bool hideIdentifiableInformation = false) => @"Editing a beatmap";
}
public class Spectating : UserActivity
public class WatchingReplay : UserActivity
{
public override string Status => @"Spectating a game";
private readonly ScoreInfo score;
protected string Username => score.User.Username;
public BeatmapInfo BeatmapInfo => score.BeatmapInfo;
public WatchingReplay(ScoreInfo score)
{
this.score = score;
}
public override string GetStatus(bool hideIdentifiableInformation = false) => hideIdentifiableInformation ? @"Watching a replay" : $@"Watching {Username}'s replay";
}
public class SpectatingUser : WatchingReplay
{
public override string GetStatus(bool hideIdentifiableInformation = false) => hideIdentifiableInformation ? @"Spectating a user" : $@"Spectating {Username}";
public SpectatingUser(ScoreInfo score)
: base(score)
{
}
}
public class SearchingForLobby : UserActivity
{
public override string Status => @"Looking for a lobby";
public override string GetStatus(bool hideIdentifiableInformation = false) => @"Looking for a lobby";
}
public class InLobby : UserActivity
{
public override string Status => @"In a lobby";
public override string GetStatus(bool hideIdentifiableInformation = false) => @"In a lobby";
public readonly Room Room;

View File

@ -18,29 +18,29 @@
</None>
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="AutoMapper" Version="11.0.1" />
<PackageReference Include="AutoMapper" Version="12.0.1" />
<PackageReference Include="DiffPlex" Version="1.7.1" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.46" />
<PackageReference Include="Humanizer" Version="2.14.1" />
<PackageReference Include="MessagePack" Version="2.4.35" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="6.0.10" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="6.0.10" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="6.0.10" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="6.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
<PackageReference Include="MessagePack" Version="2.4.59" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="7.0.2" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="7.0.2" />
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson" Version="7.0.2" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="7.0.2" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Toolkit.HighPerformance" Version="7.1.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="ppy.LocalisationAnalyser" Version="2022.809.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="10.18.0" />
<PackageReference Include="Realm" Version="10.20.0" />
<PackageReference Include="ppy.osu.Framework" Version="2023.131.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.202.0" />
<PackageReference Include="Sentry" Version="3.23.1" />
<PackageReference Include="Sentry" Version="3.28.1" />
<PackageReference Include="SharpCompress" Version="0.32.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.2" />
<PackageReference Include="SQLitePCLRaw.bundle_e_sqlite3" Version="2.1.4" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="TagLibSharp" Version="2.3.0" />