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

Merge branch 'master' into footer_V2_implementation

This commit is contained in:
Dean Herbert 2023-02-14 14:00:53 +09:00 committed by GitHub
commit 51d4ae5241
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
153 changed files with 2898 additions and 940 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

@ -29,6 +29,7 @@ namespace osu.Desktop
internal partial class OsuGameDesktop : OsuGame
{
private OsuSchemeLinkIPCChannel? osuSchemeLinkIPCChannel;
private ArchiveImportIPCChannel? archiveImportIPCChannel;
public OsuGameDesktop(string[]? args = null)
: base(args)
@ -123,6 +124,7 @@ namespace osu.Desktop
LoadComponentAsync(new ElevatedPrivilegesChecker(), Add);
osuSchemeLinkIPCChannel = new OsuSchemeLinkIPCChannel(Host, this);
archiveImportIPCChannel = new ArchiveImportIPCChannel(Host, this);
}
public override void SetHost(GameHost host)
@ -181,6 +183,7 @@ namespace osu.Desktop
{
base.Dispose(isDisposing);
osuSchemeLinkIPCChannel?.Dispose();
archiveImportIPCChannel?.Dispose();
}
private class SDL2BatteryInfo : BatteryInfo

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

@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania
};
}
private partial class ManiaScrollSlider : OsuSliderBar<double>
private partial class ManiaScrollSlider : RoundedSliderBar<double>
{
public override LocalisableString TooltipText => RulesetSettingsStrings.ScrollSpeedTooltip(Current.Value, (int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / Current.Value));
}

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

@ -187,28 +187,19 @@ namespace osu.Game.Rulesets.Osu.Edit
if (b.IsSelected)
continue;
var hitObject = (OsuHitObject)b.Item;
var snapPositions = b.ScreenSpaceSnapPoints;
Vector2? snap = checkSnap(hitObject.Position);
if (snap == null && hitObject.Position != hitObject.EndPosition)
snap = checkSnap(hitObject.EndPosition);
if (!snapPositions.Any())
continue;
if (snap != null)
var closestSnapPosition = snapPositions.MinBy(p => Vector2.Distance(p, screenSpacePosition));
if (Vector2.Distance(closestSnapPosition, screenSpacePosition) < snapRadius)
{
// only return distance portion, since time is not really valid
snapResult = new SnapResult(snap.Value, null, playfield);
snapResult = new SnapResult(closestSnapPosition, null, playfield);
return true;
}
Vector2? checkSnap(Vector2 checkPos)
{
Vector2 checkScreenPos = playfield.GamefieldToScreenSpace(checkPos);
if (Vector2.Distance(checkScreenPos, screenSpacePosition) < snapRadius)
return checkScreenPos;
return null;
}
}
snapResult = null;

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

@ -2,8 +2,11 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets.Taiko.Configuration;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Tests.Visual;
@ -14,36 +17,48 @@ namespace osu.Game.Rulesets.Taiko.Tests
{
private DrumTouchInputArea drumTouchInputArea = null!;
[SetUpSteps]
public void SetUpSteps()
private readonly Bindable<TaikoTouchControlScheme> controlScheme = new Bindable<TaikoTouchControlScheme>();
[BackgroundDependencyLoader]
private void load()
{
AddStep("create drum", () =>
var config = (TaikoRulesetConfigManager)RulesetConfigs.GetConfigFor(Ruleset.Value.CreateInstance()).AsNonNull();
config.BindWith(TaikoRulesetSetting.TouchControlScheme, controlScheme);
}
private void createDrum()
{
Child = new TaikoInputManager(new TaikoRuleset().RulesetInfo)
{
Child = new TaikoInputManager(new TaikoRuleset().RulesetInfo)
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
new InputDrum
{
new InputDrum
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Height = 0.2f,
},
drumTouchInputArea = new DrumTouchInputArea
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
},
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Height = 0.2f,
},
};
});
drumTouchInputArea = new DrumTouchInputArea
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
}
}
};
}
[Test]
public void TestDrum()
{
AddStep("create drum", createDrum);
AddStep("show drum", () => drumTouchInputArea.Show());
AddStep("change scheme (kddk)", () => controlScheme.Value = TaikoTouchControlScheme.KDDK);
AddStep("change scheme (kkdd)", () => controlScheme.Value = TaikoTouchControlScheme.KKDD);
AddStep("change scheme (ddkk)", () => controlScheme.Value = TaikoTouchControlScheme.DDKK);
}
protected override Ruleset CreateRuleset() => new TaikoRuleset();
}
}

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

@ -0,0 +1,28 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Configuration;
using osu.Game.Rulesets.Configuration;
namespace osu.Game.Rulesets.Taiko.Configuration
{
public class TaikoRulesetConfigManager : RulesetConfigManager<TaikoRulesetSetting>
{
public TaikoRulesetConfigManager(SettingsStore? settings, RulesetInfo ruleset, int? variant = null)
: base(settings, ruleset, variant)
{
}
protected override void InitialiseDefaults()
{
base.InitialiseDefaults();
SetDefault(TaikoRulesetSetting.TouchControlScheme, TaikoTouchControlScheme.KDDK);
}
}
public enum TaikoRulesetSetting
{
TouchControlScheme
}
}

View File

@ -0,0 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Rulesets.Taiko.Configuration
{
public enum TaikoTouchControlScheme
{
KDDK,
DDKK,
KKDD
}
}

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

@ -28,9 +28,13 @@ using osu.Game.Rulesets.Taiko.Skinning.Argon;
using osu.Game.Rulesets.Taiko.Skinning.Legacy;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Overlays.Settings;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning;
using osu.Game.Rulesets.Configuration;
using osu.Game.Configuration;
using osu.Game.Rulesets.Taiko.Configuration;
namespace osu.Game.Rulesets.Taiko
{
@ -194,6 +198,10 @@ namespace osu.Game.Rulesets.Taiko
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
public override IRulesetConfigManager CreateConfig(SettingsStore? settings) => new TaikoRulesetConfigManager(settings, RulesetInfo);
public override RulesetSettingsSubsection CreateSettings() => new TaikoSettingsSubsection(this);
protected override IEnumerable<HitResult> GetValidHitResults()
{
return new[]
@ -201,9 +209,8 @@ namespace osu.Game.Rulesets.Taiko
HitResult.Great,
HitResult.Ok,
HitResult.SmallTickHit,
HitResult.SmallBonus,
HitResult.LargeBonus,
};
}
@ -212,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,36 @@
// 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;
using osu.Framework.Localisation;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Taiko.Configuration;
namespace osu.Game.Rulesets.Taiko
{
public partial class TaikoSettingsSubsection : RulesetSettingsSubsection
{
protected override LocalisableString Header => "osu!taiko";
public TaikoSettingsSubsection(TaikoRuleset ruleset)
: base(ruleset)
{
}
[BackgroundDependencyLoader]
private void load()
{
var config = (TaikoRulesetConfigManager)Config;
Children = new Drawable[]
{
new SettingsEnumDropdown<TaikoTouchControlScheme>
{
LabelText = "Touch control scheme",
Current = config.GetBindable<TaikoTouchControlScheme>(TaikoRulesetSetting.TouchControlScheme)
}
};
}
}
}

View File

@ -1,9 +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.Collections.Generic;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@ -11,6 +13,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Rulesets.Taiko.Configuration;
using osuTK;
using osuTK.Graphics;
@ -31,15 +34,18 @@ namespace osu.Game.Rulesets.Taiko.UI
private Container mainContent = null!;
private QuarterCircle leftCentre = null!;
private QuarterCircle rightCentre = null!;
private QuarterCircle leftRim = null!;
private QuarterCircle rightRim = null!;
private DrumSegment leftCentre = null!;
private DrumSegment rightCentre = null!;
private DrumSegment leftRim = null!;
private DrumSegment rightRim = null!;
private readonly Bindable<TaikoTouchControlScheme> configTouchControlScheme = new Bindable<TaikoTouchControlScheme>();
[BackgroundDependencyLoader]
private void load(TaikoInputManager taikoInputManager, OsuColour colours)
private void load(TaikoInputManager taikoInputManager, TaikoRulesetConfigManager config)
{
Debug.Assert(taikoInputManager.KeyBindingContainer != null);
keyBindingContainer = taikoInputManager.KeyBindingContainer;
// Container should handle input everywhere.
@ -65,27 +71,27 @@ namespace osu.Game.Rulesets.Taiko.UI
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
leftRim = new QuarterCircle(TaikoAction.LeftRim, colours.Blue)
leftRim = new DrumSegment
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomRight,
X = -2,
},
rightRim = new QuarterCircle(TaikoAction.RightRim, colours.Blue)
rightRim = new DrumSegment
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomRight,
X = 2,
Rotation = 90,
},
leftCentre = new QuarterCircle(TaikoAction.LeftCentre, colours.Pink)
leftCentre = new DrumSegment
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomRight,
X = -2,
Scale = new Vector2(centre_region),
},
rightCentre = new QuarterCircle(TaikoAction.RightCentre, colours.Pink)
rightCentre = new DrumSegment
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomRight,
@ -98,6 +104,17 @@ namespace osu.Game.Rulesets.Taiko.UI
}
},
};
config.BindWith(TaikoRulesetSetting.TouchControlScheme, configTouchControlScheme);
configTouchControlScheme.BindValueChanged(scheme =>
{
var actions = getOrderedActionsForScheme(scheme.NewValue);
leftRim.Action = actions[0];
leftCentre.Action = actions[1];
rightCentre.Action = actions[2];
rightRim.Action = actions[3];
}, true);
}
protected override bool OnKeyDown(KeyDownEvent e)
@ -119,11 +136,47 @@ namespace osu.Game.Rulesets.Taiko.UI
base.OnTouchUp(e);
}
private static TaikoAction[] getOrderedActionsForScheme(TaikoTouchControlScheme scheme)
{
switch (scheme)
{
case TaikoTouchControlScheme.KDDK:
return new[]
{
TaikoAction.LeftRim,
TaikoAction.LeftCentre,
TaikoAction.RightCentre,
TaikoAction.RightRim
};
case TaikoTouchControlScheme.DDKK:
return new[]
{
TaikoAction.LeftCentre,
TaikoAction.RightCentre,
TaikoAction.LeftRim,
TaikoAction.RightRim
};
case TaikoTouchControlScheme.KKDD:
return new[]
{
TaikoAction.LeftRim,
TaikoAction.RightRim,
TaikoAction.LeftCentre,
TaikoAction.RightCentre
};
default:
throw new ArgumentOutOfRangeException(nameof(scheme), scheme, null);
}
}
private void handleDown(object source, Vector2 position)
{
Show();
TaikoAction taikoAction = getTaikoActionFromInput(position);
TaikoAction taikoAction = getTaikoActionFromPosition(position);
// Not too sure how this can happen, but let's avoid throwing.
if (trackedActions.ContainsKey(source))
@ -139,18 +192,15 @@ namespace osu.Game.Rulesets.Taiko.UI
trackedActions.Remove(source);
}
private bool validMouse(MouseButtonEvent e) =>
leftRim.Contains(e.ScreenSpaceMouseDownPosition) || rightRim.Contains(e.ScreenSpaceMouseDownPosition);
private TaikoAction getTaikoActionFromInput(Vector2 inputPosition)
private TaikoAction getTaikoActionFromPosition(Vector2 inputPosition)
{
bool centreHit = leftCentre.Contains(inputPosition) || rightCentre.Contains(inputPosition);
bool leftSide = ToLocalSpace(inputPosition).X < DrawWidth / 2;
if (leftSide)
return centreHit ? TaikoAction.LeftCentre : TaikoAction.LeftRim;
return centreHit ? leftCentre.Action : leftRim.Action;
return centreHit ? TaikoAction.RightCentre : TaikoAction.RightRim;
return centreHit ? rightCentre.Action : rightRim.Action;
}
protected override void PopIn()
@ -163,23 +213,42 @@ namespace osu.Game.Rulesets.Taiko.UI
mainContent.FadeOut(300);
}
private partial class QuarterCircle : CompositeDrawable, IKeyBindingHandler<TaikoAction>
private partial class DrumSegment : CompositeDrawable, IKeyBindingHandler<TaikoAction>
{
private readonly Circle overlay;
private TaikoAction action;
private readonly TaikoAction handledAction;
public TaikoAction Action
{
get => action;
set
{
if (action == value)
return;
private readonly Circle circle;
action = value;
updateColoursFromAction();
}
}
private Circle overlay = null!;
private Circle circle = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
public override bool Contains(Vector2 screenSpacePos) => circle.Contains(screenSpacePos);
public QuarterCircle(TaikoAction handledAction, Color4 colour)
public DrumSegment()
{
this.handledAction = handledAction;
RelativeSizeAxes = Axes.Both;
FillMode = FillMode.Fit;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
new Container
@ -191,7 +260,6 @@ namespace osu.Game.Rulesets.Taiko.UI
circle = new Circle
{
RelativeSizeAxes = Axes.Both,
Colour = colour.Multiply(1.4f).Darken(2.8f),
Alpha = 0.8f,
Scale = new Vector2(2),
},
@ -200,7 +268,6 @@ namespace osu.Game.Rulesets.Taiko.UI
Alpha = 0,
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
Colour = colour,
Scale = new Vector2(2),
}
}
@ -208,18 +275,52 @@ namespace osu.Game.Rulesets.Taiko.UI
};
}
protected override void LoadComplete()
{
base.LoadComplete();
updateColoursFromAction();
}
public bool OnPressed(KeyBindingPressEvent<TaikoAction> e)
{
if (e.Action == handledAction)
if (e.Action == Action)
overlay.FadeTo(1f, 80, Easing.OutQuint);
return false;
}
public void OnReleased(KeyBindingReleaseEvent<TaikoAction> e)
{
if (e.Action == handledAction)
if (e.Action == Action)
overlay.FadeOut(1000, Easing.OutQuint);
}
private void updateColoursFromAction()
{
if (!IsLoaded)
return;
var colour = getColourFromTaikoAction(Action);
circle.Colour = colour.Multiply(1.4f).Darken(2.8f);
overlay.Colour = colour;
}
private Color4 getColourFromTaikoAction(TaikoAction handledAction)
{
switch (handledAction)
{
case TaikoAction.LeftRim:
case TaikoAction.RightRim:
return colours.Blue;
case TaikoAction.LeftCentre:
case TaikoAction.RightCentre:
return colours.Pink;
}
throw new ArgumentOutOfRangeException();
}
}
}
}

View File

@ -214,7 +214,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(oneTime.EndTime, Is.EqualTo(4000 + loop_duration));
StoryboardSprite manyTimes = background.Elements.OfType<StoryboardSprite>().Single(s => s.Path == "many-times.png");
Assert.That(manyTimes.EndTime, Is.EqualTo(9000 + 40 * loop_duration));
// It is intentional that we don't consider the loop count (40) as part of the end time calculation to match stable's handling.
// If we were to include the loop count, storyboards which loop for stupid long loop counts would continue playing the outro forever.
Assert.That(manyTimes.EndTime, Is.EqualTo(9000 + loop_duration));
}
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using NUnit.Framework;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Beatmaps;
@ -12,7 +10,7 @@ using osu.Game.Screens.Edit;
namespace osu.Game.Tests.Editing
{
[TestFixture]
public class EditorChangeHandlerTest
public class BeatmapEditorChangeHandlerTest
{
private int stateChangedFired;
@ -23,18 +21,23 @@ namespace osu.Game.Tests.Editing
}
[Test]
public void TestSaveRestoreState()
public void TestSaveRestoreStateUsingTransaction()
{
var (handler, beatmap) = createChangeHandler();
Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.False);
addArbitraryChange(beatmap);
handler.SaveState();
handler.BeginChange();
// Initial state will be saved on BeginChange
Assert.That(stateChangedFired, Is.EqualTo(1));
addArbitraryChange(beatmap);
handler.EndChange();
Assert.That(stateChangedFired, Is.EqualTo(2));
Assert.That(handler.CanUndo.Value, Is.True);
Assert.That(handler.CanRedo.Value, Is.False);
@ -43,7 +46,35 @@ namespace osu.Game.Tests.Editing
Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.True);
Assert.That(stateChangedFired, Is.EqualTo(3));
}
[Test]
public void TestSaveRestoreState()
{
var (handler, beatmap) = createChangeHandler();
Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.False);
// Save initial state
handler.SaveState();
Assert.That(stateChangedFired, Is.EqualTo(1));
addArbitraryChange(beatmap);
handler.SaveState();
Assert.That(stateChangedFired, Is.EqualTo(2));
Assert.That(handler.CanUndo.Value, Is.True);
Assert.That(handler.CanRedo.Value, Is.False);
handler.RestoreState(-1);
Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.True);
Assert.That(stateChangedFired, Is.EqualTo(3));
}
[Test]
@ -54,6 +85,10 @@ namespace osu.Game.Tests.Editing
Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.False);
// Save initial state
handler.SaveState();
Assert.That(stateChangedFired, Is.EqualTo(1));
string originalHash = handler.CurrentStateHash;
addArbitraryChange(beatmap);
@ -61,7 +96,7 @@ namespace osu.Game.Tests.Editing
Assert.That(handler.CanUndo.Value, Is.True);
Assert.That(handler.CanRedo.Value, Is.False);
Assert.That(stateChangedFired, Is.EqualTo(1));
Assert.That(stateChangedFired, Is.EqualTo(2));
string hash = handler.CurrentStateHash;
@ -69,7 +104,7 @@ namespace osu.Game.Tests.Editing
handler.RestoreState(-1);
Assert.That(originalHash, Is.EqualTo(handler.CurrentStateHash));
Assert.That(stateChangedFired, Is.EqualTo(2));
Assert.That(stateChangedFired, Is.EqualTo(3));
addArbitraryChange(beatmap);
handler.SaveState();
@ -84,12 +119,16 @@ namespace osu.Game.Tests.Editing
Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.False);
// Save initial state
handler.SaveState();
Assert.That(stateChangedFired, Is.EqualTo(1));
addArbitraryChange(beatmap);
handler.SaveState();
Assert.That(handler.CanUndo.Value, Is.True);
Assert.That(handler.CanRedo.Value, Is.False);
Assert.That(stateChangedFired, Is.EqualTo(1));
Assert.That(stateChangedFired, Is.EqualTo(2));
string hash = handler.CurrentStateHash;
@ -97,7 +136,7 @@ namespace osu.Game.Tests.Editing
handler.SaveState();
Assert.That(hash, Is.EqualTo(handler.CurrentStateHash));
Assert.That(stateChangedFired, Is.EqualTo(1));
Assert.That(stateChangedFired, Is.EqualTo(2));
handler.RestoreState(-1);
@ -106,7 +145,7 @@ namespace osu.Game.Tests.Editing
// we should only be able to restore once even though we saved twice.
Assert.That(handler.CanUndo.Value, Is.False);
Assert.That(handler.CanRedo.Value, Is.True);
Assert.That(stateChangedFired, Is.EqualTo(2));
Assert.That(stateChangedFired, Is.EqualTo(3));
}
[Test]
@ -114,11 +153,15 @@ namespace osu.Game.Tests.Editing
{
var (handler, beatmap) = createChangeHandler();
// Save initial state
handler.SaveState();
Assert.That(stateChangedFired, Is.EqualTo(1));
Assert.That(handler.CanUndo.Value, Is.False);
for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++)
{
Assert.That(stateChangedFired, Is.EqualTo(i));
Assert.That(stateChangedFired, Is.EqualTo(i + 1));
addArbitraryChange(beatmap);
handler.SaveState();
@ -169,7 +212,7 @@ namespace osu.Game.Tests.Editing
},
});
var changeHandler = new EditorChangeHandler(beatmap);
var changeHandler = new BeatmapEditorChangeHandler(beatmap);
changeHandler.OnStateChange += () => stateChangedFired++;
return (changeHandler, beatmap);

View File

@ -31,8 +31,8 @@ namespace osu.Game.Tests.Visual.Audio
private WaveformTestBeatmap beatmap;
private OsuSliderBar<int> lowPassSlider;
private OsuSliderBar<int> highPassSlider;
private RoundedSliderBar<int> lowPassSlider;
private RoundedSliderBar<int> highPassSlider;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
@ -52,7 +52,7 @@ namespace osu.Game.Tests.Visual.Audio
Text = $"Low Pass: {lowPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40)
},
lowPassSlider = new OsuSliderBar<int>
lowPassSlider = new RoundedSliderBar<int>
{
Width = 500,
Height = 50,
@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Audio
Text = $"High Pass: {highPassFilter.Cutoff}hz",
Font = new FontUsage(size: 40)
},
highPassSlider = new OsuSliderBar<int>
highPassSlider = new RoundedSliderBar<int>
{
Width = 500,
Height = 50,

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

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.IO;
using System.Linq;
using NUnit.Framework;
using osu.Framework;
@ -14,6 +15,8 @@ using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Navigation
{
@ -23,11 +26,13 @@ namespace osu.Game.Tests.Visual.Navigation
{
private HeadlessGameHost ipcSenderHost = null!;
private OsuSchemeLinkIPCChannel osuSchemeLinkIPCReceiver = null!;
private OsuSchemeLinkIPCChannel osuSchemeLinkIPCSender = null!;
private ArchiveImportIPCChannel archiveImportIPCSender = null!;
private const int requested_beatmap_set_id = 1;
protected override TestOsuGame CreateTestGame() => new IpcGame(LocalStorage, API);
[Resolved]
private GameHost gameHost { get; set; } = null!;
@ -56,11 +61,11 @@ namespace osu.Game.Tests.Visual.Navigation
return false;
};
});
AddStep("create IPC receiver channel", () => osuSchemeLinkIPCReceiver = new OsuSchemeLinkIPCChannel(gameHost, Game));
AddStep("create IPC sender channel", () =>
AddStep("create IPC sender channels", () =>
{
ipcSenderHost = new HeadlessGameHost(gameHost.Name, new HostOptions { BindIPC = true });
osuSchemeLinkIPCSender = new OsuSchemeLinkIPCChannel(ipcSenderHost);
archiveImportIPCSender = new ArchiveImportIPCChannel(ipcSenderHost);
});
}
@ -72,15 +77,50 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("beatmap overlay showing content", () => Game.ChildrenOfType<BeatmapSetOverlay>().FirstOrDefault()?.Header.BeatmapSet.Value.OnlineID == requested_beatmap_set_id);
}
[Test]
public void TestArchiveImportLinkIPCChannel()
{
string? beatmapFilepath = null;
AddStep("import beatmap via IPC", () => archiveImportIPCSender.ImportAsync(beatmapFilepath = TestResources.GetQuickTestBeatmapForImport()).WaitSafely());
AddUntilStep("import complete notification was presented", () => Game.Notifications.ChildrenOfType<ProgressCompletionNotification>().Count(), () => Is.EqualTo(1));
AddAssert("original file deleted", () => File.Exists(beatmapFilepath), () => Is.False);
}
public override void TearDownSteps()
{
AddStep("dispose IPC receiver", () => osuSchemeLinkIPCReceiver.Dispose());
AddStep("dispose IPC sender", () =>
AddStep("dispose IPC senders", () =>
{
osuSchemeLinkIPCSender.Dispose();
archiveImportIPCSender.Dispose();
ipcSenderHost.Dispose();
});
base.TearDownSteps();
}
private partial class IpcGame : TestOsuGame
{
private OsuSchemeLinkIPCChannel? osuSchemeLinkIPCChannel;
private ArchiveImportIPCChannel? archiveImportIPCChannel;
public IpcGame(Storage storage, IAPIProvider api, string[]? args = null)
: base(storage, api, args)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
osuSchemeLinkIPCChannel = new OsuSchemeLinkIPCChannel(Host, this);
archiveImportIPCChannel = new ArchiveImportIPCChannel(Host, this);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
osuSchemeLinkIPCChannel?.Dispose();
archiveImportIPCChannel?.Dispose();
}
}
}
}

View File

@ -0,0 +1,92 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Profile.Header.Components;
using osuTK;
namespace osu.Game.Tests.Visual.Online
{
[TestFixture]
public partial class TestSceneGroupBadges : OsuTestScene
{
public TestSceneGroupBadges()
{
var groups = new[]
{
new APIUser(),
new APIUser
{
Groups = new[]
{
new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
}
},
new APIUser
{
Groups = new[]
{
new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko" } }
}
},
new APIUser
{
Groups = new[]
{
new APIUserGroup { Colour = "#0066FF", ShortName = "PPY", Name = "peppy" },
new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko" } }
}
},
new APIUser
{
Groups = new[]
{
new APIUserGroup { Colour = "#0066FF", ShortName = "PPY", Name = "peppy" },
new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
new APIUserGroup { Colour = "#999999", ShortName = "ALM", Name = "osu! Alumni" },
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko" } },
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators (Probationary)", Playmodes = new[] { "osu", "taiko" }, IsProbationary = true }
}
}
};
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Colour4.DarkGray
},
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(40),
Children = new[]
{
new FillFlowContainer<GroupBadgeFlow>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5),
ChildrenEnumerable = groups.Select(g => new GroupBadgeFlow { User = { Value = g } })
},
}
}
};
}
}
}

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

@ -90,7 +90,9 @@ namespace osu.Game.Tests.Visual.Online
{
new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "mania" } },
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko" } }
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko" } },
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators", Playmodes = new[] { "osu", "taiko", "fruits", "mania" } },
new APIUserGroup { Colour = "#A347EB", ShortName = "BN", Name = "Beatmap Nominators (Probationary)", Playmodes = new[] { "osu", "taiko", "fruits", "mania" }, IsProbationary = true }
},
ProfileOrder = new[]
{
@ -119,6 +121,12 @@ namespace osu.Game.Tests.Visual.Online
Data = Enumerable.Range(2345, 45).Concat(Enumerable.Range(2109, 40)).ToArray()
},
},
TournamentBanner = new TournamentBanner
{
Id = 13926,
TournamentId = 35,
ImageLowRes = "https://assets.ppy.sh/tournament-banners/official/owc2022/profile/winner_US.jpg",
},
Badges = new[]
{
new Badge

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

@ -261,7 +261,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep($"Set {name} slider to {value}", () =>
this.ChildrenOfType<DifficultyAdjustSettingsControl>().First(c => c.LabelText == name)
.ChildrenOfType<OsuSliderBar<float>>().First().Current.Value = value);
.ChildrenOfType<RoundedSliderBar<float>>().First().Current.Value = value);
}
private void checkBindableAtValue(string name, float? expectedValue)
@ -275,7 +275,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddAssert($"Slider {name} at {expectedValue}", () =>
this.ChildrenOfType<DifficultyAdjustSettingsControl>().First(c => c.LabelText == name)
.ChildrenOfType<OsuSliderBar<float>>().First().Current.Value == expectedValue);
.ChildrenOfType<RoundedSliderBar<float>>().First().Current.Value == expectedValue);
}
private void setBeatmapWithDifficultyParameters(float value)

View File

@ -270,7 +270,7 @@ namespace osu.Game.Tests.Visual.UserInterface
createScreen();
AddStep("select difficulty adjust via panel", () => getPanelForMod(typeof(OsuModDifficultyAdjust)).TriggerClick());
AddStep("set setting", () => modSelectOverlay.ChildrenOfType<OsuSliderBar<float>>().First().Current.Value = 8);
AddStep("set setting", () => modSelectOverlay.ChildrenOfType<RoundedSliderBar<float>>().First().Current.Value = 8);
AddAssert("ensure setting is propagated", () => SelectedMods.Value.OfType<OsuModDifficultyAdjust>().Single().CircleSize.Value == 8);

View File

@ -0,0 +1,37 @@
// 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.Game.Graphics.UserInterface;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneShearedSliderBar : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Purple);
private readonly BindableDouble current = new BindableDouble(5)
{
Precision = 0.1f,
MinValue = 0,
MaxValue = 15
};
[BackgroundDependencyLoader]
private void load()
{
Child = new ShearedSliderBar<double>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Current = current,
RelativeSizeAxes = Axes.X,
Width = 0.4f
};
}
}
}

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

@ -66,10 +66,16 @@ namespace osu.Game.Extensions
foreach (var (_, property) in component.GetSettingsSourceProperties())
{
if (!info.Settings.TryGetValue(property.Name.ToSnakeCase(), out object? settingValue))
continue;
var bindable = ((IBindable)property.GetValue(component)!);
skinnable.CopyAdjustedSetting(((IBindable)property.GetValue(component)!), settingValue);
if (!info.Settings.TryGetValue(property.Name.ToSnakeCase(), out object? settingValue))
{
// TODO: We probably want to restore default if not included in serialisation information.
// This is not simple to do as SetDefault() is only found in the typed Bindable<T> interface right now.
continue;
}
skinnable.CopyAdjustedSetting(bindable, settingValue);
}
}

View File

@ -294,8 +294,14 @@ namespace osu.Game.Graphics.Backgrounds
vertexBatch = renderer.CreateQuadBatch<TexturedVertex2D>(Source.AimCount, 1);
}
// Due to triangles having various sizes we would need to set a different "texelSize" value for each of them, which is insanely expensive, thus we should use one single value.
// texelSize computed for an average triangle (size 100) will result in big triangles becoming blurry, so we may just use 0 for all of them.
// But we still need to specify at least something, because otherwise other shader usages will override this value.
float texelSize = 0f;
shader.Bind();
shader.GetUniform<float>("thickness").UpdateValue(ref fill);
shader.GetUniform<float>("texelSize").UpdateValue(ref texelSize);
foreach (TriangleParticle particle in parts)
{

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

@ -44,8 +44,11 @@ namespace osu.Game.Graphics.Containers
content.AutoSizeAxes = AutoSizeAxes;
}
AddInternal(content);
Add(CreateHoverSounds(sampleSet));
AddRangeInternal(new Drawable[]
{
CreateHoverSounds(sampleSet),
content,
});
}
protected override void ClearInternal(bool disposeChildren = true) =>

View File

@ -21,7 +21,7 @@ namespace osu.Game.Graphics.UserInterface
/// </summary>
public partial class ExpandableSlider<T, TSlider> : CompositeDrawable, IExpandable, IHasCurrentValue<T>
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
where TSlider : OsuSliderBar<T>, new()
where TSlider : RoundedSliderBar<T>, new()
{
private readonly OsuSpriteText label;
private readonly TSlider slider;
@ -130,7 +130,7 @@ namespace osu.Game.Graphics.UserInterface
/// <summary>
/// An <see cref="IExpandable"/> implementation for the UI slider bar control.
/// </summary>
public partial class ExpandableSlider<T> : ExpandableSlider<T, OsuSliderBar<T>>
public partial class ExpandableSlider<T> : ExpandableSlider<T, RoundedSliderBar<T>>
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
{
}

View File

@ -44,7 +44,7 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnClick(ClickEvent e)
{
if (buttons.Contains(e.Button) && Contains(e.ScreenSpaceMousePosition))
if (buttons.Contains(e.Button))
{
var channel = Enabled.Value ? sampleClick?.GetChannel() : sampleClickDisabled?.GetChannel();

View File

@ -5,19 +5,22 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osuTK;
namespace osu.Game.Graphics.UserInterface
{
/// <summary>
/// Handles debouncing hover sounds at a global level to ensure the effects are not overwhelming.
/// </summary>
public abstract partial class HoverSampleDebounceComponent : CompositeDrawable
public abstract partial class HoverSampleDebounceComponent : Component
{
private Bindable<double?> lastPlaybackTime;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent?.ReceivePositionalInputAt(screenSpacePos) == true;
[BackgroundDependencyLoader]
private void load(SessionStatics statics)
{

View File

@ -1,10 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using JetBrains.Annotations;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Allocation;
@ -58,7 +55,7 @@ namespace osu.Game.Graphics.UserInterface
}
[BackgroundDependencyLoader(true)]
private void load([CanBeNull] OverlayColourProvider colourProvider, OsuColour colours)
private void load(OverlayColourProvider? colourProvider, OsuColour colours)
{
AccentColour = colourProvider?.Highlight1 ?? colours.Pink;
GlowingAccentColour = colourProvider?.Highlight1.Lighten(0.2f) ?? colours.PinkLighter;

View File

@ -116,7 +116,7 @@ namespace osu.Game.Graphics.UserInterface
});
if (hoverSounds.HasValue)
Add(new HoverClickSounds(hoverSounds.Value) { Enabled = { BindTarget = Enabled } });
AddInternal(new HoverClickSounds(hoverSounds.Value) { Enabled = { BindTarget = Enabled } });
}
[BackgroundDependencyLoader]

View File

@ -1,195 +1,59 @@
// 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.
#nullable disable
using System;
using System.Globalization;
using JetBrains.Annotations;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Overlays;
using osu.Game.Utils;
namespace osu.Game.Graphics.UserInterface
{
public partial class OsuSliderBar<T> : SliderBar<T>, IHasTooltip, IHasAccentColour
public abstract partial class OsuSliderBar<T> : SliderBar<T>, IHasTooltip
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
{
/// <summary>
/// Maximum number of decimal digits to be displayed in the tooltip.
/// </summary>
private const int max_decimal_digits = 5;
private Sample sample;
private double lastSampleTime;
private T lastSampleValue;
protected readonly Nub Nub;
protected readonly Box LeftBox;
protected readonly Box RightBox;
private readonly Container nubContainer;
public virtual LocalisableString TooltipText { get; private set; }
public bool PlaySamplesOnAdjust { get; set; } = true;
private readonly HoverClickSounds hoverClickSounds;
/// <summary>
/// Whether to format the tooltip as a percentage or the actual value.
/// </summary>
public bool DisplayAsPercentage { get; set; }
private Color4 accentColour;
public virtual LocalisableString TooltipText { get; private set; }
public Color4 AccentColour
{
get => accentColour;
set
{
accentColour = value;
LeftBox.Colour = value;
}
}
/// <summary>
/// Maximum number of decimal digits to be displayed in the tooltip.
/// </summary>
private const int max_decimal_digits = 5;
private Colour4 backgroundColour;
private Sample sample = null!;
public Color4 BackgroundColour
{
get => backgroundColour;
set
{
backgroundColour = value;
RightBox.Colour = value;
}
}
private double lastSampleTime;
private T lastSampleValue;
public OsuSliderBar()
{
Height = Nub.HEIGHT;
RangePadding = Nub.EXPANDED_SIZE / 2;
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Padding = new MarginPadding { Horizontal = 2 },
Child = new CircularContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Masking = true,
CornerRadius = 5f,
Children = new Drawable[]
{
LeftBox = new Box
{
Height = 5,
EdgeSmoothness = new Vector2(0, 0.5f),
RelativeSizeAxes = Axes.None,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
RightBox = new Box
{
Height = 5,
EdgeSmoothness = new Vector2(0, 0.5f),
RelativeSizeAxes = Axes.None,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
},
},
},
},
nubContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Child = Nub = new Nub
{
Origin = Anchor.TopCentre,
RelativePositionAxes = Axes.X,
Current = { Value = true }
},
},
hoverClickSounds = new HoverClickSounds()
};
}
[BackgroundDependencyLoader(true)]
private void load(AudioManager audio, [CanBeNull] OverlayColourProvider colourProvider, OsuColour colours)
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
sample = audio.Samples.Get(@"UI/notch-tick");
AccentColour = colourProvider?.Highlight1 ?? colours.Pink;
BackgroundColour = colourProvider?.Background5 ?? colours.PinkDarker.Darken(1);
}
protected override void Update()
{
base.Update();
nubContainer.Padding = new MarginPadding { Horizontal = RangePadding };
}
protected override void LoadComplete()
{
base.LoadComplete();
CurrentNumber.BindValueChanged(current => TooltipText = getTooltipText(current.NewValue), true);
Current.BindDisabledChanged(disabled =>
{
Alpha = disabled ? 0.3f : 1;
hoverClickSounds.Enabled.Value = !disabled;
}, true);
}
protected override bool OnHover(HoverEvent e)
{
updateGlow();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateGlow();
base.OnHoverLost(e);
}
protected override bool ShouldHandleAsRelativeDrag(MouseDownEvent e)
=> Nub.ReceivePositionalInputAt(e.ScreenSpaceMouseDownPosition);
protected override void OnDragEnd(DragEndEvent e)
{
updateGlow();
base.OnDragEnd(e);
}
private void updateGlow()
{
Nub.Glowing = !Current.Disabled && (IsHovered || IsDragged);
}
protected override void OnUserChange(T value)
{
base.OnUserChange(value);
playSample(value);
TooltipText = getTooltipText(value);
}
@ -236,18 +100,6 @@ namespace osu.Game.Graphics.UserInterface
return floatValue.ToString($"N{significantDigits}");
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
LeftBox.Scale = new Vector2(Math.Clamp(RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2, 0, Math.Max(0, DrawWidth)), 1);
RightBox.Scale = new Vector2(Math.Clamp(DrawWidth - Nub.DrawPosition.X - RangePadding - Nub.DrawWidth / 2, 0, Math.Max(0, DrawWidth)), 1);
}
protected override void UpdateValue(float value)
{
Nub.MoveToX(value, 250, Easing.OutQuint);
}
/// <summary>
/// Removes all non-significant digits, keeping at most a requested number of decimal digits.
/// </summary>

View File

@ -158,7 +158,7 @@ namespace osu.Game.Graphics.UserInterface
&& screenSpacePos.X >= Nub.ScreenSpaceDrawQuad.TopLeft.X;
}
protected partial class BoundSlider : OsuSliderBar<double>
protected partial class BoundSlider : RoundedSliderBar<double>
{
public string? DefaultString;
public LocalisableString? DefaultTooltip;

View File

@ -0,0 +1,170 @@
// 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 osuTK;
using osuTK.Graphics;
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.Framework.Input.Events;
using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterface
{
public partial class RoundedSliderBar<T> : OsuSliderBar<T>
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
{
protected readonly Nub Nub;
protected readonly Box LeftBox;
protected readonly Box RightBox;
private readonly Container nubContainer;
private readonly HoverClickSounds hoverClickSounds;
private Color4 accentColour;
public Color4 AccentColour
{
get => accentColour;
set
{
accentColour = value;
LeftBox.Colour = value;
}
}
private Colour4 backgroundColour;
public Color4 BackgroundColour
{
get => backgroundColour;
set
{
backgroundColour = value;
RightBox.Colour = value;
}
}
public RoundedSliderBar()
{
Height = Nub.HEIGHT;
RangePadding = Nub.EXPANDED_SIZE / 2;
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Padding = new MarginPadding { Horizontal = 2 },
Child = new CircularContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Masking = true,
CornerRadius = 5f,
Children = new Drawable[]
{
LeftBox = new Box
{
Height = 5,
EdgeSmoothness = new Vector2(0, 0.5f),
RelativeSizeAxes = Axes.None,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
RightBox = new Box
{
Height = 5,
EdgeSmoothness = new Vector2(0, 0.5f),
RelativeSizeAxes = Axes.None,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
},
},
},
},
nubContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Child = Nub = new Nub
{
Origin = Anchor.TopCentre,
RelativePositionAxes = Axes.X,
Current = { Value = true }
},
},
hoverClickSounds = new HoverClickSounds()
};
}
[BackgroundDependencyLoader(true)]
private void load(OverlayColourProvider? colourProvider, OsuColour colours)
{
AccentColour = colourProvider?.Highlight1 ?? colours.Pink;
BackgroundColour = colourProvider?.Background5 ?? colours.PinkDarker.Darken(1);
}
protected override void Update()
{
base.Update();
nubContainer.Padding = new MarginPadding { Horizontal = RangePadding };
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindDisabledChanged(disabled =>
{
Alpha = disabled ? 0.3f : 1;
hoverClickSounds.Enabled.Value = !disabled;
}, true);
}
protected override bool OnHover(HoverEvent e)
{
updateGlow();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateGlow();
base.OnHoverLost(e);
}
protected override bool ShouldHandleAsRelativeDrag(MouseDownEvent e)
=> Nub.ReceivePositionalInputAt(e.ScreenSpaceMouseDownPosition);
protected override void OnDragEnd(DragEndEvent e)
{
updateGlow();
base.OnDragEnd(e);
}
private void updateGlow()
{
Nub.Glowing = !Current.Disabled && (IsHovered || IsDragged);
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
LeftBox.Scale = new Vector2(Math.Clamp(RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2, 0, Math.Max(0, DrawWidth)), 1);
RightBox.Scale = new Vector2(Math.Clamp(DrawWidth - Nub.DrawPosition.X - RangePadding - Nub.DrawWidth / 2, 0, Math.Max(0, DrawWidth)), 1);
}
protected override void UpdateValue(float value)
{
Nub.MoveToX(value, 250, Easing.OutQuint);
}
}
}

View File

@ -0,0 +1,183 @@
// 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.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.UserInterface;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface
{
public partial class ShearedNub : Container, IHasCurrentValue<bool>, IHasAccentColour
{
protected const float BORDER_WIDTH = 3;
public const int HEIGHT = 30;
public const float EXPANDED_SIZE = 50;
public static readonly Vector2 SHEAR = new Vector2(0.15f, 0);
private readonly Box fill;
private readonly Container main;
/// <summary>
/// Implements the shape for the nub, allowing for any type of container to be used.
/// </summary>
/// <returns></returns>
public ShearedNub()
{
Size = new Vector2(EXPANDED_SIZE, HEIGHT);
InternalChild = main = new Container
{
Shear = SHEAR,
BorderColour = Colour4.White,
BorderThickness = BORDER_WIDTH,
Masking = true,
CornerRadius = 5,
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Child = fill = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true,
}
};
}
[BackgroundDependencyLoader(true)]
private void load(OverlayColourProvider? colourProvider, OsuColour colours)
{
AccentColour = colourProvider?.Highlight1 ?? colours.Pink;
GlowingAccentColour = colourProvider?.Highlight1.Lighten(0.4f) ?? colours.PinkLighter;
GlowColour = colourProvider?.Highlight1 ?? colours.PinkLighter;
main.EdgeEffect = new EdgeEffectParameters
{
Colour = GlowColour.Opacity(0),
Type = EdgeEffectType.Glow,
Radius = 8,
Roundness = 4,
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindValueChanged(onCurrentValueChanged, true);
}
private bool glowing;
public bool Glowing
{
get => glowing;
set
{
if (glowing == value)
return;
glowing = value;
if (value)
{
main.FadeColour(GlowingAccentColour.Lighten(0.1f), 40, Easing.OutQuint)
.Then()
.FadeColour(GlowingAccentColour, 800, Easing.OutQuint);
main.FadeEdgeEffectTo(Color4.White.Opacity(0.1f), 40, Easing.OutQuint)
.Then()
.FadeEdgeEffectTo(GlowColour.Opacity(0.1f), 800, Easing.OutQuint);
}
else
{
main.FadeEdgeEffectTo(GlowColour.Opacity(0), 800, Easing.OutQuint);
main.FadeColour(AccentColour, 800, Easing.OutQuint);
}
}
}
private readonly Bindable<bool> current = new Bindable<bool>();
public Bindable<bool> Current
{
get => current;
set
{
ArgumentNullException.ThrowIfNull(value);
current.UnbindBindings();
current.BindTo(value);
}
}
private Color4 accentColour;
public Color4 AccentColour
{
get => accentColour;
set
{
accentColour = value;
if (!Glowing)
main.Colour = value;
}
}
private Color4 glowingAccentColour;
public Color4 GlowingAccentColour
{
get => glowingAccentColour;
set
{
glowingAccentColour = value;
if (Glowing)
main.Colour = value;
}
}
private Color4 glowColour;
public Color4 GlowColour
{
get => glowColour;
set
{
glowColour = value;
var effect = main.EdgeEffect;
effect.Colour = Glowing ? value : value.Opacity(0);
main.EdgeEffect = effect;
}
}
private void onCurrentValueChanged(ValueChangedEvent<bool> filled)
{
const double duration = 200;
fill.FadeTo(filled.NewValue ? 1 : 0, duration, Easing.OutQuint);
if (filled.NewValue)
{
main.ResizeWidthTo(1, duration, Easing.OutElasticHalf);
main.TransformTo(nameof(BorderThickness), 8.5f, duration, Easing.OutElasticHalf);
}
else
{
main.ResizeWidthTo(0.75f, duration, Easing.OutQuint);
main.TransformTo(nameof(BorderThickness), BORDER_WIDTH, duration, Easing.OutQuint);
}
}
}
}

View File

@ -0,0 +1,173 @@
// 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 osuTK;
using osuTK.Graphics;
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.Framework.Input.Events;
using osu.Game.Overlays;
using static osu.Game.Graphics.UserInterface.ShearedNub;
namespace osu.Game.Graphics.UserInterface
{
public partial class ShearedSliderBar<T> : OsuSliderBar<T>
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
{
protected readonly ShearedNub Nub;
protected readonly Box LeftBox;
protected readonly Box RightBox;
private readonly Container nubContainer;
private readonly HoverClickSounds hoverClickSounds;
private Color4 accentColour;
public Color4 AccentColour
{
get => accentColour;
set
{
accentColour = value;
// We want to slightly darken the colour for the box because the sheared slider has the boxes at the same height as the nub,
// making the nub invisible when not hovered.
LeftBox.Colour = value.Darken(0.1f);
}
}
private Colour4 backgroundColour;
public Color4 BackgroundColour
{
get => backgroundColour;
set
{
backgroundColour = value;
RightBox.Colour = value;
}
}
public ShearedSliderBar()
{
Shear = SHEAR;
Height = HEIGHT;
RangePadding = EXPANDED_SIZE / 2;
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Padding = new MarginPadding { Horizontal = 2 },
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Masking = true,
CornerRadius = 5,
Children = new Drawable[]
{
LeftBox = new Box
{
EdgeSmoothness = new Vector2(0, 0.5f),
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
RightBox = new Box
{
EdgeSmoothness = new Vector2(0, 0.5f),
RelativeSizeAxes = Axes.Y,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
},
},
},
},
nubContainer = new Container
{
Shear = -SHEAR,
RelativeSizeAxes = Axes.Both,
Child = Nub = new ShearedNub
{
X = -SHEAR.X * HEIGHT / 2f,
Origin = Anchor.TopCentre,
RelativePositionAxes = Axes.X,
Current = { Value = true }
},
},
hoverClickSounds = new HoverClickSounds()
};
}
[BackgroundDependencyLoader(true)]
private void load(OverlayColourProvider? colourProvider, OsuColour colours)
{
AccentColour = colourProvider?.Highlight1 ?? colours.Pink;
BackgroundColour = colourProvider?.Background5 ?? colours.PinkDarker.Darken(1);
}
protected override void Update()
{
base.Update();
nubContainer.Padding = new MarginPadding { Horizontal = RangePadding };
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindDisabledChanged(disabled =>
{
Alpha = disabled ? 0.3f : 1;
hoverClickSounds.Enabled.Value = !disabled;
}, true);
}
protected override bool OnHover(HoverEvent e)
{
updateGlow();
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateGlow();
base.OnHoverLost(e);
}
protected override bool ShouldHandleAsRelativeDrag(MouseDownEvent e)
=> Nub.ReceivePositionalInputAt(e.ScreenSpaceMouseDownPosition);
protected override void OnDragEnd(DragEndEvent e)
{
updateGlow();
base.OnDragEnd(e);
}
private void updateGlow()
{
Nub.Glowing = !Current.Disabled && (IsHovered || IsDragged);
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
LeftBox.Scale = new Vector2(Math.Clamp(RangePadding + Nub.DrawPosition.X - Nub.DrawWidth / 2.15f, 0, Math.Max(0, DrawWidth)), 1);
RightBox.Scale = new Vector2(Math.Clamp(DrawWidth - Nub.DrawPosition.X - RangePadding - Nub.DrawWidth / 2.15f, 0, Math.Max(0, DrawWidth)), 1);
}
protected override void UpdateValue(float value)
{
Nub.MoveToX(value, 250, Easing.OutQuint);
}
}
}

View File

@ -10,7 +10,7 @@ namespace osu.Game.Graphics.UserInterface
/// <summary>
/// A slider bar which displays a millisecond time value.
/// </summary>
public partial class TimeSlider : OsuSliderBar<double>
public partial class TimeSlider : RoundedSliderBar<double>
{
public override LocalisableString TooltipText => $"{Current.Value:N0} ms";
}

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

@ -0,0 +1,89 @@
// 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.Localisation;
namespace osu.Game.Localisation.HUD
{
public static class BarHitErrorMeterStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.HUD.BarHitErrorMeter";
/// <summary>
/// "Judgement line thickness"
/// </summary>
public static LocalisableString JudgementLineThickness => new TranslatableString(getKey(@"judgement_line_thickness"), "Judgement line thickness");
/// <summary>
/// "How thick the individual lines should be."
/// </summary>
public static LocalisableString JudgementLineThicknessDescription => new TranslatableString(getKey(@"judgement_line_thickness_description"), "How thick the individual lines should be.");
/// <summary>
/// "Show colour bars"
/// </summary>
public static LocalisableString ColourBarVisibility => new TranslatableString(getKey(@"colour_bar_visibility"), "Show colour bars");
/// <summary>
/// "Show moving average arrow"
/// </summary>
public static LocalisableString ShowMovingAverage => new TranslatableString(getKey(@"show_moving_average"), "Show moving average arrow");
/// <summary>
/// "Whether an arrow should move beneath the bar showing the average error."
/// </summary>
public static LocalisableString ShowMovingAverageDescription => new TranslatableString(getKey(@"show_moving_average_description"), "Whether an arrow should move beneath the bar showing the average error.");
/// <summary>
/// "Centre marker style"
/// </summary>
public static LocalisableString CentreMarkerStyle => new TranslatableString(getKey(@"centre_marker_style"), "Centre marker style");
/// <summary>
/// "How to signify the centre of the display"
/// </summary>
public static LocalisableString CentreMarkerStyleDescription => new TranslatableString(getKey(@"centre_marker_style_description"), "How to signify the centre of the display");
/// <summary>
/// "None"
/// </summary>
public static LocalisableString CentreMarkerStylesNone => new TranslatableString(getKey(@"centre_marker_styles_none"), "None");
/// <summary>
/// "Circle"
/// </summary>
public static LocalisableString CentreMarkerStylesCircle => new TranslatableString(getKey(@"centre_marker_styles_circle"), "Circle");
/// <summary>
/// "Line"
/// </summary>
public static LocalisableString CentreMarkerStylesLine => new TranslatableString(getKey(@"centre_marker_styles_line"), "Line");
/// <summary>
/// "Label style"
/// </summary>
public static LocalisableString LabelStyle => new TranslatableString(getKey(@"label_style"), "Label style");
/// <summary>
/// "How to show early/late extremities"
/// </summary>
public static LocalisableString LabelStyleDescription => new TranslatableString(getKey(@"label_style_description"), "How to show early/late extremities");
/// <summary>
/// "None"
/// </summary>
public static LocalisableString LabelStylesNone => new TranslatableString(getKey(@"label_styles_none"), "None");
/// <summary>
/// "Icons"
/// </summary>
public static LocalisableString LabelStylesIcons => new TranslatableString(getKey(@"label_styles_icons"), "Icons");
/// <summary>
/// "Text"
/// </summary>
public static LocalisableString LabelStylesText => new TranslatableString(getKey(@"label_styles_text"), "Text");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@ -0,0 +1,54 @@
// 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.Localisation;
namespace osu.Game.Localisation.HUD
{
public static class ColourHitErrorMeterStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.HUD.ColourHitError";
/// <summary>
/// "Judgement count"
/// </summary>
public static LocalisableString JudgementCount => new TranslatableString(getKey(@"judgement_count"), "Judgement count");
/// <summary>
/// "The number of displayed judgements"
/// </summary>
public static LocalisableString JudgementCountDescription => new TranslatableString(getKey(@"judgement_count_description"), "The number of displayed judgements");
/// <summary>
/// "Judgement spacing"
/// </summary>
public static LocalisableString JudgementSpacing => new TranslatableString(getKey(@"judgement_spacing"), "Judgement spacing");
/// <summary>
/// "The space between each displayed judgement"
/// </summary>
public static LocalisableString JudgementSpacingDescription => new TranslatableString(getKey(@"judgement_spacing_description"), "The space between each displayed judgement");
/// <summary>
/// "Judgement shape"
/// </summary>
public static LocalisableString JudgementShape => new TranslatableString(getKey(@"judgement_shape"), "Judgement shape");
/// <summary>
/// "The shape of each displayed judgement"
/// </summary>
public static LocalisableString JudgementShapeDescription => new TranslatableString(getKey(@"judgement_shape_description"), "The shape of each displayed judgement");
/// <summary>
/// "Circle"
/// </summary>
public static LocalisableString ShapeStyleCircle => new TranslatableString(getKey(@"shape_style_cricle"), "Circle");
/// <summary>
/// "Square"
/// </summary>
public static LocalisableString ShapeStyleSquare => new TranslatableString(getKey(@"shape_style_square"), "Square");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@ -0,0 +1,39 @@
// 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.Localisation;
namespace osu.Game.Localisation.HUD
{
public static class GameplayAccuracyCounterStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.HUD.GameplayAccuracyCounter";
/// <summary>
/// "Accuracy display mode"
/// </summary>
public static LocalisableString AccuracyDisplay => new TranslatableString(getKey(@"accuracy_display"), "Accuracy display mode");
/// <summary>
/// "Which accuracy mode should be displayed."
/// </summary>
public static LocalisableString AccuracyDisplayDescription => new TranslatableString(getKey(@"accuracy_display_description"), "Which accuracy mode should be displayed.");
/// <summary>
/// "Standard"
/// </summary>
public static LocalisableString AccuracyDisplayModeStandard => new TranslatableString(getKey(@"accuracy_display_mode_standard"), "Standard");
/// <summary>
/// "Maximum achievable"
/// </summary>
public static LocalisableString AccuracyDisplayModeMax => new TranslatableString(getKey(@"accuracy_display_mode_max"), "Maximum achievable");
/// <summary>
/// "Minimum achievable"
/// </summary>
public static LocalisableString AccuracyDisplayModeMin => new TranslatableString(getKey(@"accuracy_display_mode_min"), "Minimum achievable");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@ -0,0 +1,49 @@
// 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.Localisation;
namespace osu.Game.Localisation.HUD
{
public static class JudgementCounterDisplayStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.HUD.JudgementCounterDisplay";
/// <summary>
/// "Display mode"
/// </summary>
public static LocalisableString JudgementDisplayMode => new TranslatableString(getKey(@"judgement_display_mode"), "Display mode");
/// <summary>
/// "Counter direction"
/// </summary>
public static LocalisableString FlowDirection => new TranslatableString(getKey(@"flow_direction"), "Counter direction");
/// <summary>
/// "Show judgement names"
/// </summary>
public static LocalisableString ShowJudgementNames => new TranslatableString(getKey(@"show_judgement_names"), "Show judgement names");
/// <summary>
/// "Show max judgement"
/// </summary>
public static LocalisableString ShowMaxJudgement => new TranslatableString(getKey(@"show_max_judgement"), "Show max judgement");
/// <summary>
/// "Simple"
/// </summary>
public static LocalisableString JudgementDisplayModeSimple => new TranslatableString(getKey(@"judgement_display_mode_simple"), "Simple");
/// <summary>
/// "Normal"
/// </summary>
public static LocalisableString JudgementDisplayModeNormal => new TranslatableString(getKey(@"judgement_display_mode_normal"), "Normal");
/// <summary>
/// "All"
/// </summary>
public static LocalisableString JudgementDisplayModeAll => new TranslatableString(getKey(@"judgement_display_mode_all"), "All");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

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.Localisation;
namespace osu.Game.Localisation.HUD
{
public static class SongProgressStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.HUD.SongProgress";
/// <summary>
/// "Show difficulty graph"
/// </summary>
public static LocalisableString ShowGraph => new TranslatableString(getKey(@"show_graph"), "Show difficulty graph");
/// <summary>
/// "Whether a graph displaying difficulty throughout the beatmap should be shown"
/// </summary>
public static LocalisableString ShowGraphDescription => new TranslatableString(getKey(@"show_graph_description"), "Whether a graph displaying difficulty throughout the beatmap should be shown");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@ -0,0 +1,34 @@
// 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.Localisation;
namespace osu.Game.Localisation.SkinComponents
{
public static class BeatmapAttributeTextStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.SkinComponents.BeatmapAttributeText";
/// <summary>
/// "Attribute"
/// </summary>
public static LocalisableString Attribute => new TranslatableString(getKey(@"attribute"), "Attribute");
/// <summary>
/// "The attribute to be displayed."
/// </summary>
public static LocalisableString AttributeDescription => new TranslatableString(getKey(@"attribute_description"), "The attribute to be displayed.");
/// <summary>
/// "Template"
/// </summary>
public static LocalisableString Template => new TranslatableString(getKey(@"template"), "Template");
/// <summary>
/// "Supports {{Label}} and {{Value}}, but also including arbitrary attributes like {{StarRating}} (see attribute list for supported values)."
/// </summary>
public static LocalisableString TemplateDescription => new TranslatableString(getKey(@"template_description"), @"Supports {{Label}} and {{Value}}, but also including arbitrary attributes like {{StarRating}} (see attribute list for supported values).");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@ -0,0 +1,44 @@
// 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.Localisation;
namespace osu.Game.Localisation.SkinComponents
{
public static class SkinnableComponentStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.SkinComponents.SkinnableComponentStrings";
/// <summary>
/// "Sprite name"
/// </summary>
public static LocalisableString SpriteName => new TranslatableString(getKey(@"sprite_name"), "Sprite name");
/// <summary>
/// "The filename of the sprite"
/// </summary>
public static LocalisableString SpriteNameDescription => new TranslatableString(getKey(@"sprite_name_description"), "The filename of the sprite");
/// <summary>
/// "Font"
/// </summary>
public static LocalisableString Font => new TranslatableString(getKey(@"font"), "Font");
/// <summary>
/// "The font to use."
/// </summary>
public static LocalisableString FontDescription => new TranslatableString(getKey(@"font_description"), "The font to use.");
/// <summary>
/// "Text"
/// </summary>
public static LocalisableString TextElementText => new TranslatableString(getKey(@"text_element_text"), "Text");
/// <summary>
/// "The text to be displayed."
/// </summary>
public static LocalisableString TextElementTextDescription => new TranslatableString(getKey(@"text_element_text_description"), "The text to be displayed.");
private static string getKey(string key) => $"{prefix}:{key}";
}
}

View File

@ -39,6 +39,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString Settings(string arg0) => new TranslatableString(getKey(@"settings"), @"Settings ({0})", arg0);
/// <summary>
/// "Currently editing"
/// </summary>
public static LocalisableString CurrentlyEditing => new TranslatableString(getKey(@"currently_editing"), "Currently editing");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -234,6 +234,10 @@ namespace osu.Game.Online.API.Requests.Responses
set => Statistics.RankHistory = value;
}
[JsonProperty(@"active_tournament_banner")]
[CanBeNull]
public TournamentBanner TournamentBanner;
[JsonProperty("badges")]
public Badge[] Badges;

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

@ -315,10 +315,10 @@ namespace osu.Game.Overlays
channelListing.Hide();
textBar.ShowSearch.Value = false;
if (loadedChannels.ContainsKey(newChannel))
if (loadedChannels.TryGetValue(newChannel, out var loadedChannel))
{
currentChannelContainer.Clear(false);
currentChannelContainer.Add(loadedChannels[newChannel]);
currentChannelContainer.Add(loadedChannel);
}
else
{

View File

@ -76,6 +76,7 @@ namespace osu.Game.Overlays.Comments
private GridContainer content = null!;
private VotePill votePill = null!;
private Container<CommentEditor> replyEditorContainer = null!;
private Container repliesButtonContainer = null!;
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
@ -239,10 +240,12 @@ namespace osu.Game.Overlays.Comments
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Padding = new MarginPadding { Top = 10 },
Alpha = 0,
},
new Container
repliesButtonContainer = new Container
{
AutoSizeAxes = Axes.Both,
Alpha = 0,
Children = new Drawable[]
{
showRepliesButton = new ShowRepliesButton(Comment.RepliesCount)
@ -449,6 +452,7 @@ namespace osu.Game.Overlays.Comments
{
if (replyEditorContainer.Count == 0)
{
replyEditorContainer.Show();
replyEditorContainer.Add(new ReplyCommentEditor(Comment)
{
OnPost = comments =>
@ -456,12 +460,14 @@ namespace osu.Game.Overlays.Comments
Comment.RepliesCount += comments.Length;
showRepliesButton.Count = Comment.RepliesCount;
Replies.AddRange(comments);
}
},
OnCancel = toggleReply
});
}
else
{
replyEditorContainer.Clear(true);
replyEditorContainer.ForEach(e => e.Expire());
replyEditorContainer.Hide();
}
}
@ -513,9 +519,11 @@ namespace osu.Game.Overlays.Comments
int loadedRepliesCount = loadedReplies.Count;
bool hasUnloadedReplies = loadedRepliesCount != Comment.RepliesCount;
loadRepliesButton.FadeTo(hasUnloadedReplies && loadedRepliesCount == 0 ? 1 : 0);
showMoreButton.FadeTo(hasUnloadedReplies && loadedRepliesCount > 0 ? 1 : 0);
showRepliesButton.FadeTo(loadedRepliesCount != 0 ? 1 : 0);
loadRepliesButton.FadeTo(hasUnloadedReplies && loadedRepliesCount == 0 ? 1 : 0);
repliesButtonContainer.FadeTo(repliesButtonContainer.Any(child => child.Alpha > 0) ? 1 : 0);
showMoreButton.FadeTo(hasUnloadedReplies && loadedRepliesCount > 0 ? 1 : 0);
if (Comment.IsTopLevel)
chevronButton.FadeTo(loadedRepliesCount != 0 ? 1 : 0);

View File

@ -4,7 +4,6 @@
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Framework.Logging;
using osu.Game.Online.API;
@ -33,7 +32,6 @@ namespace osu.Game.Overlays.Comments
public ReplyCommentEditor(Comment parent)
{
parentComment = parent;
OnCancel = () => this.FadeOut(200).Expire();
}
protected override void LoadComplete()

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

@ -107,7 +107,7 @@ namespace osu.Game.Overlays.FirstRunSetup
public override bool? AllowTrackAdjustments => false;
}
private partial class UIScaleSlider : OsuSliderBar<float>
private partial class UIScaleSlider : RoundedSliderBar<float>
{
public override LocalisableString TooltipText => base.TooltipText + "x";
}

View File

@ -0,0 +1,63 @@
// 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;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Profile.Header.Components;
namespace osu.Game.Overlays.Profile.Header
{
public partial class BannerHeaderContainer : CompositeDrawable
{
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
[BackgroundDependencyLoader]
private void load()
{
Alpha = 0;
RelativeSizeAxes = Axes.Both;
FillMode = FillMode.Fit;
FillAspectRatio = 1000 / 60f;
}
protected override void LoadComplete()
{
base.LoadComplete();
User.BindValueChanged(u => updateDisplay(u.NewValue?.User), true);
}
private CancellationTokenSource? cancellationTokenSource;
private void updateDisplay(APIUser? user)
{
cancellationTokenSource?.Cancel();
cancellationTokenSource = new CancellationTokenSource();
ClearInternal();
var banner = user?.TournamentBanner;
if (banner != null)
{
Show();
LoadComponentAsync(new DrawableTournamentBanner(banner), AddInternal, cancellationTokenSource.Token);
}
else
{
Hide();
}
}
protected override void Dispose(bool isDisposing)
{
cancellationTokenSource?.Cancel();
base.Dispose(isDisposing);
}
}
}

View File

@ -0,0 +1,46 @@
// 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;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Framework.Localisation;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API;
using osu.Game.Users;
namespace osu.Game.Overlays.Profile.Header.Components
{
[LongRunningLoad]
public partial class DrawableTournamentBanner : OsuClickableContainer
{
private readonly TournamentBanner banner;
public DrawableTournamentBanner(TournamentBanner banner)
{
this.banner = banner;
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(LargeTextureStore textures, OsuGame? game, IAPIProvider api)
{
Child = new Sprite
{
RelativeSizeAxes = Axes.Both,
Texture = textures.Get(banner.Image),
};
Action = () => game?.OpenUrlExternally($@"{api.WebsiteRootUrl}/community/tournaments/{banner.TournamentId}");
}
protected override void LoadComplete()
{
base.LoadComplete();
this.FadeInFromZero(200);
}
public override LocalisableString TooltipText => "view in browser";
}
}

View File

@ -35,6 +35,11 @@ namespace osu.Game.Overlays.Profile.Header.Components
CornerRadius = 8;
TooltipText = group.Name;
if (group.IsProbationary)
{
Alpha = 0.6f;
}
}
[BackgroundDependencyLoader]
@ -47,7 +52,11 @@ namespace osu.Game.Overlays.Profile.Header.Components
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider?.Background6 ?? Colour4.Black
Colour = colourProvider?.Background6 ?? Colour4.Black,
// Normal badges background opacity is 75%, probationary is full opacity as the whole badge gets a bit transparent
// Goal is to match osu-web so this is the most accurate it can be, its a bit scuffed but it is what it is
// Source: https://github.com/ppy/osu-web/blob/master/resources/css/bem/user-group-badge.less#L50
Alpha = group.IsProbationary ? 1 : 0.75f,
},
innerContainer = new FillFlowContainer
{

View File

@ -66,10 +66,12 @@ namespace osu.Game.Overlays.Profile.Header.Components
{
int days = ranked_days - index + 1;
return new UserGraphTooltipContent(
UsersStrings.ShowRankGlobalSimple,
rank.ToLocalisableString("\\##,##0"),
days == 0 ? "now" : $"{"day".ToQuantity(days)} ago");
return new UserGraphTooltipContent
{
Name = UsersStrings.ShowRankGlobalSimple,
Count = rank.ToLocalisableString("\\##,##0"),
Time = days == 0 ? "now" : $"{"day".ToQuantity(days)} ago",
};
}
}
}

View File

@ -12,7 +12,6 @@ using osu.Framework.Graphics.Shapes;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Overlays.Profile.Header.Components;
@ -104,76 +103,69 @@ namespace osu.Game.Overlays.Profile.Header
Colour = Colour4.Black.Opacity(0.25f),
}
},
new OsuContextMenuContainer
new FillFlowContainer
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Child = new FillFlowContainer
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Children = new Drawable[]
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Children = new Drawable[]
new FillFlowContainer
{
new FillFlowContainer
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5, 0),
Children = new Drawable[]
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5, 0),
Children = new Drawable[]
usernameText = new OsuSpriteText
{
usernameText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Regular)
},
supporterTag = new SupporterIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Height = 15,
},
openUserExternally = new ExternalLinkButton
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
groupBadgeFlow = new GroupBadgeFlow
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
}
},
titleText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular),
Margin = new MarginPadding { Bottom = 5 }
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Regular)
},
supporterTag = new SupporterIcon
{
userFlag = new UpdateableFlag
{
Size = new Vector2(28, 20),
ShowPlaceholderOnUnknown = false,
},
userCountryText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular),
Margin = new MarginPadding { Left = 5 },
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
}
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Height = 15,
},
openUserExternally = new ExternalLinkButton
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
groupBadgeFlow = new GroupBadgeFlow
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
}
},
titleText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular),
Margin = new MarginPadding { Bottom = 5 }
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
userFlag = new UpdateableFlag
{
Size = new Vector2(28, 20),
ShowPlaceholderOnUnknown = false,
},
userCountryText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 14f, weight: FontWeight.Regular),
Margin = new MarginPadding { Left = 5 },
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
}
},
}
},
}
},
}
},
}
},

View File

@ -47,6 +47,10 @@ namespace osu.Game.Overlays.Profile
RelativeSizeAxes = Axes.X,
User = { BindTarget = User },
},
new BannerHeaderContainer
{
User = { BindTarget = User },
},
new BadgeHeaderContainer
{
RelativeSizeAxes = Axes.X,

View File

@ -36,21 +36,24 @@ namespace osu.Game.Overlays.Profile
AutoSizeAxes = Axes.Y;
RelativeSizeAxes = Axes.X;
Masking = true;
CornerRadius = 10;
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Offset = new Vector2(0, 1),
Radius = 3,
Colour = Colour4.Black.Opacity(0.25f)
};
InternalChildren = new Drawable[]
{
background = new Box
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 10,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Offset = new Vector2(0, 1),
Radius = 3,
Colour = Colour4.Black.Opacity(0.25f)
},
Child = background = new Box
{
RelativeSizeAxes = Axes.Both,
},
},
new FillFlowContainer
{

View File

@ -27,9 +27,11 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
protected override float GetDataPointHeight(long playCount) => playCount;
protected override UserGraphTooltipContent GetTooltipContent(DateTime date, long playCount) =>
new UserGraphTooltipContent(
tooltipCounterName,
playCount.ToLocalisableString("N0"),
date.ToLocalisableString("MMMM yyyy"));
new UserGraphTooltipContent
{
Name = tooltipCounterName,
Count = playCount.ToLocalisableString("N0"),
Time = date.ToLocalisableString("MMMM yyyy")
};
}
}

View File

@ -298,16 +298,8 @@ namespace osu.Game.Overlays.Profile
public class UserGraphTooltipContent
{
// todo: could use init-only properties on C# 9 which read better than a constructor.
public LocalisableString Name { get; }
public LocalisableString Count { get; }
public LocalisableString Time { get; }
public UserGraphTooltipContent(LocalisableString name, LocalisableString count, LocalisableString time)
{
Name = name;
Count = count;
Time = time;
}
public LocalisableString Name { get; init; }
public LocalisableString Count { get; init; }
public LocalisableString Time { get; init; }
}
}

View File

@ -58,7 +58,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
{
protected override Drawable CreateControl()
{
var sliderBar = (OsuSliderBar<double>)base.CreateControl();
var sliderBar = (RoundedSliderBar<double>)base.CreateControl();
sliderBar.PlaySamplesOnAdjust = false;
return sliderBar;
}

View File

@ -329,7 +329,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
}
}
private partial class UIScaleSlider : OsuSliderBar<float>
private partial class UIScaleSlider : RoundedSliderBar<float>
{
public override LocalisableString TooltipText => base.TooltipText + "x";
}

View File

@ -135,7 +135,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
}
}
public partial class SensitivitySlider : OsuSliderBar<double>
public partial class SensitivitySlider : RoundedSliderBar<double>
{
public override LocalisableString TooltipText => Current.Disabled ? MouseSettingsStrings.EnableHighPrecisionForSensitivityAdjust : $"{base.TooltipText}x";
}

View File

@ -13,7 +13,7 @@ namespace osu.Game.Overlays.Settings.Sections
/// <summary>
/// A slider intended to show a "size" multiplier number, where 1x is 1.0.
/// </summary>
public partial class SizeSlider<T> : OsuSliderBar<T>
public partial class SizeSlider<T> : RoundedSliderBar<T>
where T : struct, IEquatable<T>, IComparable<T>, IConvertible, IFormattable
{
public override LocalisableString TooltipText => Current.Value.ToString(@"0.##x", NumberFormatInfo.CurrentInfo);

View File

@ -10,14 +10,14 @@ using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings
{
public partial class SettingsSlider<T> : SettingsSlider<T, OsuSliderBar<T>>
public partial class SettingsSlider<T> : SettingsSlider<T, RoundedSliderBar<T>>
where T : struct, IEquatable<T>, IComparable<T>, IConvertible
{
}
public partial class SettingsSlider<TValue, TSlider> : SettingsItem<TValue>
where TValue : struct, IEquatable<TValue>, IComparable<TValue>, IConvertible
where TSlider : OsuSliderBar<TValue>, new()
where TSlider : RoundedSliderBar<TValue>, new()
{
protected override Drawable CreateControl() => new TSlider
{

View File

@ -40,7 +40,7 @@ namespace osu.Game.Overlays.SkinEditor
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(2)
Spacing = new Vector2(EditorSidebar.PADDING)
};
reloadComponents();
@ -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

@ -24,6 +24,7 @@ using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Overlays.OSD;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Edit.Components.Menus;
using osu.Game.Skinning;
@ -31,7 +32,7 @@ using osu.Game.Skinning;
namespace osu.Game.Overlays.SkinEditor
{
[Cached(typeof(SkinEditor))]
public partial class SkinEditor : VisibilityContainer, ICanAcceptFiles, IKeyBindingHandler<PlatformAction>
public partial class SkinEditor : VisibilityContainer, ICanAcceptFiles, IKeyBindingHandler<PlatformAction>, IEditorChangeHandler
{
public const double TRANSITION_DURATION = 300;
@ -72,6 +73,11 @@ namespace osu.Game.Overlays.SkinEditor
private EditorSidebar componentsSidebar = null!;
private EditorSidebar settingsSidebar = null!;
private SkinEditorChangeHandler? changeHandler;
private EditorMenuItem undoMenuItem = null!;
private EditorMenuItem redoMenuItem = null!;
[Resolved]
private OnScreenDisplay? onScreenDisplay { get; set; }
@ -125,12 +131,20 @@ namespace osu.Game.Overlays.SkinEditor
{
Items = new[]
{
new EditorMenuItem(Resources.Localisation.Web.CommonStrings.ButtonsSave, MenuItemType.Standard, Save),
new EditorMenuItem(Resources.Localisation.Web.CommonStrings.ButtonsSave, MenuItemType.Standard, () => Save()),
new EditorMenuItem(CommonStrings.RevertToDefault, MenuItemType.Destructive, revert),
new EditorMenuItemSpacer(),
new EditorMenuItem(CommonStrings.Exit, MenuItemType.Standard, () => skinEditorOverlay?.Hide()),
},
},
new MenuItem(CommonStrings.MenuBarEdit)
{
Items = new[]
{
undoMenuItem = new EditorMenuItem(CommonStrings.Undo, MenuItemType.Standard, Undo),
redoMenuItem = new EditorMenuItem(CommonStrings.Redo, MenuItemType.Standard, Redo),
}
},
}
},
headerText = new OsuTextFlowContainer
@ -210,6 +224,14 @@ namespace osu.Game.Overlays.SkinEditor
{
switch (e.Action)
{
case PlatformAction.Undo:
Undo();
return true;
case PlatformAction.Redo:
Redo();
return true;
case PlatformAction.Save:
if (e.Repeat)
return false;
@ -229,6 +251,8 @@ namespace osu.Game.Overlays.SkinEditor
{
this.targetScreen = targetScreen;
changeHandler?.Dispose();
SelectedComponents.Clear();
// Immediately clear the previous blueprint container to ensure it doesn't try to interact with the old target.
@ -241,6 +265,10 @@ namespace osu.Game.Overlays.SkinEditor
{
Debug.Assert(content != null);
changeHandler = new SkinEditorChangeHandler(targetScreen);
changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
content.Child = new SkinBlueprintContainer(targetScreen);
componentsSidebar.Child = new SkinComponentToolbox(getFirstTarget() as CompositeDrawable)
@ -256,13 +284,13 @@ namespace osu.Game.Overlays.SkinEditor
headerText.AddParagraph(SkinEditorStrings.SkinEditor, cp => cp.Font = OsuFont.Default.With(size: 16));
headerText.NewParagraph();
headerText.AddText("Currently editing ", cp =>
headerText.AddText(SkinEditorStrings.CurrentlyEditing, cp =>
{
cp.Font = OsuFont.Default.With(size: 12);
cp.Colour = colours.Yellow;
});
headerText.AddText($"{currentSkin.Value.SkinInfo}", cp =>
headerText.AddText($" {currentSkin.Value.SkinInfo}", cp =>
{
cp.Font = OsuFont.Default.With(size: 12, weight: FontWeight.Bold);
cp.Colour = colours.Yellow;
@ -333,7 +361,11 @@ namespace osu.Game.Overlays.SkinEditor
}
}
public void Save()
protected void Undo() => changeHandler?.RestoreState(-1);
protected void Redo() => changeHandler?.RestoreState(1);
public void Save(bool userTriggered = true)
{
if (!hasBegunMutating)
return;
@ -343,8 +375,9 @@ namespace osu.Game.Overlays.SkinEditor
foreach (var t in targetContainers)
currentSkin.Value.UpdateDrawableTarget(t);
skins.Save(skins.CurrentSkin.Value);
onScreenDisplay?.Display(new SkinEditorToast(ToastStrings.SkinSaved, currentSkin.Value.SkinInfo.ToString() ?? "Unknown"));
// In the case the save was user triggered, always show the save message to make them feel confident.
if (skins.Save(skins.CurrentSkin.Value) || userTriggered)
onScreenDisplay?.Display(new SkinEditorToast(ToastStrings.SkinSaved, currentSkin.Value.SkinInfo.ToString() ?? "Unknown"));
}
protected override bool OnHover(HoverEvent e) => true;
@ -435,5 +468,27 @@ namespace osu.Game.Overlays.SkinEditor
{
}
}
#region Delegation of IEditorChangeHandler
public event Action? OnStateChange
{
add => throw new NotImplementedException();
remove => throw new NotImplementedException();
}
private IEditorChangeHandler? beginChangeHandler;
public void BeginChange()
{
// Change handler may change between begin and end, which can cause unbalanced operations.
// Let's track the one that was used when beginning the change so we can call EndChange on it specifically.
(beginChangeHandler = changeHandler)?.BeginChange();
}
public void EndChange() => beginChangeHandler?.EndChange();
public void SaveState() => changeHandler?.SaveState();
#endregion
}
}

View File

@ -0,0 +1,78 @@
// 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 System.IO;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Extensions;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
namespace osu.Game.Overlays.SkinEditor
{
public partial class SkinEditorChangeHandler : EditorChangeHandler
{
private readonly ISkinnableTarget? firstTarget;
// ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable
private readonly BindableList<ISkinnableDrawable>? components;
public SkinEditorChangeHandler(Drawable targetScreen)
{
// To keep things simple, we are currently only handling the current target screen for undo / redo.
// In the future we'll want this to cover all changes, even to skin's `InstantiationInfo`.
// We'll also need to consider cases where multiple targets are on screen at the same time.
firstTarget = targetScreen.ChildrenOfType<ISkinnableTarget>().FirstOrDefault();
if (firstTarget == null)
return;
components = new BindableList<ISkinnableDrawable> { BindTarget = firstTarget.Components };
components.BindCollectionChanged((_, _) => SaveState());
}
protected override void WriteCurrentStateToStream(MemoryStream stream)
{
if (firstTarget == null)
return;
var skinnableInfos = firstTarget.CreateSkinnableInfo().ToArray();
string json = JsonConvert.SerializeObject(skinnableInfos, new JsonSerializerSettings { Formatting = Formatting.Indented });
stream.Write(Encoding.UTF8.GetBytes(json));
}
protected override void ApplyStateChange(byte[] previousState, byte[] newState)
{
if (firstTarget == null)
return;
var deserializedContent = JsonConvert.DeserializeObject<IEnumerable<SkinnableInfo>>(Encoding.UTF8.GetString(newState));
if (deserializedContent == null)
return;
SkinnableInfo[] skinnableInfo = deserializedContent.ToArray();
Drawable[] targetComponents = firstTarget.Components.OfType<Drawable>().ToArray();
if (!skinnableInfo.Select(s => s.Type).SequenceEqual(targetComponents.Select(d => d.GetType())))
{
// Perform a naive full reload for now.
firstTarget.Reload(skinnableInfo);
}
else
{
int i = 0;
foreach (var drawable in targetComponents)
drawable.ApplySkinnableInfo(skinnableInfo[i++]);
}
}
}
}

View File

@ -147,7 +147,7 @@ namespace osu.Game.Overlays.SkinEditor
if (skinEditor == null) return;
skinEditor.Save();
skinEditor.Save(userTriggered: false);
// ensure the toolbar is re-hidden even if a new screen decides to try and show it.
updateComponentVisibility();

View File

@ -241,6 +241,8 @@ namespace osu.Game.Overlays.SkinEditor
private void applyOrigins(Anchor origin)
{
OnOperationBegan();
foreach (var item in SelectedItems)
{
var drawable = (Drawable)item;
@ -255,6 +257,8 @@ namespace osu.Game.Overlays.SkinEditor
ApplyClosestAnchor(drawable);
}
OnOperationEnded();
}
/// <summary>
@ -266,6 +270,8 @@ namespace osu.Game.Overlays.SkinEditor
private void applyFixedAnchors(Anchor anchor)
{
OnOperationBegan();
foreach (var item in SelectedItems)
{
var drawable = (Drawable)item;
@ -273,15 +279,21 @@ namespace osu.Game.Overlays.SkinEditor
item.UsesFixedAnchor = true;
applyAnchor(drawable, anchor);
}
OnOperationEnded();
}
private void applyClosestAnchors()
{
OnOperationBegan();
foreach (var item in SelectedItems)
{
item.UsesFixedAnchor = false;
ApplyClosestAnchor((Drawable)item);
}
OnOperationEnded();
}
private static Anchor getClosestAnchor(Drawable drawable)

View File

@ -2,10 +2,13 @@
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osu.Game.Localisation;
using osu.Game.Overlays.Settings;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components;
using osuTK;
@ -13,19 +16,41 @@ namespace osu.Game.Overlays.SkinEditor
{
internal partial class SkinSettingsToolbox : EditorSidebarSection
{
[Resolved]
private IEditorChangeHandler? changeHandler { get; set; }
protected override Container<Drawable> Content { get; }
private readonly Drawable component;
public SkinSettingsToolbox(Drawable component)
: base(SkinEditorStrings.Settings(component.GetType().Name))
{
this.component = component;
base.Content.Add(Content = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
Children = component.CreateSettingsControls().ToArray()
});
}
[BackgroundDependencyLoader]
private void load()
{
var controls = component.CreateSettingsControls().ToArray();
Content.AddRange(controls);
// track any changes to update undo states.
foreach (var c in controls.OfType<ISettingsItem>())
{
// TODO: SettingChanged is called too often for cases like SettingsTextBox and SettingsSlider.
// We will want to expose a SettingCommitted or similar to make this work better.
c.SettingChanged += () => changeHandler?.SaveState();
}
}
}
}

View File

@ -14,6 +14,7 @@ using osu.Framework.Input.Events;
using osu.Game.Extensions;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
@ -100,17 +101,22 @@ namespace osu.Game.Overlays
Origin = Anchor.TopCentre,
};
Add(sectionsContainer = new ProfileSectionsContainer
Add(new OsuContextMenuContainer
{
ExpandableHeader = Header,
FixedHeader = tabs,
HeaderBackground = new Box
RelativeSizeAxes = Axes.Both,
Child = sectionsContainer = new ProfileSectionsContainer
{
// this is only visible as the ProfileTabControl background
Colour = ColourProvider.Background5,
RelativeSizeAxes = Axes.Both
},
ExpandableHeader = Header,
FixedHeader = tabs,
HeaderBackground = new Box
{
// this is only visible as the ProfileTabControl background
Colour = ColourProvider.Background5,
RelativeSizeAxes = Axes.Both
},
}
});
sectionsContainer.SelectedSection.ValueChanged += section =>
{
if (lastSection != section.NewValue)

View File

@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Edit
/// </summary>
protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent?.ReceivePositionalInputAt(screenSpacePos) ?? false;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent?.ReceivePositionalInputAt(screenSpacePos) == true;
protected override bool Handle(UIEvent e)
{

View File

@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Mods
{
InternalChildren = new Drawable[]
{
new OsuSliderBar<float>
new RoundedSliderBar<float>
{
RelativeSizeAxes = Axes.X,
Current = currentNumber,

View File

@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Mods
}
}
public partial class PercentSlider : OsuSliderBar<double>
public partial class PercentSlider : RoundedSliderBar<double>
{
public PercentSlider()
{

View File

@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Mods
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
}
public partial class MuteComboSlider : OsuSliderBar<int>
public partial class MuteComboSlider : RoundedSliderBar<int>
{
public override LocalisableString TooltipText => Current.Value == 0 ? "always muted" : base.TooltipText;
}

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