1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-21 23:47:21 +08:00

Merge branch 'master' into clamp-scale2

This commit is contained in:
Bartłomiej Dach 2024-10-11 15:13:54 +02:00
commit 5db1f05953
No known key found for this signature in database
115 changed files with 2697 additions and 743 deletions

View File

@ -133,10 +133,7 @@ jobs:
dotnet-version: "8.0.x" dotnet-version: "8.0.x"
- name: Install .NET Workloads - name: Install .NET Workloads
run: dotnet workload install maui-ios run: dotnet workload install ios --from-rollback-file https://raw.githubusercontent.com/ppy/osu-framework/refs/heads/master/workloads.json
- name: Select Xcode 16
run: sudo xcode-select -s /Applications/Xcode_16.app/Contents/Developer
- name: Build - name: Build
run: dotnet build -c Debug osu.iOS run: dotnet build -c Debug osu.iOS

View File

@ -104,6 +104,25 @@ env:
EXECUTION_ID: execution-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }} EXECUTION_ID: execution-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}
jobs: jobs:
master-environment:
name: Save master environment
runs-on: ubuntu-latest
outputs:
HEAD: ${{ steps.get-head.outputs.HEAD }}
steps:
- name: Checkout osu
uses: actions/checkout@v4
with:
ref: master
sparse-checkout: |
README.md
- name: Get HEAD ref
id: get-head
run: |
ref=$(git log -1 --format='%H')
echo "HEAD=https://github.com/${{ github.repository }}/commit/${ref}" >> "${GITHUB_OUTPUT}"
check-permissions: check-permissions:
name: Check permissions name: Check permissions
runs-on: ubuntu-latest runs-on: ubuntu-latest
@ -121,7 +140,7 @@ jobs:
create-comment: create-comment:
name: Create PR comment name: Create PR comment
needs: check-permissions needs: [ master-environment, check-permissions ]
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }} if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
steps: steps:
@ -158,7 +177,7 @@ jobs:
environment: environment:
name: Setup environment name: Setup environment
needs: directory needs: [ master-environment, directory ]
runs-on: self-hosted runs-on: self-hosted
env: env:
VARS_JSON: ${{ toJSON(vars) }} VARS_JSON: ${{ toJSON(vars) }}
@ -182,6 +201,10 @@ jobs:
fi fi
done done
- name: Add master environment
run: |
sed -i "s;^OSU_A=.*$;OSU_A=${{ needs.master-environment.outputs.HEAD }};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
- name: Add pull-request environment - name: Add pull-request environment
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }} if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
run: | run: |
@ -361,8 +384,7 @@ jobs:
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0 uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with: with:
comment_tag: ${{ env.EXECUTION_ID }} comment_tag: ${{ env.EXECUTION_ID }}
mode: upsert mode: recreate
create_if_not_exists: false
message: | message: |
Target: ${{ needs.generator.outputs.TARGET }} Target: ${{ needs.generator.outputs.TARGET }}
Spreadsheet: ${{ needs.generator.outputs.SPREADSHEET_LINK }} Spreadsheet: ${{ needs.generator.outputs.SPREADSHEET_LINK }}
@ -372,8 +394,7 @@ jobs:
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0 uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with: with:
comment_tag: ${{ env.EXECUTION_ID }} comment_tag: ${{ env.EXECUTION_ID }}
mode: upsert mode: recreate
create_if_not_exists: false
message: | message: |
Difficulty calculation failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} Difficulty calculation failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.927.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2024.1009.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged. <!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -6,28 +6,29 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game; using osu.Game;
using osu.Game.Screens.Play;
namespace osu.Android namespace osu.Android
{ {
public partial class GameplayScreenRotationLocker : Component public partial class GameplayScreenRotationLocker : Component
{ {
private Bindable<bool> localUserPlaying = null!; private IBindable<LocalUserPlayingState> localUserPlaying = null!;
[Resolved] [Resolved]
private OsuGameActivity gameActivity { get; set; } = null!; private OsuGameActivity gameActivity { get; set; } = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuGame game) private void load(ILocalUserPlayInfo localUserPlayInfo)
{ {
localUserPlaying = game.LocalUserPlaying.GetBoundCopy(); localUserPlaying = localUserPlayInfo.PlayingState.GetBoundCopy();
localUserPlaying.BindValueChanged(updateLock, true); localUserPlaying.BindValueChanged(updateLock, true);
} }
private void updateLock(ValueChangedEvent<bool> userPlaying) private void updateLock(ValueChangedEvent<LocalUserPlayingState> userPlaying)
{ {
gameActivity.RunOnUiThread(() => gameActivity.RunOnUiThread(() =>
{ {
gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : gameActivity.DefaultOrientation; gameActivity.RequestedOrientation = userPlaying.NewValue != LocalUserPlayingState.NotPlaying ? ScreenOrientation.Locked : gameActivity.DefaultOrientation;
}); });
} }
} }

View File

@ -279,10 +279,12 @@ namespace osu.Desktop
// As above, discord decides that *non-empty* strings shorter than 2 characters cannot possibly be valid input, because... reasons? // As above, discord decides that *non-empty* strings shorter than 2 characters cannot possibly be valid input, because... reasons?
// And yes, that is two *characters*, or *codepoints*, not *bytes* as further down below (as determined by empirical testing). // And yes, that is two *characters*, or *codepoints*, not *bytes* as further down below (as determined by empirical testing).
// That seems very questionable, and isn't even documented anywhere. So to *make it* accept such valid input, // Also, spaces don't count. Because reasons, clearly.
// just tack on enough of U+200B ZERO WIDTH SPACEs at the end. // That all seems very questionable, and isn't even documented anywhere. So to *make it* accept such valid input,
if (str.Length < 2) // just tack on enough of U+200B ZERO WIDTH SPACEs at the end. After making sure to trim whitespace.
return str.PadRight(2, '\u200B'); string trimmed = str.Trim();
if (trimmed.Length < 2)
return trimmed.PadRight(2, '\u200B');
if (Encoding.UTF8.GetByteCount(str) <= 128) if (Encoding.UTF8.GetByteCount(str) <= 128)
return str; return str;

View File

@ -25,6 +25,8 @@ namespace osu.Desktop.Updater
[Resolved] [Resolved]
private ILocalUserPlayInfo? localUserInfo { get; set; } private ILocalUserPlayInfo? localUserInfo { get; set; }
private bool isInGameplay => localUserInfo?.PlayingState.Value != LocalUserPlayingState.NotPlaying;
private UpdateInfo? pendingUpdate; private UpdateInfo? pendingUpdate;
public VelopackUpdateManager() public VelopackUpdateManager()
@ -43,7 +45,7 @@ namespace osu.Desktop.Updater
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false); protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
private async Task<bool> checkForUpdateAsync(UpdateProgressNotification? notification = null) private async Task<bool> checkForUpdateAsync()
{ {
// whether to check again in 30 minutes. generally only if there's an error or no update was found (yet). // whether to check again in 30 minutes. generally only if there's an error or no update was found (yet).
bool scheduleRecheck = false; bool scheduleRecheck = false;
@ -51,10 +53,10 @@ namespace osu.Desktop.Updater
try try
{ {
// Avoid any kind of update checking while gameplay is running. // Avoid any kind of update checking while gameplay is running.
if (localUserInfo?.IsPlaying.Value == true) if (isInGameplay)
{ {
scheduleRecheck = true; scheduleRecheck = true;
return false; return true;
} }
// TODO: we should probably be checking if there's a more recent update, rather than shortcutting here. // TODO: we should probably be checking if there's a more recent update, rather than shortcutting here.
@ -84,27 +86,22 @@ namespace osu.Desktop.Updater
} }
// An update is found, let's notify the user and start downloading it. // An update is found, let's notify the user and start downloading it.
if (notification == null) UpdateProgressNotification notification = new UpdateProgressNotification
{ {
notification = new UpdateProgressNotification CompletionClickAction = () =>
{ {
CompletionClickAction = () => Task.Run(restartToApplyUpdate);
{ return true;
Task.Run(restartToApplyUpdate); },
return true; };
},
};
Schedule(() => notificationOverlay.Post(notification));
}
runOutsideOfGameplay(() => notificationOverlay.Post(notification));
notification.StartDownload(); notification.StartDownload();
try try
{ {
await updateManager.DownloadUpdatesAsync(pendingUpdate, p => notification.Progress = p / 100f).ConfigureAwait(false); await updateManager.DownloadUpdatesAsync(pendingUpdate, p => notification.Progress = p / 100f).ConfigureAwait(false);
runOutsideOfGameplay(() => notification.State = ProgressNotificationState.Completed);
notification.State = ProgressNotificationState.Completed;
} }
catch (Exception e) catch (Exception e)
{ {
@ -131,6 +128,17 @@ namespace osu.Desktop.Updater
return true; return true;
} }
private void runOutsideOfGameplay(Action action)
{
if (isInGameplay)
{
Scheduler.AddDelayed(() => runOutsideOfGameplay(action), 1000);
return;
}
action();
}
private async Task restartToApplyUpdate() private async Task restartToApplyUpdate()
{ {
await updateManager.WaitExitThenApplyUpdatesAsync(pendingUpdate?.TargetFullRelease).ConfigureAwait(false); await updateManager.WaitExitThenApplyUpdatesAsync(pendingUpdate?.TargetFullRelease).ConfigureAwait(false);

View File

@ -13,7 +13,7 @@ namespace osu.Desktop.Windows
public partial class GameplayWinKeyBlocker : Component public partial class GameplayWinKeyBlocker : Component
{ {
private Bindable<bool> disableWinKey = null!; private Bindable<bool> disableWinKey = null!;
private IBindable<bool> localUserPlaying = null!; private IBindable<LocalUserPlayingState> localUserPlaying = null!;
private IBindable<bool> isActive = null!; private IBindable<bool> isActive = null!;
[Resolved] [Resolved]
@ -22,7 +22,7 @@ namespace osu.Desktop.Windows
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ILocalUserPlayInfo localUserInfo, OsuConfigManager config) private void load(ILocalUserPlayInfo localUserInfo, OsuConfigManager config)
{ {
localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy(); localUserPlaying = localUserInfo.PlayingState.GetBoundCopy();
localUserPlaying.BindValueChanged(_ => updateBlocking()); localUserPlaying.BindValueChanged(_ => updateBlocking());
isActive = host.IsActive.GetBoundCopy(); isActive = host.IsActive.GetBoundCopy();
@ -34,7 +34,7 @@ namespace osu.Desktop.Windows
private void updateBlocking() private void updateBlocking()
{ {
bool shouldDisable = isActive.Value && disableWinKey.Value && localUserPlaying.Value; bool shouldDisable = isActive.Value && disableWinKey.Value && localUserPlaying.Value == LocalUserPlayingState.Playing;
if (shouldDisable) if (shouldDisable)
host.InputThread.Scheduler.Add(WindowsKey.Disable); host.InputThread.Scheduler.Add(WindowsKey.Disable);

View File

@ -24,7 +24,7 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="System.IO.Packaging" Version="8.0.0" /> <PackageReference Include="System.IO.Packaging" Version="8.0.1" />
<PackageReference Include="DiscordRichPresence" Version="1.2.1.24" /> <PackageReference Include="DiscordRichPresence" Version="1.2.1.24" />
<PackageReference Include="Velopack" Version="0.0.630-g9c52e40" /> <PackageReference Include="Velopack" Version="0.0.630-g9c52e40" />
</ItemGroup> </ItemGroup>

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Localisation; using osu.Framework.Localisation;
@ -31,6 +32,7 @@ using osu.Game.Scoring;
using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Ranking.Statistics; using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Catch namespace osu.Game.Rulesets.Catch
{ {
@ -223,10 +225,28 @@ namespace osu.Game.Rulesets.Catch
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this); public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
public override IEnumerable<SetupSection> CreateEditorSetupSections() => public override IEnumerable<Drawable> CreateEditorSetupSections() =>
[ [
new MetadataSection(),
new DifficultySection(), new DifficultySection(),
new ColoursSection(), new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(SetupScreen.SPACING),
Children = new Drawable[]
{
new ResourcesSection
{
RelativeSizeAxes = Axes.X,
},
new ColoursSection
{
RelativeSizeAxes = Axes.X,
}
}
},
new DesignSection(),
]; ];
public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier(); public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier();

View File

@ -20,10 +20,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
[Test] [Test]
public void TestKeyCountChange() public void TestKeyCountChange()
{ {
LabelledSliderBar<float> keyCount = null!; FormSliderBar<float> keyCount = null!;
AddStep("go to setup screen", () => InputManager.Key(Key.F4)); AddStep("go to setup screen", () => InputManager.Key(Key.F4));
AddUntilStep("retrieve key count slider", () => keyCount = Editor.ChildrenOfType<SetupScreen>().Single().ChildrenOfType<LabelledSliderBar<float>>().First(), () => Is.Not.Null); AddUntilStep("retrieve key count slider", () => keyCount = Editor.ChildrenOfType<SetupScreen>().Single().ChildrenOfType<FormSliderBar<float>>().First(), () => Is.Not.Null);
AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5)); AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
AddStep("change key count to 8", () => AddStep("change key count to 8", () =>
{ {

View File

@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private readonly bool isForCurrentRuleset; private readonly bool isForCurrentRuleset;
private readonly double originalOverallDifficulty; private readonly double originalOverallDifficulty;
public override int Version => 20230817; public override int Version => 20241007;
public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) public ManiaDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap) : base(ruleset, beatmap)

View File

@ -19,12 +19,12 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
{ {
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader; public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
private LabelledSliderBar<float> keyCountSlider { get; set; } = null!; private FormSliderBar<float> keyCountSlider { get; set; } = null!;
private LabelledSwitchButton specialStyle { get; set; } = null!; private FormCheckBox specialStyle { get; set; } = null!;
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!; private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!; private FormSliderBar<float> overallDifficultySlider { get; set; } = null!;
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!; private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!; private FormSliderBar<double> tickRateSlider { get; set; } = null!;
[Resolved] [Resolved]
private Editor? editor { get; set; } private Editor? editor { get; set; }
@ -37,77 +37,81 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
keyCountSlider = new LabelledSliderBar<float> keyCountSlider = new FormSliderBar<float>
{ {
Label = BeatmapsetsStrings.ShowStatsCsMania, Caption = BeatmapsetsStrings.ShowStatsCsMania,
FixedLabelWidth = LABEL_WIDTH, HintText = "The number of columns in the beatmap",
Description = "The number of columns in the beatmap",
Current = new BindableFloat(Beatmap.Difficulty.CircleSize) Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
{ {
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0, MinValue = 0,
MaxValue = 10, MaxValue = 10,
Precision = 1, Precision = 1,
} },
TransferValueOnCommit = true,
TabbableContentContainer = this,
}, },
specialStyle = new LabelledSwitchButton specialStyle = new FormCheckBox
{ {
Label = "Use special (N+1) style", Caption = "Use special (N+1) style",
FixedLabelWidth = LABEL_WIDTH, HintText = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.",
Description = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.",
Current = { Value = Beatmap.BeatmapInfo.SpecialStyle } Current = { Value = Beatmap.BeatmapInfo.SpecialStyle }
}, },
healthDrainSlider = new LabelledSliderBar<float> healthDrainSlider = new FormSliderBar<float>
{ {
Label = BeatmapsetsStrings.ShowStatsDrain, Caption = BeatmapsetsStrings.ShowStatsDrain,
FixedLabelWidth = LABEL_WIDTH, HintText = EditorSetupStrings.DrainRateDescription,
Description = EditorSetupStrings.DrainRateDescription,
Current = new BindableFloat(Beatmap.Difficulty.DrainRate) Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
{ {
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0, MinValue = 0,
MaxValue = 10, MaxValue = 10,
Precision = 0.1f, Precision = 0.1f,
} },
TransferValueOnCommit = true,
TabbableContentContainer = this,
}, },
overallDifficultySlider = new LabelledSliderBar<float> overallDifficultySlider = new FormSliderBar<float>
{ {
Label = BeatmapsetsStrings.ShowStatsAccuracy, Caption = BeatmapsetsStrings.ShowStatsAccuracy,
FixedLabelWidth = LABEL_WIDTH, HintText = EditorSetupStrings.OverallDifficultyDescription,
Description = EditorSetupStrings.OverallDifficultyDescription,
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty) Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
{ {
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0, MinValue = 0,
MaxValue = 10, MaxValue = 10,
Precision = 0.1f, Precision = 0.1f,
} },
TransferValueOnCommit = true,
TabbableContentContainer = this,
}, },
baseVelocitySlider = new LabelledSliderBar<double> baseVelocitySlider = new FormSliderBar<double>
{ {
Label = EditorSetupStrings.BaseVelocity, Caption = EditorSetupStrings.BaseVelocity,
FixedLabelWidth = LABEL_WIDTH, HintText = EditorSetupStrings.BaseVelocityDescription,
Description = EditorSetupStrings.BaseVelocityDescription,
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier) Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
{ {
Default = 1.4, Default = 1.4,
MinValue = 0.4, MinValue = 0.4,
MaxValue = 3.6, MaxValue = 3.6,
Precision = 0.01f, Precision = 0.01f,
} },
TransferValueOnCommit = true,
TabbableContentContainer = this,
}, },
tickRateSlider = new LabelledSliderBar<double> tickRateSlider = new FormSliderBar<double>
{ {
Label = EditorSetupStrings.TickRate, Caption = EditorSetupStrings.TickRate,
FixedLabelWidth = LABEL_WIDTH, HintText = EditorSetupStrings.TickRateDescription,
Description = EditorSetupStrings.TickRateDescription,
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate) Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
{ {
Default = 1, Default = 1,
MinValue = 1, MinValue = 1,
MaxValue = 4, MaxValue = 4,
Precision = 1, Precision = 1,
} },
TransferValueOnCommit = true,
TabbableContentContainer = this,
}, },
}; };

View File

@ -419,9 +419,12 @@ namespace osu.Game.Rulesets.Mania
return new ManiaFilterCriteria(); return new ManiaFilterCriteria();
} }
public override IEnumerable<SetupSection> CreateEditorSetupSections() => public override IEnumerable<Drawable> CreateEditorSetupSections() =>
[ [
new MetadataSection(),
new ManiaDifficultySection(), new ManiaDifficultySection(),
new ResourcesSection(),
new DesignSection(),
]; ];
public int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList<Mod>? mods = null) public int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList<Mod>? mods = null)

View File

@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
[TestCase(6.710442985146793d, 239, "diffcalc-test")] [TestCase(6.7171144000821119d, 239, "diffcalc-test")]
[TestCase(1.4386882251130073d, 54, "zero-length-sliders")] [TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
[TestCase(0.42506480230838789d, 4, "very-fast-slider")] [TestCase(0.42630400627180914d, 4, "very-fast-slider")]
[TestCase(0.14102693012101306d, 2, "nan-slider")] [TestCase(0.14143808967817237d, 2, "nan-slider")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name) public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name); => base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(8.9742952703071666d, 239, "diffcalc-test")] [TestCase(8.9825709931204205d, 239, "diffcalc-test")]
[TestCase(1.743180218215227d, 54, "zero-length-sliders")] [TestCase(1.7550169162648608d, 54, "zero-length-sliders")]
[TestCase(0.55071082800473514d, 4, "very-fast-slider")] [TestCase(0.55231632896800109d, 4, "very-fast-slider")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
[TestCase(6.710442985146793d, 239, "diffcalc-test")] [TestCase(6.7171144000821119d, 239, "diffcalc-test")]
[TestCase(1.4386882251130073d, 54, "zero-length-sliders")] [TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
[TestCase(0.42506480230838789d, 4, "very-fast-slider")] [TestCase(0.42630400627180914d, 4, "very-fast-slider")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic()); => Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());

View File

@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
@ -10,8 +12,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
{ {
public static class RhythmEvaluator public static class RhythmEvaluator
{ {
private const int history_time_max = 5000; // 5 seconds of calculatingRhythmBonus max. private const int history_time_max = 5 * 1000; // 5 seconds
private const double rhythm_multiplier = 0.75; private const int history_objects_max = 32;
private const double rhythm_overall_multiplier = 0.95;
private const double rhythm_ratio_multiplier = 12.0;
/// <summary> /// <summary>
/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>. /// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>.
@ -21,15 +25,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
if (current.BaseObject is Spinner) if (current.BaseObject is Spinner)
return 0; return 0;
int previousIslandSize = 0;
double rhythmComplexitySum = 0; double rhythmComplexitySum = 0;
int islandSize = 1;
double deltaDifferenceEpsilon = ((OsuDifficultyHitObject)current).HitWindowGreat * 0.3;
var island = new Island(deltaDifferenceEpsilon);
var previousIsland = new Island(deltaDifferenceEpsilon);
// we can't use dictionary here because we need to compare island with a tolerance
// which is impossible to pass into the hash comparer
var islandCounts = new List<(Island Island, int Count)>();
double startRatio = 0; // store the ratio of the current start of an island to buff for tighter rhythms double startRatio = 0; // store the ratio of the current start of an island to buff for tighter rhythms
bool firstDeltaSwitch = false; bool firstDeltaSwitch = false;
int historicalNoteCount = Math.Min(current.Index, 32); int historicalNoteCount = Math.Min(current.Index, history_objects_max);
int rhythmStart = 0; int rhythmStart = 0;
@ -39,74 +50,177 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(rhythmStart); OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(rhythmStart);
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)current.Previous(rhythmStart + 1); OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)current.Previous(rhythmStart + 1);
// we go from the furthest object back to the current one
for (int i = rhythmStart; i > 0; i--) for (int i = rhythmStart; i > 0; i--)
{ {
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1); OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1);
double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now // scales note 0 to 1 from history to now
double timeDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max;
double noteDecay = (double)(historicalNoteCount - i) / historicalNoteCount;
currHistoricalDecay = Math.Min((double)(historicalNoteCount - i) / historicalNoteCount, currHistoricalDecay); // either we're limited by time or limited by object count. double currHistoricalDecay = Math.Min(noteDecay, timeDecay); // either we're limited by time or limited by object count.
double currDelta = currObj.StrainTime; double currDelta = currObj.StrainTime;
double prevDelta = prevObj.StrainTime; double prevDelta = prevObj.StrainTime;
double lastDelta = lastObj.StrainTime; double lastDelta = lastObj.StrainTime;
double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses.
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - currObj.HitWindowGreat * 0.3) / (currObj.HitWindowGreat * 0.3)); // calculate how much current delta difference deserves a rhythm bonus
// this function is meant to reduce rhythm bonus for deltas that are multiples of each other (i.e 100 and 200)
double deltaDifferenceRatio = Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta);
double currRatio = 1.0 + rhythm_ratio_multiplier * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / deltaDifferenceRatio), 2));
windowPenalty = Math.Min(1, windowPenalty); // reduce ratio bonus if delta difference is too big
double fraction = Math.Max(prevDelta / currDelta, currDelta / prevDelta);
double fractionMultiplier = Math.Clamp(2.0 - fraction / 8.0, 0.0, 1.0);
double effectiveRatio = windowPenalty * currRatio; double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - deltaDifferenceEpsilon) / deltaDifferenceEpsilon);
double effectiveRatio = windowPenalty * currRatio * fractionMultiplier;
if (firstDeltaSwitch) if (firstDeltaSwitch)
{ {
if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta)) if (Math.Abs(prevDelta - currDelta) < deltaDifferenceEpsilon)
{ {
if (islandSize < 7) // island is still progressing
islandSize++; // island is still progressing, count size. island.AddDelta((int)currDelta);
} }
else else
{ {
if (currObj.BaseObject is Slider) // bpm change is into slider, this is easy acc window // bpm change is into slider, this is easy acc window
if (currObj.BaseObject is Slider)
effectiveRatio *= 0.125; effectiveRatio *= 0.125;
if (prevObj.BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle // bpm change was from a slider, this is easier typically than circle -> circle
effectiveRatio *= 0.25; // unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders
if (prevObj.BaseObject is Slider)
effectiveRatio *= 0.3;
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet) // repeated island polarity (2 -> 4, 3 -> 5)
effectiveRatio *= 0.25; if (island.IsSimilarPolarity(previousIsland))
effectiveRatio *= 0.5;
if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
effectiveRatio *= 0.50; if (lastDelta > prevDelta + deltaDifferenceEpsilon && prevDelta > currDelta + deltaDifferenceEpsilon)
if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
effectiveRatio *= 0.125; effectiveRatio *= 0.125;
rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2; // repeated island size (ex: triplet -> triplet)
// TODO: remove this nerf since its staying here only for balancing purposes because of the flawed ratio calculation
if (previousIsland.DeltaCount == island.DeltaCount)
effectiveRatio *= 0.5;
var islandCount = islandCounts.FirstOrDefault(x => x.Island.Equals(island));
if (islandCount != default)
{
int countIndex = islandCounts.IndexOf(islandCount);
// only add island to island counts if they're going one after another
if (previousIsland.Equals(island))
islandCount.Count++;
// repeated island (ex: triplet -> triplet)
double power = logistic(island.Delta, 2.75, 0.24, 14);
effectiveRatio *= Math.Min(3.0 / islandCount.Count, Math.Pow(1.0 / islandCount.Count, power));
islandCounts[countIndex] = (islandCount.Island, islandCount.Count);
}
else
{
islandCounts.Add((island, 1));
}
// scale down the difficulty if the object is doubletappable
double doubletapness = prevObj.GetDoubletapness(currObj);
effectiveRatio *= 1 - doubletapness * 0.75;
rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay;
startRatio = effectiveRatio; startRatio = effectiveRatio;
previousIslandSize = islandSize; // log the last island size. previousIsland = island;
if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting if (prevDelta + deltaDifferenceEpsilon < currDelta) // we're slowing down, stop counting
firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size. firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size.
islandSize = 1; island = new Island((int)currDelta, deltaDifferenceEpsilon);
} }
} }
else if (prevDelta > 1.25 * currDelta) // we want to be speeding up. else if (prevDelta > currDelta + deltaDifferenceEpsilon) // we're speeding up
{ {
// Begin counting island until we change speed again. // Begin counting island until we change speed again.
firstDeltaSwitch = true; firstDeltaSwitch = true;
// bpm change is into slider, this is easy acc window
if (currObj.BaseObject is Slider)
effectiveRatio *= 0.6;
// bpm change was from a slider, this is easier typically than circle -> circle
// unintentional side effect is that bursts with kicksliders at the ends might have lower difficulty than bursts without sliders
if (prevObj.BaseObject is Slider)
effectiveRatio *= 0.6;
startRatio = effectiveRatio; startRatio = effectiveRatio;
islandSize = 1;
island = new Island((int)currDelta, deltaDifferenceEpsilon);
} }
lastObj = prevObj; lastObj = prevObj;
prevObj = currObj; prevObj = currObj;
} }
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though) return Math.Sqrt(4 + rhythmComplexitySum * rhythm_overall_multiplier) / 2.0; // produces multiplier that can be applied to strain. range [1, infinity) (not really though)
}
private static double logistic(double x, double maxValue, double multiplier, double offset) => (maxValue / (1 + Math.Pow(Math.E, offset - (multiplier * x))));
private class Island : IEquatable<Island>
{
private readonly double deltaDifferenceEpsilon;
public Island(double epsilon)
{
deltaDifferenceEpsilon = epsilon;
}
public Island(int delta, double epsilon)
{
deltaDifferenceEpsilon = epsilon;
Delta = Math.Max(delta, OsuDifficultyHitObject.MIN_DELTA_TIME);
DeltaCount++;
}
public int Delta { get; private set; } = int.MaxValue;
public int DeltaCount { get; private set; }
public void AddDelta(int delta)
{
if (Delta == int.MaxValue)
Delta = Math.Max(delta, OsuDifficultyHitObject.MIN_DELTA_TIME);
DeltaCount++;
}
public bool IsSimilarPolarity(Island other)
{
// TODO: consider islands to be of similar polarity only if they're having the same average delta (we don't want to consider 3 singletaps similar to a triple)
// naively adding delta check here breaks _a lot_ of maps because of the flawed ratio calculation
return DeltaCount % 2 == other.DeltaCount % 2;
}
public bool Equals(Island? other)
{
if (other == null)
return false;
return Math.Abs(Delta - other.Delta) < deltaDifferenceEpsilon &&
DeltaCount == other.DeltaCount;
}
public override string ToString()
{
return $"{Delta}x{DeltaCount}";
}
} }
} }
} }

View File

@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
private const double single_spacing_threshold = 125; // 1.25 circles distance between centers private const double single_spacing_threshold = 125; // 1.25 circles distance between centers
private const double min_speed_bonus = 75; // ~200BPM private const double min_speed_bonus = 75; // ~200BPM
private const double speed_balancing_factor = 40; private const double speed_balancing_factor = 40;
private const double distance_multiplier = 0.94;
/// <summary> /// <summary>
/// Evaluates the difficulty of tapping the current object, based on: /// Evaluates the difficulty of tapping the current object, based on:
@ -30,32 +31,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
// derive strainTime for calculation // derive strainTime for calculation
var osuCurrObj = (OsuDifficultyHitObject)current; var osuCurrObj = (OsuDifficultyHitObject)current;
var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null; var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null;
var osuNextObj = (OsuDifficultyHitObject?)current.Next(0);
double strainTime = osuCurrObj.StrainTime; double strainTime = osuCurrObj.StrainTime;
double doubletapness = 1; double doubletapness = 1.0 - osuCurrObj.GetDoubletapness((OsuDifficultyHitObject?)osuCurrObj.Next(0));
// Nerf doubletappable doubles.
if (osuNextObj != null)
{
double currDeltaTime = Math.Max(1, osuCurrObj.DeltaTime);
double nextDeltaTime = Math.Max(1, osuNextObj.DeltaTime);
double deltaDifference = Math.Abs(nextDeltaTime - currDeltaTime);
double speedRatio = currDeltaTime / Math.Max(currDeltaTime, deltaDifference);
double windowRatio = Math.Pow(Math.Min(1, currDeltaTime / osuCurrObj.HitWindowGreat), 2);
doubletapness = Math.Pow(speedRatio, 1 - windowRatio);
}
// Cap deltatime to the OD 300 hitwindow. // Cap deltatime to the OD 300 hitwindow.
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap. // 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
strainTime /= Math.Clamp((strainTime / osuCurrObj.HitWindowGreat) / 0.93, 0.92, 1); strainTime /= Math.Clamp((strainTime / osuCurrObj.HitWindowGreat) / 0.93, 0.92, 1);
// speedBonus will be 1.0 for BPM < 200 // speedBonus will be 0.0 for BPM < 200
double speedBonus = 1.0; double speedBonus = 0.0;
// Add additional scaling bonus for streams/bursts higher than 200bpm // Add additional scaling bonus for streams/bursts higher than 200bpm
if (strainTime < min_speed_bonus) if (strainTime < min_speed_bonus)
speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2); speedBonus = 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
double travelDistance = osuPrevObj?.TravelDistance ?? 0; double travelDistance = osuPrevObj?.TravelDistance ?? 0;
double distance = travelDistance + osuCurrObj.MinimumJumpDistance; double distance = travelDistance + osuCurrObj.MinimumJumpDistance;
@ -63,11 +52,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
// Cap distance at single_spacing_threshold // Cap distance at single_spacing_threshold
distance = Math.Min(distance, single_spacing_threshold); distance = Math.Min(distance, single_spacing_threshold);
// Max distance bonus is 2 at single_spacing_threshold // Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
double distanceBonus = 1 + Math.Pow(distance / single_spacing_threshold, 3.5); double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.95) * distance_multiplier;
// Base difficulty with all bonuses // Base difficulty with all bonuses
double difficulty = speedBonus * distanceBonus * 1000 / strainTime; double difficulty = (1 + speedBonus + distanceBonus) * 1000 / strainTime;
// Apply penalty if there's doubletappable doubles // Apply penalty if there's doubletappable doubles
return difficulty * doubletapness; return difficulty * doubletapness;

View File

@ -46,6 +46,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
[JsonProperty("slider_factor")] [JsonProperty("slider_factor")]
public double SliderFactor { get; set; } public double SliderFactor { get; set; }
[JsonProperty("aim_difficult_strain_count")]
public double AimDifficultStrainCount { get; set; }
[JsonProperty("speed_difficult_strain_count")]
public double SpeedDifficultStrainCount { get; set; }
/// <summary> /// <summary>
/// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc). /// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
/// </summary> /// </summary>
@ -99,6 +105,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty); yield return (ATTRIB_ID_FLASHLIGHT, FlashlightDifficulty);
yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor); yield return (ATTRIB_ID_SLIDER_FACTOR, SliderFactor);
yield return (ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT, AimDifficultStrainCount);
yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount);
yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount); yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
} }
@ -113,8 +122,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
StarRating = values[ATTRIB_ID_DIFFICULTY]; StarRating = values[ATTRIB_ID_DIFFICULTY];
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT); FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR]; SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT];
SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT];
SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT]; SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT];
DrainRate = onlineInfo.DrainRate; DrainRate = onlineInfo.DrainRate;
HitCircleCount = onlineInfo.CircleCount; HitCircleCount = onlineInfo.CircleCount;
SliderCount = onlineInfo.SliderCount; SliderCount = onlineInfo.SliderCount;

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{ {
private const double difficulty_multiplier = 0.0675; private const double difficulty_multiplier = 0.0675;
public override int Version => 20220902; public override int Version => 20241007;
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap) : base(ruleset, beatmap)
@ -48,6 +48,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1; double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountDifficultStrains();
double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountDifficultStrains();
if (mods.Any(m => m is OsuModTouchDevice)) if (mods.Any(m => m is OsuModTouchDevice))
{ {
aimRating = Math.Pow(aimRating, 0.8); aimRating = Math.Pow(aimRating, 0.8);
@ -100,6 +103,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
SpeedNoteCount = speedNotes, SpeedNoteCount = speedNotes,
FlashlightDifficulty = flashlightRating, FlashlightDifficulty = flashlightRating,
SliderFactor = sliderFactor, SliderFactor = sliderFactor,
AimDifficultStrainCount = aimDifficultyStrainCount,
SpeedDifficultStrainCount = speedDifficultyStrainCount,
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5, ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
OverallDifficulty = (80 - hitWindowGreat) / 6, OverallDifficulty = (80 - hitWindowGreat) / 6,
DrainRate = drainRate, DrainRate = drainRate,

View File

@ -14,7 +14,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{ {
public class OsuPerformanceCalculator : PerformanceCalculator public class OsuPerformanceCalculator : PerformanceCalculator
{ {
public const double PERFORMANCE_BASE_MULTIPLIER = 1.14; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. public const double PERFORMANCE_BASE_MULTIPLIER = 1.15; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
private bool usingClassicSliderAccuracy;
private double accuracy; private double accuracy;
private int scoreMaxCombo; private int scoreMaxCombo;
@ -34,6 +36,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{ {
var osuAttributes = (OsuDifficultyAttributes)attributes; var osuAttributes = (OsuDifficultyAttributes)attributes;
usingClassicSliderAccuracy = score.Mods.OfType<OsuModClassic>().Any(m => m.NoSliderHeadAccuracy.Value);
accuracy = score.Accuracy; accuracy = score.Accuracy;
scoreMaxCombo = score.MaxCombo; scoreMaxCombo = score.MaxCombo;
countGreat = score.Statistics.GetValueOrDefault(HitResult.Great); countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
@ -93,11 +97,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
aimValue *= lengthBonus; aimValue *= lengthBonus;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0) if (effectiveMissCount > 0)
aimValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), effectiveMissCount); aimValue *= calculateMissPenalty(effectiveMissCount, attributes.AimDifficultStrainCount);
aimValue *= getComboScalingFactor(attributes);
double approachRateFactor = 0.0; double approachRateFactor = 0.0;
if (attributes.ApproachRate > 10.33) if (attributes.ApproachRate > 10.33)
@ -146,11 +147,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
speedValue *= lengthBonus; speedValue *= lengthBonus;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0) if (effectiveMissCount > 0)
speedValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875)); speedValue *= calculateMissPenalty(effectiveMissCount, attributes.SpeedDifficultStrainCount);
speedValue *= getComboScalingFactor(attributes);
double approachRateFactor = 0.0; double approachRateFactor = 0.0;
if (attributes.ApproachRate > 10.33) if (attributes.ApproachRate > 10.33)
@ -177,7 +175,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0); double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
// Scale the speed value with accuracy and OD. // Scale the speed value with accuracy and OD.
speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2); speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2);
// Scale the speed value with # of 50s to punish doubletapping. // Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
@ -193,6 +191,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window. // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window.
double betterAccuracyPercentage; double betterAccuracyPercentage;
int amountHitObjectsWithAccuracy = attributes.HitCircleCount; int amountHitObjectsWithAccuracy = attributes.HitCircleCount;
if (!usingClassicSliderAccuracy)
amountHitObjectsWithAccuracy += attributes.SliderCount;
if (amountHitObjectsWithAccuracy > 0) if (amountHitObjectsWithAccuracy > 0)
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6); betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
@ -265,6 +265,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return Math.Max(countMiss, comboBasedMissCount); return Math.Max(countMiss, comboBasedMissCount);
} }
// Miss penalty assumes that a player will miss on the hardest parts of a map,
// so we use the amount of relatively difficult sections to adjust miss penalty
// to make it more punishing on maps with lower amount of hard sections.
private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.96 / ((missCount / (4 * Math.Pow(Math.Log(difficultStrainCount), 0.94))) + 1);
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0); private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalHits => countGreat + countOk + countMeh + countMiss;
} }

View File

@ -20,7 +20,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
/// </summary> /// </summary>
public const int NORMALISED_RADIUS = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths. public const int NORMALISED_RADIUS = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
private const int min_delta_time = 25; public const int MIN_DELTA_TIME = 25;
private const float maximum_slider_radius = NORMALISED_RADIUS * 2.4f; private const float maximum_slider_radius = NORMALISED_RADIUS * 2.4f;
private const float assumed_slider_radius = NORMALISED_RADIUS * 1.8f; private const float assumed_slider_radius = NORMALISED_RADIUS * 1.8f;
@ -93,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
this.lastObject = (OsuHitObject)lastObject; this.lastObject = (OsuHitObject)lastObject;
// Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects. // Capped to 25ms to prevent difficulty calculation breaking from simultaneous objects.
StrainTime = Math.Max(DeltaTime, min_delta_time); StrainTime = Math.Max(DeltaTime, MIN_DELTA_TIME);
if (BaseObject is Slider sliderObject) if (BaseObject is Slider sliderObject)
{ {
@ -136,6 +137,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
return Math.Clamp((time - fadeInStartTime) / fadeInDuration, 0.0, 1.0); return Math.Clamp((time - fadeInStartTime) / fadeInDuration, 0.0, 1.0);
} }
/// <summary>
/// Returns how possible is it to doubletap this object together with the next one and get perfect judgement in range from 0 to 1
/// </summary>
public double GetDoubletapness(OsuDifficultyHitObject? osuNextObj)
{
if (osuNextObj != null)
{
double currDeltaTime = Math.Max(1, DeltaTime);
double nextDeltaTime = Math.Max(1, osuNextObj.DeltaTime);
double deltaDifference = Math.Abs(nextDeltaTime - currDeltaTime);
double speedRatio = currDeltaTime / Math.Max(currDeltaTime, deltaDifference);
double windowRatio = Math.Pow(Math.Min(1, currDeltaTime / HitWindowGreat), 2);
return 1.0 - Math.Pow(speedRatio, 1 - windowRatio);
}
return 0;
}
private void setDistances(double clockRate) private void setDistances(double clockRate)
{ {
if (BaseObject is Slider currentSlider) if (BaseObject is Slider currentSlider)
@ -143,7 +162,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
computeSliderCursorPosition(currentSlider); computeSliderCursorPosition(currentSlider);
// Bonus for repeat sliders until a better per nested object strain system can be achieved. // Bonus for repeat sliders until a better per nested object strain system can be achieved.
TravelDistance = currentSlider.LazyTravelDistance * (float)Math.Pow(1 + currentSlider.RepeatCount / 2.5, 1.0 / 2.5); TravelDistance = currentSlider.LazyTravelDistance * (float)Math.Pow(1 + currentSlider.RepeatCount / 2.5, 1.0 / 2.5);
TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time); TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, MIN_DELTA_TIME);
} }
// We don't need to calculate either angle or distance when one of the last->curr objects is a spinner // We don't need to calculate either angle or distance when one of the last->curr objects is a spinner
@ -167,8 +186,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
if (lastObject is Slider lastSlider) if (lastObject is Slider lastSlider)
{ {
double lastTravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time); double lastTravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, MIN_DELTA_TIME);
MinimumJumpTime = Math.Max(StrainTime - lastTravelTime, min_delta_time); MinimumJumpTime = Math.Max(StrainTime - lastTravelTime, MIN_DELTA_TIME);
// //
// There are two types of slider-to-object patterns to consider in order to better approximate the real movement a player will take to jump between the hitobjects. // There are two types of slider-to-object patterns to consider in order to better approximate the real movement a player will take to jump between the hitobjects.

View File

@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
private double currentStrain; private double currentStrain;
private double skillMultiplier => 24.963; private double skillMultiplier => 25.18;
private double strainDecayBase => 0.15; private double strainDecayBase => 0.15;
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
@ -34,6 +34,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
{ {
currentStrain *= strainDecay(current.DeltaTime); currentStrain *= strainDecay(current.DeltaTime);
currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier; currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier;
ObjectStrains.Add(currentStrain);
return currentStrain; return currentStrain;
} }

View File

@ -23,6 +23,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
/// </summary> /// </summary>
protected virtual double ReducedStrainBaseline => 0.75; protected virtual double ReducedStrainBaseline => 0.75;
protected List<double> ObjectStrains = new List<double>();
protected double Difficulty;
protected OsuStrainSkill(Mod[] mods) protected OsuStrainSkill(Mod[] mods)
: base(mods) : base(mods)
{ {
@ -30,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
public override double DifficultyValue() public override double DifficultyValue()
{ {
double difficulty = 0; Difficulty = 0;
double weight = 1; double weight = 1;
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
@ -50,11 +53,25 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
// We're sorting from highest to lowest strain. // We're sorting from highest to lowest strain.
foreach (double strain in strains.OrderDescending()) foreach (double strain in strains.OrderDescending())
{ {
difficulty += strain * weight; Difficulty += strain * weight;
weight *= DecayWeight; weight *= DecayWeight;
} }
return difficulty; return Difficulty;
}
/// <summary>
/// Returns the number of strains weighted against the top strain.
/// The result is scaled by clock rate as it affects the total number of strains.
/// </summary>
public double CountDifficultStrains()
{
if (Difficulty == 0)
return 0.0;
double consistentTopStrain = Difficulty / 10; // What would the top strain be if all strain values were identical
// Use a weighted sum of all strains. Constants are arbitrary and give nice values
return ObjectStrains.Sum(s => 1.1 / (1 + Math.Exp(-10 * (s / consistentTopStrain - 0.88))));
} }
public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0; public static double DifficultyToPerformance(double difficulty) => Math.Pow(5.0 * Math.Max(1.0, difficulty / 0.0675) - 4.0, 3.0) / 100000.0;

View File

@ -6,7 +6,6 @@ using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Evaluators; using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
using System.Collections.Generic;
using System.Linq; using System.Linq;
namespace osu.Game.Rulesets.Osu.Difficulty.Skills namespace osu.Game.Rulesets.Osu.Difficulty.Skills
@ -24,8 +23,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
protected override int ReducedSectionCount => 5; protected override int ReducedSectionCount => 5;
private readonly List<double> objectStrains = new List<double>();
public Speed(Mod[] mods) public Speed(Mod[] mods)
: base(mods) : base(mods)
{ {
@ -43,23 +40,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current); currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);
double totalStrain = currentStrain * currentRhythm; double totalStrain = currentStrain * currentRhythm;
ObjectStrains.Add(totalStrain);
objectStrains.Add(totalStrain);
return totalStrain; return totalStrain;
} }
public double RelevantNoteCount() public double RelevantNoteCount()
{ {
if (objectStrains.Count == 0) if (ObjectStrains.Count == 0)
return 0; return 0;
double maxStrain = objectStrains.Max(); double maxStrain = ObjectStrains.Max();
if (maxStrain == 0) if (maxStrain == 0)
return 0; return 0;
return objectStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxStrain * 12.0 - 6.0)))); return ObjectStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxStrain * 12.0 - 6.0))));
} }
} }
} }

View File

@ -0,0 +1,126 @@
// 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.Input.Events;
using osu.Game.Rulesets.Edit;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Osu.Edit.Blueprints
{
public partial class GridPlacementBlueprint : PlacementBlueprint
{
[Resolved]
private HitObjectComposer? hitObjectComposer { get; set; }
private OsuGridToolboxGroup gridToolboxGroup = null!;
private Vector2 originalOrigin;
private float originalSpacing;
private float originalRotation;
[BackgroundDependencyLoader]
private void load(OsuGridToolboxGroup gridToolboxGroup)
{
this.gridToolboxGroup = gridToolboxGroup;
originalOrigin = gridToolboxGroup.StartPosition.Value;
originalSpacing = gridToolboxGroup.Spacing.Value;
originalRotation = gridToolboxGroup.GridLinesRotation.Value;
}
public override void EndPlacement(bool commit)
{
if (!commit && PlacementActive != PlacementState.Finished)
{
gridToolboxGroup.StartPosition.Value = originalOrigin;
gridToolboxGroup.Spacing.Value = originalSpacing;
if (!gridToolboxGroup.GridLinesRotation.Disabled)
gridToolboxGroup.GridLinesRotation.Value = originalRotation;
}
base.EndPlacement(commit);
// You typically only place the grid once, so we switch back to the last tool after placement.
if (commit && hitObjectComposer is OsuHitObjectComposer osuHitObjectComposer)
osuHitObjectComposer.SetLastTool();
}
protected override bool OnClick(ClickEvent e)
{
if (e.Button == MouseButton.Left)
{
switch (PlacementActive)
{
case PlacementState.Waiting:
BeginPlacement(true);
return true;
case PlacementState.Active:
EndPlacement(true);
return true;
}
}
return base.OnClick(e);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
if (e.Button == MouseButton.Right)
{
// Reset the grid to the default values.
gridToolboxGroup.StartPosition.Value = gridToolboxGroup.StartPosition.Default;
gridToolboxGroup.Spacing.Value = gridToolboxGroup.Spacing.Default;
if (!gridToolboxGroup.GridLinesRotation.Disabled)
gridToolboxGroup.GridLinesRotation.Value = gridToolboxGroup.GridLinesRotation.Default;
EndPlacement(true);
return true;
}
return base.OnMouseDown(e);
}
protected override bool OnDragStart(DragStartEvent e)
{
if (e.Button == MouseButton.Left)
{
BeginPlacement(true);
return true;
}
return base.OnDragStart(e);
}
protected override void OnDragEnd(DragEndEvent e)
{
if (PlacementActive == PlacementState.Active)
EndPlacement(true);
base.OnDragEnd(e);
}
public override SnapType SnapType => ~SnapType.GlobalGrids;
public override void UpdateTimeAndPosition(SnapResult result)
{
var pos = ToLocalSpace(result.ScreenSpacePosition);
if (PlacementActive != PlacementState.Active)
gridToolboxGroup.StartPosition.Value = pos;
else
{
// Default to the original spacing and rotation if the distance is too small.
if (Vector2.Distance(gridToolboxGroup.StartPosition.Value, pos) < 2)
{
gridToolboxGroup.Spacing.Value = originalSpacing;
if (!gridToolboxGroup.GridLinesRotation.Disabled)
gridToolboxGroup.GridLinesRotation.Value = originalRotation;
}
else
{
gridToolboxGroup.SetGridFromPoints(gridToolboxGroup.StartPosition.Value, pos);
}
}
}
}
}

View File

@ -178,7 +178,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private bool isSplittable(PathControlPointPiece<T> p) => private bool isSplittable(PathControlPointPiece<T> p) =>
// A hit object can only be split on control points which connect two different path segments. // A hit object can only be split on control points which connect two different path segments.
p.ControlPoint.Type.HasValue && p != Pieces.FirstOrDefault() && p != Pieces.LastOrDefault(); p.ControlPoint.Type.HasValue && p.ControlPoint != controlPoints.FirstOrDefault() && p.ControlPoint != controlPoints.LastOrDefault();
private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e) private void onControlPointsChanged(object sender, NotifyCollectionChangedEventArgs e)
{ {

View File

@ -0,0 +1,29 @@
// 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.Sprites;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Osu.Edit.Blueprints;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class GridFromPointsTool : CompositionTool
{
public GridFromPointsTool()
: base("Grid")
{
TooltipText = """
Left click to set the origin.
Left click again to set the spacing and rotation.
Right click to reset to default.
Click and drag to set the origin, spacing and rotation.
""";
}
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.Solid.DraftingCompass };
public override PlacementBlueprint CreatePlacementBlueprint() => new GridPlacementBlueprint();
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -14,7 +15,6 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.RadioButtons; using osu.Game.Screens.Edit.Components.RadioButtons;
@ -38,7 +38,6 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
MinValue = 0f, MinValue = 0f,
MaxValue = OsuPlayfield.BASE_SIZE.X, MaxValue = OsuPlayfield.BASE_SIZE.X,
Precision = 1f
}; };
/// <summary> /// <summary>
@ -48,7 +47,6 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
MinValue = 0f, MinValue = 0f,
MaxValue = OsuPlayfield.BASE_SIZE.Y, MaxValue = OsuPlayfield.BASE_SIZE.Y,
Precision = 1f
}; };
/// <summary> /// <summary>
@ -58,7 +56,6 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
MinValue = 4f, MinValue = 4f,
MaxValue = 128f, MaxValue = 128f,
Precision = 1f
}; };
/// <summary> /// <summary>
@ -68,14 +65,13 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
MinValue = -180f, MinValue = -180f,
MaxValue = 180f, MaxValue = 180f,
Precision = 1f
}; };
/// <summary> /// <summary>
/// Read-only bindable representing the grid's origin. /// Read-only bindable representing the grid's origin.
/// Equivalent to <code>new Vector2(StartPositionX, StartPositionY)</code> /// Equivalent to <code>new Vector2(StartPositionX, StartPositionY)</code>
/// </summary> /// </summary>
public Bindable<Vector2> StartPosition { get; } = new Bindable<Vector2>(); public Bindable<Vector2> StartPosition { get; } = new Bindable<Vector2>(OsuPlayfield.BASE_SIZE / 2);
/// <summary> /// <summary>
/// Read-only bindable representing the grid's spacing in both the X and Y dimension. /// Read-only bindable representing the grid's spacing in both the X and Y dimension.
@ -91,8 +87,6 @@ namespace osu.Game.Rulesets.Osu.Edit
private ExpandableSlider<float> gridLinesRotationSlider = null!; private ExpandableSlider<float> gridLinesRotationSlider = null!;
private EditorRadioButtonCollection gridTypeButtons = null!; private EditorRadioButtonCollection gridTypeButtons = null!;
private ExpandableButton useSelectedObjectPositionButton = null!;
public OsuGridToolboxGroup() public OsuGridToolboxGroup()
: base("grid") : base("grid")
{ {
@ -100,6 +94,26 @@ namespace osu.Game.Rulesets.Osu.Edit
private const float max_automatic_spacing = 64; private const float max_automatic_spacing = 64;
public void SetGridFromPoints(Vector2 point1, Vector2 point2)
{
StartPositionX.Value = point1.X;
StartPositionY.Value = point1.Y;
// Get the angle between the two points and normalize to the valid range.
if (!GridLinesRotation.Disabled)
{
float period = GridLinesRotation.MaxValue - GridLinesRotation.MinValue;
GridLinesRotation.Value = normalizeRotation(MathHelper.RadiansToDegrees(MathF.Atan2(point2.Y - point1.Y, point2.X - point1.X)), period);
}
// Divide the distance so that there is a good density of grid lines.
// This matches the maximum grid size of the grid size cycling hotkey.
float dist = Vector2.Distance(point1, point2);
while (dist >= max_automatic_spacing)
dist /= 2;
Spacing.Value = dist;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -115,19 +129,6 @@ namespace osu.Game.Rulesets.Osu.Edit
Current = StartPositionY, Current = StartPositionY,
KeyboardStep = 1, KeyboardStep = 1,
}, },
useSelectedObjectPositionButton = new ExpandableButton
{
ExpandedLabelText = "Centre on selected object",
Action = () =>
{
if (editorBeatmap.SelectedHitObjects.Count != 1)
return;
StartPosition.Value = ((IHasPosition)editorBeatmap.SelectedHitObjects.Single()).Position;
updateEnabledStates();
},
RelativeSizeAxes = Axes.X,
},
spacingSlider = new ExpandableSlider<float> spacingSlider = new ExpandableSlider<float>
{ {
Current = Spacing, Current = Spacing,
@ -176,22 +177,28 @@ namespace osu.Game.Rulesets.Osu.Edit
StartPositionX.BindValueChanged(x => StartPositionX.BindValueChanged(x =>
{ {
startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:N0}"; startPositionXSlider.ContractedLabelText = $"X: {x.NewValue:#,0.##}";
startPositionXSlider.ExpandedLabelText = $"X Offset: {x.NewValue:N0}"; startPositionXSlider.ExpandedLabelText = $"X Offset: {x.NewValue:#,0.##}";
StartPosition.Value = new Vector2(x.NewValue, StartPosition.Value.Y); StartPosition.Value = new Vector2(x.NewValue, StartPosition.Value.Y);
}, true); }, true);
StartPositionY.BindValueChanged(y => StartPositionY.BindValueChanged(y =>
{ {
startPositionYSlider.ContractedLabelText = $"Y: {y.NewValue:N0}"; startPositionYSlider.ContractedLabelText = $"Y: {y.NewValue:#,0.##}";
startPositionYSlider.ExpandedLabelText = $"Y Offset: {y.NewValue:N0}"; startPositionYSlider.ExpandedLabelText = $"Y Offset: {y.NewValue:#,0.##}";
StartPosition.Value = new Vector2(StartPosition.Value.X, y.NewValue); StartPosition.Value = new Vector2(StartPosition.Value.X, y.NewValue);
}, true); }, true);
StartPosition.BindValueChanged(pos =>
{
StartPositionX.Value = pos.NewValue.X;
StartPositionY.Value = pos.NewValue.Y;
});
Spacing.BindValueChanged(spacing => Spacing.BindValueChanged(spacing =>
{ {
spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:N0}"; spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:#,0.##}";
spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:N0}"; spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:#,0.##}";
SpacingVector.Value = new Vector2(spacing.NewValue); SpacingVector.Value = new Vector2(spacing.NewValue);
editorBeatmap.BeatmapInfo.GridSize = (int)spacing.NewValue; editorBeatmap.BeatmapInfo.GridSize = (int)spacing.NewValue;
}, true); }, true);
@ -209,34 +216,29 @@ namespace osu.Game.Rulesets.Osu.Edit
switch (v.NewValue) switch (v.NewValue)
{ {
case PositionSnapGridType.Square: case PositionSnapGridType.Square:
GridLinesRotation.Value = ((GridLinesRotation.Value + 405) % 90) - 45; GridLinesRotation.Value = normalizeRotation(GridLinesRotation.Value, 90);
GridLinesRotation.MinValue = -45; GridLinesRotation.MinValue = -45;
GridLinesRotation.MaxValue = 45; GridLinesRotation.MaxValue = 45;
break; break;
case PositionSnapGridType.Triangle: case PositionSnapGridType.Triangle:
GridLinesRotation.Value = ((GridLinesRotation.Value + 390) % 60) - 30; GridLinesRotation.Value = normalizeRotation(GridLinesRotation.Value, 60);
GridLinesRotation.MinValue = -30; GridLinesRotation.MinValue = -30;
GridLinesRotation.MaxValue = 30; GridLinesRotation.MaxValue = 30;
break; break;
} }
}, true); }, true);
editorBeatmap.BeatmapReprocessed += updateEnabledStates;
editorBeatmap.SelectedHitObjects.BindCollectionChanged((_, _) => updateEnabledStates());
expandingContainer?.Expanded.BindValueChanged(v => expandingContainer?.Expanded.BindValueChanged(v =>
{ {
gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint); gridTypeButtons.FadeTo(v.NewValue ? 1f : 0f, 500, Easing.OutQuint);
gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None; gridTypeButtons.BypassAutoSizeAxes = !v.NewValue ? Axes.Y : Axes.None;
updateEnabledStates();
}, true); }, true);
} }
private void updateEnabledStates() private float normalizeRotation(float rotation, float period)
{ {
useSelectedObjectPositionButton.Enabled.Value = expandingContainer?.Expanded.Value == true return ((rotation + 360 + period * 0.5f) % period) - period * 0.5f;
&& editorBeatmap.SelectedHitObjects.Count == 1
&& StartPosition.Value != ((IHasPosition)editorBeatmap.SelectedHitObjects.Single()).Position;
} }
private void nextGridSize() private void nextGridSize()

View File

@ -45,7 +45,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
new HitCircleCompositionTool(), new HitCircleCompositionTool(),
new SliderCompositionTool(), new SliderCompositionTool(),
new SpinnerCompositionTool() new SpinnerCompositionTool(),
new GridFromPointsTool()
}; };
private readonly Bindable<TernaryState> rectangularGridSnapToggle = new Bindable<TernaryState>(); private readonly Bindable<TernaryState> rectangularGridSnapToggle = new Bindable<TernaryState>();
@ -79,13 +80,12 @@ namespace osu.Game.Rulesets.Osu.Edit
// Give a bit of breathing room around the playfield content. // Give a bit of breathing room around the playfield content.
PlayfieldContentContainer.Padding = new MarginPadding(10); PlayfieldContentContainer.Padding = new MarginPadding(10);
LayerBelowRuleset.AddRange(new Drawable[] LayerBelowRuleset.Add(
{
distanceSnapGridContainer = new Container distanceSnapGridContainer = new Container
{ {
RelativeSizeAxes = Axes.Both RelativeSizeAxes = Axes.Both
} }
}); );
selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy(); selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy();
selectedHitObjects.CollectionChanged += (_, _) => updateDistanceSnapGrid(); selectedHitObjects.CollectionChanged += (_, _) => updateDistanceSnapGrid();

View File

@ -16,13 +16,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
{ {
public partial class OsuDifficultySection : SetupSection public partial class OsuDifficultySection : SetupSection
{ {
private LabelledSliderBar<float> circleSizeSlider { get; set; } = null!; private FormSliderBar<float> circleSizeSlider { get; set; } = null!;
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!; private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
private LabelledSliderBar<float> approachRateSlider { get; set; } = null!; private FormSliderBar<float> approachRateSlider { get; set; } = null!;
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!; private FormSliderBar<float> overallDifficultySlider { get; set; } = null!;
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!; private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!; private FormSliderBar<double> tickRateSlider { get; set; } = null!;
private LabelledSliderBar<float> stackLeniency { get; set; } = null!; private FormSliderBar<float> stackLeniency { get; set; } = null!;
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader; public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
@ -31,103 +31,110 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
circleSizeSlider = new LabelledSliderBar<float> circleSizeSlider = new FormSliderBar<float>
{ {
Label = BeatmapsetsStrings.ShowStatsCs, Caption = BeatmapsetsStrings.ShowStatsCs,
FixedLabelWidth = LABEL_WIDTH, HintText = EditorSetupStrings.CircleSizeDescription,
Description = EditorSetupStrings.CircleSizeDescription,
Current = new BindableFloat(Beatmap.Difficulty.CircleSize) Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
{ {
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0, MinValue = 0,
MaxValue = 10, MaxValue = 10,
Precision = 0.1f, Precision = 0.1f,
} },
TransferValueOnCommit = true,
TabbableContentContainer = this,
}, },
healthDrainSlider = new LabelledSliderBar<float> healthDrainSlider = new FormSliderBar<float>
{ {
Label = BeatmapsetsStrings.ShowStatsDrain, Caption = BeatmapsetsStrings.ShowStatsDrain,
FixedLabelWidth = LABEL_WIDTH, HintText = EditorSetupStrings.DrainRateDescription,
Description = EditorSetupStrings.DrainRateDescription,
Current = new BindableFloat(Beatmap.Difficulty.DrainRate) Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
{ {
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0, MinValue = 0,
MaxValue = 10, MaxValue = 10,
Precision = 0.1f, Precision = 0.1f,
} },
TransferValueOnCommit = true,
TabbableContentContainer = this,
}, },
approachRateSlider = new LabelledSliderBar<float> approachRateSlider = new FormSliderBar<float>
{ {
Label = BeatmapsetsStrings.ShowStatsAr, Caption = BeatmapsetsStrings.ShowStatsAr,
FixedLabelWidth = LABEL_WIDTH, HintText = EditorSetupStrings.ApproachRateDescription,
Description = EditorSetupStrings.ApproachRateDescription,
Current = new BindableFloat(Beatmap.Difficulty.ApproachRate) Current = new BindableFloat(Beatmap.Difficulty.ApproachRate)
{ {
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0, MinValue = 0,
MaxValue = 10, MaxValue = 10,
Precision = 0.1f, Precision = 0.1f,
} },
TransferValueOnCommit = true,
TabbableContentContainer = this,
}, },
overallDifficultySlider = new LabelledSliderBar<float> overallDifficultySlider = new FormSliderBar<float>
{ {
Label = BeatmapsetsStrings.ShowStatsAccuracy, Caption = BeatmapsetsStrings.ShowStatsAccuracy,
FixedLabelWidth = LABEL_WIDTH, HintText = EditorSetupStrings.OverallDifficultyDescription,
Description = EditorSetupStrings.OverallDifficultyDescription,
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty) Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
{ {
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0, MinValue = 0,
MaxValue = 10, MaxValue = 10,
Precision = 0.1f, Precision = 0.1f,
} },
TransferValueOnCommit = true,
TabbableContentContainer = this,
}, },
baseVelocitySlider = new LabelledSliderBar<double> baseVelocitySlider = new FormSliderBar<double>
{ {
Label = EditorSetupStrings.BaseVelocity, Caption = EditorSetupStrings.BaseVelocity,
FixedLabelWidth = LABEL_WIDTH, HintText = EditorSetupStrings.BaseVelocityDescription,
Description = EditorSetupStrings.BaseVelocityDescription,
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier) Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
{ {
Default = 1.4, Default = 1.4,
MinValue = 0.4, MinValue = 0.4,
MaxValue = 3.6, MaxValue = 3.6,
Precision = 0.01f, Precision = 0.01f,
} },
TransferValueOnCommit = true,
TabbableContentContainer = this,
}, },
tickRateSlider = new LabelledSliderBar<double> tickRateSlider = new FormSliderBar<double>
{ {
Label = EditorSetupStrings.TickRate, Caption = EditorSetupStrings.TickRate,
FixedLabelWidth = LABEL_WIDTH, HintText = EditorSetupStrings.TickRateDescription,
Description = EditorSetupStrings.TickRateDescription,
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate) Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
{ {
Default = 1, Default = 1,
MinValue = 1, MinValue = 1,
MaxValue = 4, MaxValue = 4,
Precision = 1, Precision = 1,
} },
TransferValueOnCommit = true,
TabbableContentContainer = this,
}, },
stackLeniency = new LabelledSliderBar<float> stackLeniency = new FormSliderBar<float>
{ {
Label = "Stack Leniency", Caption = "Stack Leniency",
FixedLabelWidth = LABEL_WIDTH, HintText = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.",
Description = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.",
Current = new BindableFloat(Beatmap.BeatmapInfo.StackLeniency) Current = new BindableFloat(Beatmap.BeatmapInfo.StackLeniency)
{ {
Default = 0.7f, Default = 0.7f,
MinValue = 0, MinValue = 0,
MaxValue = 1, MaxValue = 1,
Precision = 0.1f Precision = 0.1f
} },
TransferValueOnCommit = true,
TabbableContentContainer = this,
}, },
}; };
foreach (var item in Children.OfType<LabelledSliderBar<float>>()) foreach (var item in Children.OfType<FormSliderBar<float>>())
item.Current.ValueChanged += _ => updateValues(); item.Current.ValueChanged += _ => updateValues();
foreach (var item in Children.OfType<LabelledSliderBar<double>>()) foreach (var item in Children.OfType<FormSliderBar<double>>())
item.Current.ValueChanged += _ => updateValues(); item.Current.ValueChanged += _ => updateValues();
} }

View File

@ -204,6 +204,7 @@ namespace osu.Game.Rulesets.Osu.Objects
SpanStartTime = e.SpanStartTime, SpanStartTime = e.SpanStartTime,
StartTime = e.Time, StartTime = e.Time,
Position = Position + Path.PositionAt(e.PathProgress), Position = Position + Path.PositionAt(e.PathProgress),
PathProgress = e.PathProgress,
StackHeight = StackHeight, StackHeight = StackHeight,
}); });
break; break;
@ -236,6 +237,7 @@ namespace osu.Game.Rulesets.Osu.Objects
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration, StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
Position = Position + Path.PositionAt(e.PathProgress), Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight, StackHeight = StackHeight,
PathProgress = e.PathProgress,
}); });
break; break;
} }
@ -248,14 +250,27 @@ namespace osu.Game.Rulesets.Osu.Objects
{ {
endPositionCache.Invalidate(); endPositionCache.Invalidate();
if (HeadCircle != null) foreach (var nested in NestedHitObjects)
HeadCircle.Position = Position; {
switch (nested)
{
case SliderHeadCircle headCircle:
headCircle.Position = Position;
break;
if (TailCircle != null) case SliderTailCircle tailCircle:
TailCircle.Position = EndPosition; tailCircle.Position = EndPosition;
break;
if (LastRepeat != null) case SliderRepeat repeat:
LastRepeat.Position = RepeatCount % 2 == 0 ? Position : Position + Path.PositionAt(1); repeat.Position = Position + Path.PositionAt(repeat.PathProgress);
break;
case SliderTick tick:
tick.Position = Position + Path.PositionAt(tick.PathProgress);
break;
}
}
} }
protected void UpdateNestedSamples() protected void UpdateNestedSamples()

View File

@ -5,6 +5,8 @@ namespace osu.Game.Rulesets.Osu.Objects
{ {
public class SliderRepeat : SliderEndCircle public class SliderRepeat : SliderEndCircle
{ {
public double PathProgress { get; set; }
public SliderRepeat(Slider slider) public SliderRepeat(Slider slider)
: base(slider) : base(slider)
{ {

View File

@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Objects
{ {
public int SpanIndex { get; set; } public int SpanIndex { get; set; }
public double SpanStartTime { get; set; } public double SpanStartTime { get; set; }
public double PathProgress { get; set; }
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
{ {

View File

@ -5,6 +5,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Localisation; using osu.Framework.Localisation;
@ -39,6 +40,7 @@ using osu.Game.Scoring;
using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Ranking.Statistics; using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning; using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu namespace osu.Game.Rulesets.Osu
{ {
@ -336,10 +338,28 @@ namespace osu.Game.Rulesets.Osu
}; };
} }
public override IEnumerable<SetupSection> CreateEditorSetupSections() => public override IEnumerable<Drawable> CreateEditorSetupSections() =>
[ [
new MetadataSection(),
new OsuDifficultySection(), new OsuDifficultySection(),
new ColoursSection(), new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(SetupScreen.SPACING),
Children = new Drawable[]
{
new ResourcesSection
{
RelativeSizeAxes = Axes.X,
},
new ColoursSection
{
RelativeSizeAxes = Axes.X,
}
}
},
new DesignSection(),
]; ];
/// <seealso cref="OsuHitObject.ApplyDefaultsToSelf"/> /// <seealso cref="OsuHitObject.ApplyDefaultsToSelf"/>

View File

@ -232,8 +232,6 @@ namespace osu.Game.Rulesets.Osu.Utils
slider.Position = workingObject.PositionModified = new Vector2(newX, newY); slider.Position = workingObject.PositionModified = new Vector2(newX, newY);
workingObject.EndPositionModified = slider.EndPosition; workingObject.EndPositionModified = slider.EndPosition;
shiftNestedObjects(slider, workingObject.PositionModified - workingObject.PositionOriginal);
return workingObject.PositionModified - previousPosition; return workingObject.PositionModified - previousPosition;
} }
@ -307,22 +305,6 @@ namespace osu.Game.Rulesets.Osu.Utils
return new RectangleF(left, top, right - left, bottom - top); return new RectangleF(left, top, right - left, bottom - top);
} }
/// <summary>
/// Shifts all nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s by the specified shift.
/// </summary>
/// <param name="slider"><see cref="Slider"/> whose nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted</param>
/// <param name="shift">The <see cref="Vector2"/> the <see cref="Slider"/>'s nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted by</param>
private static void shiftNestedObjects(Slider slider, Vector2 shift)
{
foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat))
{
if (!(hitObject is OsuHitObject osuHitObject))
continue;
osuHitObject.Position += shift;
}
}
/// <summary> /// <summary>
/// Clamp a position to playfield, keeping a specified distance from the edges. /// Clamp a position to playfield, keeping a specified distance from the edges.
/// </summary> /// </summary>
@ -431,7 +413,6 @@ namespace osu.Game.Rulesets.Osu.Utils
private class WorkingObject private class WorkingObject
{ {
public float RotationOriginal { get; } public float RotationOriginal { get; }
public Vector2 PositionOriginal { get; }
public Vector2 PositionModified { get; set; } public Vector2 PositionModified { get; set; }
public Vector2 EndPositionModified { get; set; } public Vector2 EndPositionModified { get; set; }
@ -442,7 +423,7 @@ namespace osu.Game.Rulesets.Osu.Utils
{ {
PositionInfo = positionInfo; PositionInfo = positionInfo;
RotationOriginal = HitObject is Slider slider ? getSliderRotation(slider) : 0; RotationOriginal = HitObject is Slider slider ? getSliderRotation(slider) : 0;
PositionModified = PositionOriginal = HitObject.Position; PositionModified = HitObject.Position;
EndPositionModified = HitObject.EndPosition; EndPositionModified = HitObject.EndPosition;
} }
} }

View File

@ -43,6 +43,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
[JsonProperty("great_hit_window")] [JsonProperty("great_hit_window")]
public double GreatHitWindow { get; set; } public double GreatHitWindow { get; set; }
/// <summary>
/// The perceived hit window for an OK hit inclusive of rate-adjusting mods (DT/HT/etc).
/// </summary>
/// <remarks>
/// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing.
/// </remarks>
[JsonProperty("ok_hit_window")]
public double OkHitWindow { get; set; }
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes() public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
{ {
foreach (var v in base.ToDatabaseAttributes()) foreach (var v in base.ToDatabaseAttributes())
@ -50,6 +59,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
yield return (ATTRIB_ID_DIFFICULTY, StarRating); yield return (ATTRIB_ID_DIFFICULTY, StarRating);
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow); yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow);
} }
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo) public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
@ -58,6 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
StarRating = values[ATTRIB_ID_DIFFICULTY]; StarRating = values[ATTRIB_ID_DIFFICULTY];
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW]; GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW];
} }
} }
} }

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private const double colour_skill_multiplier = 0.375 * difficulty_multiplier; private const double colour_skill_multiplier = 0.375 * difficulty_multiplier;
private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier; private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier;
public override int Version => 20221107; public override int Version => 20241007;
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap) public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
: base(ruleset, beatmap) : base(ruleset, beatmap)
@ -99,6 +99,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
ColourDifficulty = colourRating, ColourDifficulty = colourRating,
PeakDifficulty = combinedRating, PeakDifficulty = combinedRating,
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate,
MaxCombo = beatmap.GetMaxCombo(), MaxCombo = beatmap.GetMaxCombo(),
}; };

View File

@ -18,6 +18,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
[JsonProperty("effective_miss_count")] [JsonProperty("effective_miss_count")]
public double EffectiveMissCount { get; set; } public double EffectiveMissCount { get; set; }
[JsonProperty("estimated_unstable_rate")]
public double? EstimatedUnstableRate { get; set; }
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay() public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
{ {
foreach (var attribute in base.GetAttributesForDisplay()) foreach (var attribute in base.GetAttributesForDisplay())

View File

@ -9,6 +9,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Taiko.Difficulty namespace osu.Game.Rulesets.Taiko.Difficulty
{ {
@ -18,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private int countOk; private int countOk;
private int countMeh; private int countMeh;
private int countMiss; private int countMiss;
private double accuracy; private double? estimatedUnstableRate;
private double effectiveMissCount; private double effectiveMissCount;
@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok); countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh); countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss); countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
accuracy = customAccuracy; estimatedUnstableRate = computeDeviationUpperBound(taikoAttributes) * 10;
// The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000. // The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000.
if (totalSuccessfulHits > 0) if (totalSuccessfulHits > 0)
@ -65,6 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
Difficulty = difficultyValue, Difficulty = difficultyValue,
Accuracy = accuracyValue, Accuracy = accuracyValue,
EffectiveMissCount = effectiveMissCount, EffectiveMissCount = effectiveMissCount,
EstimatedUnstableRate = estimatedUnstableRate,
Total = totalValue Total = totalValue
}; };
} }
@ -85,35 +87,94 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
difficultyValue *= 1.025; difficultyValue *= 1.025;
if (score.Mods.Any(m => m is ModHardRock)) if (score.Mods.Any(m => m is ModHardRock))
difficultyValue *= 1.050; difficultyValue *= 1.10;
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>)) if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>))
difficultyValue *= 1.050 * lengthBonus; difficultyValue *= 1.050 * lengthBonus;
return difficultyValue * Math.Pow(accuracy, 2.0); if (estimatedUnstableRate == null)
return 0;
return difficultyValue * Math.Pow(SpecialFunctions.Erf(400 / (Math.Sqrt(2) * estimatedUnstableRate.Value)), 2.0);
} }
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert) private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert)
{ {
if (attributes.GreatHitWindow <= 0) if (attributes.GreatHitWindow <= 0 || estimatedUnstableRate == null)
return 0; return 0;
double accuracyValue = Math.Pow(60.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(accuracy, 8.0) * Math.Pow(attributes.StarRating, 0.4) * 27.0; double accuracyValue = Math.Pow(70 / estimatedUnstableRate.Value, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0;
double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); double lengthBonus = Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));
accuracyValue *= lengthBonus;
// Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values. // Slight HDFL Bonus for accuracy. A clamp is used to prevent against negative values.
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>) && score.Mods.Any(m => m is ModHidden) && !isConvert) if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>) && score.Mods.Any(m => m is ModHidden) && !isConvert)
accuracyValue *= Math.Max(1.0, 1.1 * lengthBonus); accuracyValue *= Math.Max(1.0, 1.05 * lengthBonus);
return accuracyValue; return accuracyValue;
} }
/// <summary>
/// Computes an upper bound on the player's tap deviation based on the OD, number of circles and sliders,
/// and the hit judgements, assuming the player's mean hit error is 0. The estimation is consistent in that
/// two SS scores on the same map with the same settings will always return the same deviation.
/// </summary>
private double? computeDeviationUpperBound(TaikoDifficultyAttributes attributes)
{
if (totalSuccessfulHits == 0 || attributes.GreatHitWindow <= 0)
return null;
double h300 = attributes.GreatHitWindow;
double h100 = attributes.OkHitWindow;
const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed).
// The upper bound on deviation, calculated with the ratio of 300s to objects, and the great hit window.
double? calcDeviationGreatWindow()
{
if (countGreat == 0) return null;
double n = totalHits;
// Proportion of greats hit.
double p = countGreat / n;
// We can be 99% confident that p is at least this value.
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
// We can be 99% confident that the deviation is not higher than:
return h300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
}
// The upper bound on deviation, calculated with the ratio of 300s + 100s to objects, and the good hit window.
// This will return a lower value than the first method when the number of 100s is high, but the miss count is low.
double? calcDeviationGoodWindow()
{
if (totalSuccessfulHits == 0) return null;
double n = totalHits;
// Proportion of greats + goods hit.
double p = totalSuccessfulHits / n;
// We can be 99% confident that p is at least this value.
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
// We can be 99% confident that the deviation is not higher than:
return h100 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
}
double? deviationGreatWindow = calcDeviationGreatWindow();
double? deviationGoodWindow = calcDeviationGoodWindow();
if (deviationGreatWindow is null)
return deviationGoodWindow;
return Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value);
}
private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh; private int totalSuccessfulHits => countGreat + countOk + countMeh;
private double customAccuracy => totalHits > 0 ? (countGreat * 300 + countOk * 150) / (totalHits * 300.0) : 0;
} }
} }

View File

@ -16,10 +16,10 @@ namespace osu.Game.Rulesets.Taiko.Edit.Setup
{ {
public partial class TaikoDifficultySection : SetupSection public partial class TaikoDifficultySection : SetupSection
{ {
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!; private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!; private FormSliderBar<float> overallDifficultySlider { get; set; } = null!;
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!; private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!; private FormSliderBar<double> tickRateSlider { get; set; } = null!;
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader; public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
@ -28,64 +28,68 @@ namespace osu.Game.Rulesets.Taiko.Edit.Setup
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
healthDrainSlider = new LabelledSliderBar<float> healthDrainSlider = new FormSliderBar<float>
{ {
Label = BeatmapsetsStrings.ShowStatsDrain, Caption = BeatmapsetsStrings.ShowStatsDrain,
FixedLabelWidth = LABEL_WIDTH, HintText = EditorSetupStrings.DrainRateDescription,
Description = EditorSetupStrings.DrainRateDescription,
Current = new BindableFloat(Beatmap.Difficulty.DrainRate) Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
{ {
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0, MinValue = 0,
MaxValue = 10, MaxValue = 10,
Precision = 0.1f, Precision = 0.1f,
} },
TransferValueOnCommit = true,
TabbableContentContainer = this,
}, },
overallDifficultySlider = new LabelledSliderBar<float> overallDifficultySlider = new FormSliderBar<float>
{ {
Label = BeatmapsetsStrings.ShowStatsAccuracy, Caption = BeatmapsetsStrings.ShowStatsAccuracy,
FixedLabelWidth = LABEL_WIDTH, HintText = EditorSetupStrings.OverallDifficultyDescription,
Description = EditorSetupStrings.OverallDifficultyDescription,
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty) Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
{ {
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0, MinValue = 0,
MaxValue = 10, MaxValue = 10,
Precision = 0.1f, Precision = 0.1f,
} },
TransferValueOnCommit = true,
TabbableContentContainer = this,
}, },
baseVelocitySlider = new LabelledSliderBar<double> baseVelocitySlider = new FormSliderBar<double>
{ {
Label = EditorSetupStrings.BaseVelocity, Caption = EditorSetupStrings.BaseVelocity,
FixedLabelWidth = LABEL_WIDTH, HintText = EditorSetupStrings.BaseVelocityDescription,
Description = EditorSetupStrings.BaseVelocityDescription,
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier) Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
{ {
Default = 1.4, Default = 1.4,
MinValue = 0.4, MinValue = 0.4,
MaxValue = 3.6, MaxValue = 3.6,
Precision = 0.01f, Precision = 0.01f,
} },
TransferValueOnCommit = true,
TabbableContentContainer = this,
}, },
tickRateSlider = new LabelledSliderBar<double> tickRateSlider = new FormSliderBar<double>
{ {
Label = EditorSetupStrings.TickRate, Caption = EditorSetupStrings.TickRate,
FixedLabelWidth = LABEL_WIDTH, HintText = EditorSetupStrings.TickRateDescription,
Description = EditorSetupStrings.TickRateDescription,
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate) Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
{ {
Default = 1, Default = 1,
MinValue = 1, MinValue = 1,
MaxValue = 4, MaxValue = 4,
Precision = 1, Precision = 1,
} },
TransferValueOnCommit = true,
TabbableContentContainer = this,
}, },
}; };
foreach (var item in Children.OfType<LabelledSliderBar<float>>()) foreach (var item in Children.OfType<FormSliderBar<float>>())
item.Current.ValueChanged += _ => updateValues(); item.Current.ValueChanged += _ => updateValues();
foreach (var item in Children.OfType<LabelledSliderBar<double>>()) foreach (var item in Children.OfType<FormSliderBar<double>>())
item.Current.ValueChanged += _ => updateValues(); item.Current.ValueChanged += _ => updateValues();
} }

View File

@ -190,9 +190,12 @@ namespace osu.Game.Rulesets.Taiko
public override HitObjectComposer CreateHitObjectComposer() => new TaikoHitObjectComposer(this); public override HitObjectComposer CreateHitObjectComposer() => new TaikoHitObjectComposer(this);
public override IEnumerable<SetupSection> CreateEditorSetupSections() => public override IEnumerable<Drawable> CreateEditorSetupSections() =>
[ [
new MetadataSection(),
new TaikoDifficultySection(), new TaikoDifficultySection(),
new ResourcesSection(),
new DesignSection(),
]; ];
public override IBeatmapVerifier CreateBeatmapVerifier() => new TaikoBeatmapVerifier(); public override IBeatmapVerifier CreateBeatmapVerifier() => new TaikoBeatmapVerifier();

View File

@ -22,9 +22,9 @@ namespace osu.Game.Tests.Database
[HeadlessTest] [HeadlessTest]
public partial class BackgroundDataStoreProcessorTests : OsuTestScene, ILocalUserPlayInfo public partial class BackgroundDataStoreProcessorTests : OsuTestScene, ILocalUserPlayInfo
{ {
public IBindable<bool> IsPlaying => isPlaying; public IBindable<LocalUserPlayingState> PlayingState => isPlaying;
private readonly Bindable<bool> isPlaying = new Bindable<bool>(); private readonly Bindable<LocalUserPlayingState> isPlaying = new Bindable<LocalUserPlayingState>();
private BeatmapSetInfo importedSet = null!; private BeatmapSetInfo importedSet = null!;
@ -37,7 +37,7 @@ namespace osu.Game.Tests.Database
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {
AddStep("Set not playing", () => isPlaying.Value = false); AddStep("Set not playing", () => isPlaying.Value = LocalUserPlayingState.NotPlaying);
} }
[Test] [Test]
@ -89,7 +89,7 @@ namespace osu.Game.Tests.Database
}); });
}); });
AddStep("Set playing", () => isPlaying.Value = true); AddStep("Set playing", () => isPlaying.Value = LocalUserPlayingState.Playing);
AddStep("Reset difficulty", () => AddStep("Reset difficulty", () =>
{ {
@ -117,7 +117,7 @@ namespace osu.Game.Tests.Database
}); });
}); });
AddStep("Set not playing", () => isPlaying.Value = false); AddStep("Set not playing", () => isPlaying.Value = LocalUserPlayingState.NotPlaying);
AddUntilStep("wait for difficulties repopulated", () => AddUntilStep("wait for difficulties repopulated", () =>
{ {

View File

@ -3,11 +3,13 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Configuration; using osu.Framework.Configuration;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Input; using osu.Game.Input;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Input namespace osu.Game.Tests.Input
@ -15,9 +17,20 @@ namespace osu.Game.Tests.Input
[HeadlessTest] [HeadlessTest]
public partial class ConfineMouseTrackerTest : OsuGameTestScene public partial class ConfineMouseTrackerTest : OsuGameTestScene
{ {
private readonly Bindable<LocalUserPlayingState> playingState = new Bindable<LocalUserPlayingState>();
[Resolved] [Resolved]
private FrameworkConfigManager frameworkConfigManager { get; set; } = null!; private FrameworkConfigManager frameworkConfigManager { get; set; } = null!;
[SetUpSteps]
public override void SetUpSteps()
{
base.SetUpSteps();
// a bit dodgy.
AddStep("bind playing state", () => ((IBindable<LocalUserPlayingState>)playingState).BindTo(((ILocalUserPlayInfo)Game).PlayingState));
}
[TestCase(WindowMode.Windowed)] [TestCase(WindowMode.Windowed)]
[TestCase(WindowMode.Borderless)] [TestCase(WindowMode.Borderless)]
public void TestDisableConfining(WindowMode windowMode) public void TestDisableConfining(WindowMode windowMode)
@ -88,7 +101,7 @@ namespace osu.Game.Tests.Input
=> AddStep($"set {mode} game-side", () => Game.LocalConfig.SetValue(OsuSetting.ConfineMouseMode, mode)); => AddStep($"set {mode} game-side", () => Game.LocalConfig.SetValue(OsuSetting.ConfineMouseMode, mode));
private void setLocalUserPlayingTo(bool playing) private void setLocalUserPlayingTo(bool playing)
=> AddStep($"local user {(playing ? "playing" : "not playing")}", () => Game.LocalUserPlaying.Value = playing); => AddStep($"local user {(playing ? "playing" : "not playing")}", () => playingState.Value = playing ? LocalUserPlayingState.Playing : LocalUserPlayingState.NotPlaying);
private void gameSideModeIs(OsuConfineMouseMode mode) private void gameSideModeIs(OsuConfineMouseMode mode)
=> AddAssert($"mode is {mode} game-side", () => Game.LocalConfig.Get<OsuConfineMouseMode>(OsuSetting.ConfineMouseMode) == mode); => AddAssert($"mode is {mode} game-side", () => Game.LocalConfig.Get<OsuConfineMouseMode>(OsuSetting.ConfineMouseMode) == mode);

View File

@ -0,0 +1,123 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup;
using osu.Game.Skinning;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.Editing
{
[HeadlessTest]
public partial class TestSceneColoursSection : OsuManualInputManagerTestScene
{
[Test]
public void TestNoBeatmapSkinColours()
{
LegacyBeatmapSkin skin = null!;
ColoursSection coloursSection = null!;
AddStep("create beatmap skin", () => skin = new LegacyBeatmapSkin(new BeatmapInfo(), null));
AddStep("create colours section", () => Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies =
[
(typeof(EditorBeatmap), new EditorBeatmap(new Beatmap
{
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }
}, skin)),
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Aquamarine))
],
Child = coloursSection = new ColoursSection
{
RelativeSizeAxes = Axes.X,
}
});
AddAssert("beatmap skin has no colours", () => skin.Configuration.CustomComboColours, () => Is.Empty);
AddAssert("section displays default combo colours",
() => coloursSection.ChildrenOfType<FormColourPalette>().Single().Colours,
() => Is.EquivalentTo(new Colour4[]
{
SkinConfiguration.DefaultComboColours[1],
SkinConfiguration.DefaultComboColours[2],
SkinConfiguration.DefaultComboColours[3],
SkinConfiguration.DefaultComboColours[0],
}));
AddStep("add a colour", () => coloursSection.ChildrenOfType<FormColourPalette>().Single().Colours.Add(Colour4.Aqua));
AddAssert("beatmap skin has colours",
() => skin.Configuration.CustomComboColours,
() => Is.EquivalentTo(new[]
{
SkinConfiguration.DefaultComboColours[1],
SkinConfiguration.DefaultComboColours[2],
SkinConfiguration.DefaultComboColours[3],
Color4.Aqua,
SkinConfiguration.DefaultComboColours[0],
}));
}
[Test]
public void TestExistingColours()
{
LegacyBeatmapSkin skin = null!;
ColoursSection coloursSection = null!;
AddStep("create beatmap skin", () =>
{
skin = new LegacyBeatmapSkin(new BeatmapInfo(), null);
skin.Configuration.CustomComboColours = new List<Color4>
{
Color4.Azure,
Color4.Beige,
Color4.Chartreuse
};
});
AddStep("create colours section", () => Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies =
[
(typeof(EditorBeatmap), new EditorBeatmap(new Beatmap
{
BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo }
}, skin)),
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Aquamarine))
],
Child = coloursSection = new ColoursSection
{
RelativeSizeAxes = Axes.X,
}
});
AddAssert("section displays combo colours",
() => coloursSection.ChildrenOfType<FormColourPalette>().Single().Colours,
() => Is.EquivalentTo(new[]
{
Colour4.Beige,
Colour4.Chartreuse,
Colour4.Azure,
}));
AddStep("add a colour", () => coloursSection.ChildrenOfType<FormColourPalette>().Single().Colours.Add(Colour4.Aqua));
AddAssert("beatmap skin has colours",
() => skin.Configuration.CustomComboColours,
() => Is.EquivalentTo(new[]
{
Color4.Azure,
Color4.Beige,
Color4.Aqua,
Color4.Chartreuse
}));
}
}
}

View File

@ -8,6 +8,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -82,7 +83,7 @@ namespace osu.Game.Tests.Visual.Editing
} }
[Test] [Test]
public void TestNudgeSelection() public void TestNudgeSelectionTime()
{ {
HitCircle[] addedObjects = null!; HitCircle[] addedObjects = null!;
@ -103,6 +104,51 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("objects reverted to original position", () => addedObjects[0].StartTime == 100); AddAssert("objects reverted to original position", () => addedObjects[0].StartTime == 100);
} }
[Test]
public void TestNudgeSelectionPosition()
{
HitCircle addedObject = null!;
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[]
{
addedObject = new HitCircle { StartTime = 200, Position = new Vector2(100) },
}));
AddStep("select object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
AddStep("nudge up", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.Up);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddAssert("object position moved up", () => addedObject.Position.Y, () => Is.EqualTo(99).Within(Precision.FLOAT_EPSILON));
AddStep("nudge down", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.Down);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddAssert("object position moved down", () => addedObject.Position.Y, () => Is.EqualTo(100).Within(Precision.FLOAT_EPSILON));
AddStep("nudge left", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddAssert("object position moved left", () => addedObject.Position.X, () => Is.EqualTo(99).Within(Precision.FLOAT_EPSILON));
AddStep("nudge right", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.Right);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddAssert("object position moved right", () => addedObject.Position.X, () => Is.EqualTo(100).Within(Precision.FLOAT_EPSILON));
}
[Test] [Test]
public void TestRotateHotkeys() public void TestRotateHotkeys()
{ {

View File

@ -7,12 +7,14 @@ using System;
using System.Globalization; using System.Globalization;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Edit.Setup;
@ -25,6 +27,9 @@ namespace osu.Game.Tests.Visual.Editing
private TestDesignSection designSection; private TestDesignSection designSection;
private EditorBeatmap editorBeatmap { get; set; } private EditorBeatmap editorBeatmap { get; set; }
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
[SetUpSteps] [SetUpSteps]
public void SetUp() public void SetUp()
{ {
@ -42,7 +47,7 @@ namespace osu.Game.Tests.Visual.Editing
{ {
(typeof(EditorBeatmap), editorBeatmap) (typeof(EditorBeatmap), editorBeatmap)
}, },
Child = designSection = new TestDesignSection() Child = designSection = new TestDesignSection { RelativeSizeAxes = Axes.X }
}); });
} }
@ -99,11 +104,11 @@ namespace osu.Game.Tests.Visual.Editing
private partial class TestDesignSection : DesignSection private partial class TestDesignSection : DesignSection
{ {
public new LabelledSwitchButton EnableCountdown => base.EnableCountdown; public new FormCheckBox EnableCountdown => base.EnableCountdown;
public new FillFlowContainer CountdownSettings => base.CountdownSettings; public new FillFlowContainer CountdownSettings => base.CountdownSettings;
public new LabelledEnumDropdown<CountdownType> CountdownSpeed => base.CountdownSpeed; public new FormEnumDropdown<CountdownType> CountdownSpeed => base.CountdownSpeed;
public new LabelledNumberBox CountdownOffset => base.CountdownOffset; public new FormTextBox CountdownOffset => base.CountdownOffset;
} }
} }
} }

View File

@ -1,11 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Tests.Visual.Editing namespace osu.Game.Tests.Visual.Editing
@ -135,9 +137,42 @@ namespace osu.Game.Tests.Visual.Editing
pressAndCheckTime(Key.Up, 0); pressAndCheckTime(Key.Up, 0);
} }
private void pressAndCheckTime(Key key, double expectedTime) [Test]
public void TestSeekBetweenObjects()
{ {
AddStep($"press {key}", () => InputManager.Key(key)); AddStep("add objects", () =>
{
EditorBeatmap.Clear();
EditorBeatmap.AddRange(new[]
{
new HitCircle { StartTime = 1000, },
new HitCircle { StartTime = 2250, },
new HitCircle { StartTime = 3600, },
});
});
AddStep("seek to 0", () => EditorClock.Seek(0));
pressAndCheckTime(Key.Right, 1000, Key.ControlLeft);
pressAndCheckTime(Key.Right, 2250, Key.ControlLeft);
pressAndCheckTime(Key.Right, 3600, Key.ControlLeft);
pressAndCheckTime(Key.Right, 3600, Key.ControlLeft);
pressAndCheckTime(Key.Left, 2250, Key.ControlLeft);
pressAndCheckTime(Key.Left, 1000, Key.ControlLeft);
pressAndCheckTime(Key.Left, 1000, Key.ControlLeft);
}
private void pressAndCheckTime(Key key, double expectedTime, params Key[] modifiers)
{
AddStep($"press {key} with {(modifiers.Any() ? string.Join(',', modifiers) : "no modifiers")}", () =>
{
foreach (var modifier in modifiers)
InputManager.PressKey(modifier);
InputManager.Key(key);
foreach (var modifier in modifiers)
InputManager.ReleaseKey(modifier);
});
AddUntilStep($"time is {expectedTime}", () => EditorClock.CurrentTime, () => Is.EqualTo(expectedTime).Within(1)); AddUntilStep($"time is {expectedTime}", () => EditorClock.CurrentTime, () => Is.EqualTo(expectedTime).Within(1));
} }
} }

View File

@ -6,11 +6,13 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Edit.Setup;
@ -20,6 +22,9 @@ namespace osu.Game.Tests.Visual.Editing
{ {
public partial class TestSceneMetadataSection : OsuManualInputManagerTestScene public partial class TestSceneMetadataSection : OsuManualInputManagerTestScene
{ {
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
[Cached] [Cached]
private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap private EditorBeatmap editorBeatmap = new EditorBeatmap(new Beatmap
{ {
@ -201,7 +206,7 @@ namespace osu.Game.Tests.Visual.Editing
} }
private void createSection() private void createSection()
=> AddStep("create metadata section", () => Child = metadataSection = new TestMetadataSection()); => AddStep("create metadata section", () => Child = metadataSection = new TestMetadataSection { RelativeSizeAxes = Axes.X });
private void assertArtistMetadata(string expected) private void assertArtistMetadata(string expected)
=> AddAssert($"artist metadata is {expected}", () => editorBeatmap.Metadata.Artist, () => Is.EqualTo(expected)); => AddAssert($"artist metadata is {expected}", () => editorBeatmap.Metadata.Artist, () => Is.EqualTo(expected));
@ -226,11 +231,11 @@ namespace osu.Game.Tests.Visual.Editing
private partial class TestMetadataSection : MetadataSection private partial class TestMetadataSection : MetadataSection
{ {
public new LabelledTextBox ArtistTextBox => base.ArtistTextBox; public new FormTextBox ArtistTextBox => base.ArtistTextBox;
public new LabelledTextBox RomanisedArtistTextBox => base.RomanisedArtistTextBox; public new FormTextBox RomanisedArtistTextBox => base.RomanisedArtistTextBox;
public new LabelledTextBox TitleTextBox => base.TitleTextBox; public new FormTextBox TitleTextBox => base.TitleTextBox;
public new LabelledTextBox RomanisedTitleTextBox => base.RomanisedTitleTextBox; public new FormTextBox RomanisedTitleTextBox => base.RomanisedTitleTextBox;
} }
} }
} }

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -12,7 +10,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
public partial class TestSceneOverlayActivation : OsuPlayerTestScene public partial class TestSceneOverlayActivation : OsuPlayerTestScene
{ {
protected new OverlayTestPlayer Player => base.Player as OverlayTestPlayer; protected new OverlayTestPlayer Player => (OverlayTestPlayer)base.Player;
public override void SetUpSteps() public override void SetUpSteps()
{ {

View File

@ -25,14 +25,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Cached(typeof(ILocalUserPlayInfo))] [Cached(typeof(ILocalUserPlayInfo))]
private ILocalUserPlayInfo localUserInfo; private ILocalUserPlayInfo localUserInfo;
private readonly Bindable<bool> localUserPlaying = new Bindable<bool>(); private readonly Bindable<LocalUserPlayingState> playingState = new Bindable<LocalUserPlayingState>();
private TextBox textBox => chatDisplay.ChildrenOfType<TextBox>().First(); private TextBox textBox => chatDisplay.ChildrenOfType<TextBox>().First();
public TestSceneGameplayChatDisplay() public TestSceneGameplayChatDisplay()
{ {
var mockLocalUserInfo = new Mock<ILocalUserPlayInfo>(); var mockLocalUserInfo = new Mock<ILocalUserPlayInfo>();
mockLocalUserInfo.SetupGet(i => i.IsPlaying).Returns(localUserPlaying); mockLocalUserInfo.SetupGet(i => i.PlayingState).Returns(playingState);
localUserInfo = mockLocalUserInfo.Object; localUserInfo = mockLocalUserInfo.Object;
} }
@ -124,6 +124,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert($"chat {(isFocused ? "focused" : "not focused")}", () => textBox.HasFocus == isFocused); AddAssert($"chat {(isFocused ? "focused" : "not focused")}", () => textBox.HasFocus == isFocused);
private void setLocalUserPlaying(bool playing) => private void setLocalUserPlaying(bool playing) =>
AddStep($"local user {(playing ? "playing" : "not playing")}", () => localUserPlaying.Value = playing); AddStep($"local user {(playing ? "playing" : "not playing")}", () => playingState.Value = playing ? LocalUserPlayingState.Playing : LocalUserPlayingState.NotPlaying);
} }
} }

View File

@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select EZ mod", () => AddStep("select EZ mod", () =>
{ {
var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull(); var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull();
SelectedMods.Value = new[] { ruleset.CreateMod<ModEasy>() }; advancedStats.Mods.Value = new[] { ruleset.CreateMod<ModEasy>() };
}); });
AddAssert("circle size bar is blue", () => barIsBlue(advancedStats.FirstValue)); AddAssert("circle size bar is blue", () => barIsBlue(advancedStats.FirstValue));
@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep("select HR mod", () => AddStep("select HR mod", () =>
{ {
var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull(); var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull();
SelectedMods.Value = new[] { ruleset.CreateMod<ModHardRock>() }; advancedStats.Mods.Value = new[] { ruleset.CreateMod<ModHardRock>() };
}); });
AddAssert("circle size bar is red", () => barIsRed(advancedStats.FirstValue)); AddAssert("circle size bar is red", () => barIsRed(advancedStats.FirstValue));
@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.SongSelect
var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull(); var ruleset = advancedStats.BeatmapInfo.Ruleset.CreateInstance().AsNonNull();
var difficultyAdjustMod = ruleset.CreateMod<ModDifficultyAdjust>().AsNonNull(); var difficultyAdjustMod = ruleset.CreateMod<ModDifficultyAdjust>().AsNonNull();
difficultyAdjustMod.ReadFromDifficulty(advancedStats.BeatmapInfo.Difficulty); difficultyAdjustMod.ReadFromDifficulty(advancedStats.BeatmapInfo.Difficulty);
SelectedMods.Value = new[] { difficultyAdjustMod }; advancedStats.Mods.Value = new[] { difficultyAdjustMod };
}); });
AddAssert("circle size bar is white", () => barIsWhite(advancedStats.FirstValue)); AddAssert("circle size bar is white", () => barIsWhite(advancedStats.FirstValue));
@ -143,7 +143,7 @@ namespace osu.Game.Tests.Visual.SongSelect
difficultyAdjustMod.ReadFromDifficulty(originalDifficulty); difficultyAdjustMod.ReadFromDifficulty(originalDifficulty);
difficultyAdjustMod.DrainRate.Value = originalDifficulty.DrainRate - 0.5f; difficultyAdjustMod.DrainRate.Value = originalDifficulty.DrainRate - 0.5f;
difficultyAdjustMod.ApproachRate.Value = originalDifficulty.ApproachRate + 2.2f; difficultyAdjustMod.ApproachRate.Value = originalDifficulty.ApproachRate + 2.2f;
SelectedMods.Value = new[] { difficultyAdjustMod }; advancedStats.Mods.Value = new[] { difficultyAdjustMod };
}); });
AddAssert("circle size bar is white", () => barIsWhite(advancedStats.FirstValue)); AddAssert("circle size bar is white", () => barIsWhite(advancedStats.FirstValue));

View File

@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual.UserInterface
}, },
new FormSliderBar<float> new FormSliderBar<float>
{ {
Caption = "Instantaneous slider", Caption = "Slider",
Current = new BindableFloat Current = new BindableFloat
{ {
MinValue = 0, MinValue = 0,
@ -82,19 +82,6 @@ namespace osu.Game.Tests.Visual.UserInterface
}, },
TabbableContentContainer = this, TabbableContentContainer = this,
}, },
new FormSliderBar<float>
{
Caption = "Non-instantaneous slider",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 10,
Value = 5,
Precision = 0.1f,
},
Instantaneous = false,
TabbableContentContainer = this,
},
new FormEnumDropdown<CountdownType> new FormEnumDropdown<CountdownType>
{ {
Caption = EditorSetupStrings.EnableCountdown, Caption = EditorSetupStrings.EnableCountdown,
@ -105,6 +92,17 @@ namespace osu.Game.Tests.Visual.UserInterface
Caption = "Audio file", Caption = "Audio file",
PlaceholderText = "Select an audio file", PlaceholderText = "Select an audio file",
}, },
new FormColourPalette
{
Caption = "Combo colours",
Colours =
{
Colour4.Red,
Colour4.Green,
Colour4.Blue,
Colour4.Yellow,
}
},
}, },
}, },
} }

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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneFormSliderBar : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
[Test]
public void TestTransferValueOnCommit()
{
OsuSpriteText text;
FormSliderBar<float> slider = null!;
AddStep("create content", () =>
{
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
Children = new Drawable[]
{
text = new OsuSpriteText(),
slider = new FormSliderBar<float>
{
Caption = "Slider",
Current = new BindableFloat
{
MinValue = 0,
MaxValue = 10,
Precision = 0.1f,
Default = 5f,
}
},
}
};
slider.Current.BindValueChanged(_ => text.Text = $"Current value is: {slider.Current.Value}", true);
});
AddToggleStep("toggle transfer value on commit", b =>
{
if (slider.IsNotNull())
slider.TransferValueOnCommit = b;
});
}
}
}

View File

@ -2,18 +2,16 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Dialog;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
namespace osu.Game.Tournament.Screens.Editors.Components namespace osu.Game.Tournament.Screens.Editors.Components
{ {
public partial class DeleteRoundDialog : DangerousActionDialog public partial class DeleteRoundDialog : DeletionDialog
{ {
public DeleteRoundDialog(TournamentRound round, Action action) public DeleteRoundDialog(TournamentRound round, Action action)
{ {
HeaderText = round.Name.Value.Length > 0 ? $@"Delete round ""{round.Name.Value}""?" : @"Delete unnamed round?"; HeaderText = round.Name.Value.Length > 0 ? $@"Delete round ""{round.Name.Value}""?" : @"Delete unnamed round?";
Icon = FontAwesome.Solid.Trash;
DangerousAction = action; DangerousAction = action;
} }
} }

View File

@ -2,20 +2,18 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Dialog;
using osu.Game.Tournament.Models; using osu.Game.Tournament.Models;
namespace osu.Game.Tournament.Screens.Editors.Components namespace osu.Game.Tournament.Screens.Editors.Components
{ {
public partial class DeleteTeamDialog : DangerousActionDialog public partial class DeleteTeamDialog : DeletionDialog
{ {
public DeleteTeamDialog(TournamentTeam team, Action action) public DeleteTeamDialog(TournamentTeam team, Action action)
{ {
HeaderText = team.FullName.Value.Length > 0 ? $@"Delete team ""{team.FullName.Value}""?" : HeaderText = team.FullName.Value.Length > 0 ? $@"Delete team ""{team.FullName.Value}""?" :
team.Acronym.Value.Length > 0 ? $@"Delete team ""{team.Acronym.Value}""?" : team.Acronym.Value.Length > 0 ? $@"Delete team ""{team.Acronym.Value}""?" :
@"Delete unnamed team?"; @"Delete unnamed team?";
Icon = FontAwesome.Solid.Trash;
DangerousAction = action; DangerousAction = action;
} }
} }

View File

@ -8,7 +8,7 @@ using osu.Game.Overlays.Dialog;
namespace osu.Game.Collections namespace osu.Game.Collections
{ {
public partial class DeleteCollectionDialog : DangerousActionDialog public partial class DeleteCollectionDialog : DeletionDialog
{ {
public DeleteCollectionDialog(Live<BeatmapCollection> collection, Action deleteAction) public DeleteCollectionDialog(Live<BeatmapCollection> collection, Action deleteAction)
{ {

View File

@ -606,7 +606,7 @@ namespace osu.Game.Database
{ {
// Importantly, also sleep if high performance session is active. // Importantly, also sleep if high performance session is active.
// If we don't do this, memory usage can become runaway due to GC running in a more lenient mode. // If we don't do this, memory usage can become runaway due to GC running in a more lenient mode.
while (localUserPlayInfo?.IsPlaying.Value == true || highPerformanceSessionManager?.IsSessionActive == true) while (localUserPlayInfo?.PlayingState.Value != LocalUserPlayingState.NotPlaying || highPerformanceSessionManager?.IsSessionActive == true)
{ {
Logger.Log("Background processing sleeping due to active gameplay..."); Logger.Log("Background processing sleeping due to active gameplay...");
Thread.Sleep(TimeToSleepDuringGameplay); Thread.Sleep(TimeToSleepDuringGameplay);

View File

@ -44,7 +44,7 @@ namespace osu.Game.Database
{ {
if (changes == null) if (changes == null)
{ {
if (detachedBeatmapSets.Count > 0 && sender.Count == 0) if (sender is RealmResetEmptySet<BeatmapSetInfo>)
{ {
// Usually we'd reset stuff here, but doing so triggers a silly flow which ends up deadlocking realm. // Usually we'd reset stuff here, but doing so triggers a silly flow which ends up deadlocking realm.
// Additionally, user should not be at song select when realm is blocking all operations in the first place. // Additionally, user should not be at song select when realm is blocking all operations in the first place.

View File

@ -568,7 +568,7 @@ namespace osu.Game.Database
lock (notificationsResetMap) lock (notificationsResetMap)
{ {
// Store an action which is used when blocking to ensure consumers don't use results of a stale changeset firing. // Store an action which is used when blocking to ensure consumers don't use results of a stale changeset firing.
notificationsResetMap.Add(action, () => callback(new EmptyRealmSet<T>(), null)); notificationsResetMap.Add(action, () => callback(new RealmResetEmptySet<T>(), null));
} }
return RegisterCustomSubscription(action); return RegisterCustomSubscription(action);

View File

@ -12,7 +12,13 @@ using Realms.Schema;
namespace osu.Game.Database namespace osu.Game.Database
{ {
public class EmptyRealmSet<T> : IRealmCollection<T> /// <summary>
/// This can arrive in <see cref="RealmAccess.RegisterForNotifications{T}"/> callbacks to imply that realm access has been reset.
/// </summary>
/// <remarks>
/// Usually implies that the original database may return soon and the callback can usually be silently ignored.
///</remarks>
public class RealmResetEmptySet<T> : IRealmCollection<T>
{ {
private IList<T> emptySet => Array.Empty<T>(); private IList<T> emptySet => Array.Empty<T>();

View File

@ -6,6 +6,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -26,6 +27,8 @@ namespace osu.Game.Graphics.UserInterface
public const int TEXT_SIZE = 17; public const int TEXT_SIZE = 17;
public const int TRANSITION_LENGTH = 80; public const int TRANSITION_LENGTH = 80;
public BindableBool ShowCheckbox { get; } = new BindableBool();
private TextContainer text; private TextContainer text;
private HotkeyDisplay hotkey; private HotkeyDisplay hotkey;
private HoverClickSounds hoverClickSounds; private HoverClickSounds hoverClickSounds;
@ -72,6 +75,7 @@ namespace osu.Game.Graphics.UserInterface
{ {
base.LoadComplete(); base.LoadComplete();
ShowCheckbox.BindValueChanged(_ => updateState());
Item.Action.BindDisabledChanged(_ => updateState(), true); Item.Action.BindDisabledChanged(_ => updateState(), true);
FinishTransforms(); FinishTransforms();
} }
@ -138,6 +142,8 @@ namespace osu.Game.Graphics.UserInterface
text.BoldText.FadeOut(TRANSITION_LENGTH, Easing.OutQuint); text.BoldText.FadeOut(TRANSITION_LENGTH, Easing.OutQuint);
text.NormalText.FadeIn(TRANSITION_LENGTH, Easing.OutQuint); text.NormalText.FadeIn(TRANSITION_LENGTH, Easing.OutQuint);
} }
text.CheckboxContainer.Alpha = ShowCheckbox.Value ? 1 : 0;
} }
protected sealed override Drawable CreateContent() => text = CreateTextContainer(); protected sealed override Drawable CreateContent() => text = CreateTextContainer();

View File

@ -42,6 +42,25 @@ namespace osu.Game.Graphics.UserInterface
sampleClose = audio.Samples.Get(@"UI/dropdown-close"); sampleClose = audio.Samples.Get(@"UI/dropdown-close");
} }
protected override void Update()
{
base.Update();
bool showCheckboxes = false;
foreach (var drawableItem in ItemsContainer)
{
if (drawableItem.Item is StatefulMenuItem)
showCheckboxes = true;
}
foreach (var drawableItem in ItemsContainer)
{
if (drawableItem is DrawableOsuMenuItem osuItem)
osuItem.ShowCheckbox.Value = showCheckboxes;
}
}
protected override void AnimateOpen() protected override void AnimateOpen()
{ {
if (!TopLevelMenu && !wasOpened) if (!TopLevelMenu && !wasOpened)

View File

@ -0,0 +1,238 @@
// 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.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Resources.Localisation.Web;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterfaceV2
{
public partial class FormColourPalette : CompositeDrawable
{
public BindableList<Colour4> Colours { get; } = new BindableList<Colour4>();
public LocalisableString Caption { get; init; }
public LocalisableString HintText { get; init; }
private Box background = null!;
private FormFieldCaption caption = null!;
private FillFlowContainer flow = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Masking = true;
CornerRadius = 5;
RoundedButton button;
InternalChildren = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(9),
Spacing = new Vector2(7),
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
caption = new FormFieldCaption
{
Caption = Caption,
TooltipText = HintText,
},
flow = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Full,
Spacing = new Vector2(5),
Child = button = new RoundedButton
{
Action = addNewColour,
Size = new Vector2(70),
Text = "+",
}
}
},
},
};
flow.SetLayoutPosition(button, float.MaxValue);
}
protected override void LoadComplete()
{
base.LoadComplete();
Colours.BindCollectionChanged((_, args) =>
{
if (args.Action != NotifyCollectionChangedAction.Replace)
updateColours();
}, true);
updateState();
}
protected override bool OnHover(HoverEvent e)
{
updateState();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
updateState();
}
private void addNewColour()
{
Color4 startingColour = Colours.Count > 0
? Colours.Last()
: Colour4.White;
Colours.Add(startingColour);
flow.OfType<ColourButton>().Last().TriggerClick();
}
private void updateState()
{
background.Colour = colourProvider.Background5;
caption.Colour = colourProvider.Content2;
BorderThickness = IsHovered ? 2 : 0;
if (IsHovered)
BorderColour = colourProvider.Light4;
}
private void updateColours()
{
flow.RemoveAll(d => d is ColourButton, true);
for (int i = 0; i < Colours.Count; ++i)
{
// copy to avoid accesses to modified closure.
int colourIndex = i;
var colourButton = new ColourButton { Current = { Value = Colours[colourIndex] } };
colourButton.Current.BindValueChanged(colour => Colours[colourIndex] = colour.NewValue);
colourButton.DeleteRequested = () => Colours.RemoveAt(colourIndex);
flow.Add(colourButton);
}
}
private partial class ColourButton : OsuClickableContainer, IHasPopover, IHasContextMenu
{
public Bindable<Colour4> Current { get; } = new Bindable<Colour4>();
public Action? DeleteRequested { get; set; }
private Box background = null!;
private OsuSpriteText hexCode = null!;
[BackgroundDependencyLoader]
private void load()
{
Size = new Vector2(70);
Masking = true;
CornerRadius = 35;
Action = this.ShowPopover;
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
},
hexCode = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindValueChanged(_ => updateState(), true);
}
public Popover GetPopover() => new ColourPickerPopover
{
Current = { BindTarget = Current }
};
public MenuItem[] ContextMenuItems => new MenuItem[]
{
new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => DeleteRequested?.Invoke())
};
private void updateState()
{
background.Colour = Current.Value;
hexCode.Text = Current.Value.ToHex();
hexCode.Colour = OsuColour.ForegroundTextColourFor(Current.Value);
}
}
private partial class ColourPickerPopover : OsuPopover, IHasCurrentValue<Colour4>
{
public Bindable<Colour4> Current
{
get => current.Current;
set => current.Current = value;
}
private readonly BindableWithCurrent<Colour4> current = new BindableWithCurrent<Colour4>();
public ColourPickerPopover()
: base(false)
{
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
Child = new OsuColourPicker
{
Current = { BindTarget = Current }
};
Body.BorderThickness = 2;
Body.BorderColour = colourProvider.Highlight1;
Content.Padding = new MarginPadding(2);
}
}
}
}

View File

@ -68,6 +68,8 @@ namespace osu.Game.Graphics.UserInterfaceV2
/// </summary> /// </summary>
public LocalisableString PlaceholderText { get; init; } public LocalisableString PlaceholderText { get; init; }
public Container PreviewContainer { get; private set; } = null!;
private Box background = null!; private Box background = null!;
private FormFieldCaption caption = null!; private FormFieldCaption caption = null!;
@ -89,7 +91,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
private void load() private void load()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = 50; AutoSizeAxes = Axes.Y;
Masking = true; Masking = true;
CornerRadius = 5; CornerRadius = 5;
@ -101,9 +103,23 @@ namespace osu.Game.Graphics.UserInterfaceV2
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5, Colour = colourProvider.Background5,
}, },
PreviewContainer = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding
{
Horizontal = 1.5f,
Top = 1.5f,
Bottom = 50
},
},
new Container new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.X,
Height = 50,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Padding = new MarginPadding(9), Padding = new MarginPadding(9),
Children = new Drawable[] Children = new Drawable[]
{ {
@ -148,12 +164,13 @@ namespace osu.Game.Graphics.UserInterfaceV2
base.LoadComplete(); base.LoadComplete();
popoverState.BindValueChanged(_ => updateState()); popoverState.BindValueChanged(_ => updateState());
current.BindDisabledChanged(_ => updateState());
current.BindValueChanged(_ => current.BindValueChanged(_ =>
{ {
updateState(); updateState();
onFileSelected(); onFileSelected();
}); }, true);
current.BindDisabledChanged(_ => updateState(), true); FinishTransforms(true);
game.RegisterImportHandler(this); game.RegisterImportHandler(this);
} }
@ -189,7 +206,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
private void updateState() private void updateState()
{ {
caption.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2; caption.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2;
filenameText.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; filenameText.Colour = Current.Disabled || Current.Value == null ? colourProvider.Foreground1 : colourProvider.Content1;
if (!Current.Disabled) if (!Current.Disabled)
{ {

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Globalization;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
{ {
public partial class FormNumberBox : FormTextBox public partial class FormNumberBox : FormTextBox
@ -10,6 +12,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
internal override InnerTextBox CreateTextBox() => new InnerNumberBox internal override InnerTextBox CreateTextBox() => new InnerNumberBox
{ {
AllowDecimals = AllowDecimals, AllowDecimals = AllowDecimals,
SelectAllOnFocus = true,
}; };
internal partial class InnerNumberBox : InnerTextBox internal partial class InnerNumberBox : InnerTextBox
@ -17,7 +20,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
public bool AllowDecimals { get; init; } public bool AllowDecimals { get; init; }
protected override bool CanAddCharacter(char character) protected override bool CanAddCharacter(char character)
=> char.IsAsciiDigit(character) || (AllowDecimals && character == '.'); => char.IsAsciiDigit(character) || (AllowDecimals && CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator.Contains(character));
} }
} }
} }

View File

@ -17,6 +17,7 @@ using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Overlays; using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterfaceV2 namespace osu.Game.Graphics.UserInterfaceV2
@ -27,27 +28,23 @@ namespace osu.Game.Graphics.UserInterfaceV2
public Bindable<T> Current public Bindable<T> Current
{ {
get => current.Current; get => current.Current;
set => current.Current = value;
}
private bool instantaneous = true;
/// <summary>
/// Whether changes to the slider should instantaneously transfer to the text box (and vice versa).
/// If <see langword="false"/>, the transfer will happen on text box commit (explicit, or implicit via focus loss), or on slider drag end.
/// </summary>
public bool Instantaneous
{
get => instantaneous;
set set
{ {
instantaneous = value; current.Current = value;
currentNumberInstantaneous.Default = current.Default;
if (slider.IsNotNull())
slider.TransferValueOnCommit = !instantaneous;
} }
} }
private readonly BindableNumberWithCurrent<T> current = new BindableNumberWithCurrent<T>();
private readonly BindableNumber<T> currentNumberInstantaneous = new BindableNumber<T>();
/// <summary>
/// Whether changes to the value should instantaneously transfer to outside bindables.
/// If <see langword="false"/>, the transfer will happen on text box commit (explicit, or implicit via focus loss), or on slider commit.
/// </summary>
public bool TransferValueOnCommit { get; set; }
private CompositeDrawable? tabbableContentContainer; private CompositeDrawable? tabbableContentContainer;
public CompositeDrawable? TabbableContentContainer public CompositeDrawable? TabbableContentContainer
@ -61,8 +58,6 @@ namespace osu.Game.Graphics.UserInterfaceV2
} }
} }
private readonly BindableNumberWithCurrent<T> current = new BindableNumberWithCurrent<T>();
/// <summary> /// <summary>
/// Caption describing this slider bar, displayed on top of the controls. /// Caption describing this slider bar, displayed on top of the controls.
/// </summary> /// </summary>
@ -83,8 +78,10 @@ namespace osu.Game.Graphics.UserInterfaceV2
[Resolved] [Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!; private OverlayColourProvider colourProvider { get; set; } = null!;
private readonly Bindable<Language> currentLanguage = new Bindable<Language>();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours, OsuGame? game)
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = 50; Height = 50;
@ -107,7 +104,12 @@ namespace osu.Game.Graphics.UserInterfaceV2
new Container new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(9), Padding = new MarginPadding
{
Vertical = 9,
Left = 9,
Right = 5,
},
Children = new Drawable[] Children = new Drawable[]
{ {
caption = new FormFieldCaption caption = new FormFieldCaption
@ -139,12 +141,15 @@ namespace osu.Game.Graphics.UserInterfaceV2
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Width = 0.5f, Width = 0.5f,
Current = Current, Current = currentNumberInstantaneous,
TransferValueOnCommit = !instantaneous, OnCommit = () => current.Value = currentNumberInstantaneous.Value,
} }
}, },
}, },
}; };
if (game != null)
currentLanguage.BindTo(game.CurrentLanguage);
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -159,10 +164,30 @@ namespace osu.Game.Graphics.UserInterfaceV2
slider.IsDragging.BindValueChanged(_ => updateState()); slider.IsDragging.BindValueChanged(_ => updateState());
current.BindValueChanged(_ => current.ValueChanged += e => currentNumberInstantaneous.Value = e.NewValue;
current.MinValueChanged += v => currentNumberInstantaneous.MinValue = v;
current.MaxValueChanged += v => currentNumberInstantaneous.MaxValue = v;
current.PrecisionChanged += v => currentNumberInstantaneous.Precision = v;
current.DisabledChanged += disabled =>
{ {
if (disabled)
{
// revert any changes before disabling to make sure we are in a consistent state.
currentNumberInstantaneous.Value = current.Value;
}
currentNumberInstantaneous.Disabled = disabled;
};
current.CopyTo(currentNumberInstantaneous);
currentLanguage.BindValueChanged(_ => Schedule(updateValueDisplay));
currentNumberInstantaneous.BindValueChanged(e =>
{
if (!TransferValueOnCommit)
current.Value = e.NewValue;
updateState(); updateState();
updateTextBoxFromSlider(); updateValueDisplay();
}, true); }, true);
} }
@ -170,17 +195,15 @@ namespace osu.Game.Graphics.UserInterfaceV2
private void textChanged(ValueChangedEvent<string> change) private void textChanged(ValueChangedEvent<string> change)
{ {
if (!instantaneous) return;
tryUpdateSliderFromTextBox(); tryUpdateSliderFromTextBox();
} }
private void textCommitted(TextBox t, bool isNew) private void textCommitted(TextBox t, bool isNew)
{ {
tryUpdateSliderFromTextBox(); tryUpdateSliderFromTextBox();
// If the attempted update above failed, restore text box to match the slider. // If the attempted update above failed, restore text box to match the slider.
Current.TriggerChange(); currentNumberInstantaneous.TriggerChange();
current.Value = currentNumberInstantaneous.Value;
flashLayer.Colour = ColourInfo.GradientVertical(colourProvider.Dark2.Opacity(0), colourProvider.Dark2); flashLayer.Colour = ColourInfo.GradientVertical(colourProvider.Dark2.Opacity(0), colourProvider.Dark2);
flashLayer.FadeOutFromOne(800, Easing.OutQuint); flashLayer.FadeOutFromOne(800, Easing.OutQuint);
@ -192,7 +215,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
try try
{ {
switch (Current) switch (currentNumberInstantaneous)
{ {
case Bindable<int> bindableInt: case Bindable<int> bindableInt:
bindableInt.Value = int.Parse(textBox.Current.Value); bindableInt.Value = int.Parse(textBox.Current.Value);
@ -203,7 +226,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
break; break;
default: default:
Current.Parse(textBox.Current.Value, CultureInfo.CurrentCulture); currentNumberInstantaneous.Parse(textBox.Current.Value, CultureInfo.CurrentCulture);
break; break;
} }
} }
@ -238,9 +261,9 @@ namespace osu.Game.Graphics.UserInterfaceV2
{ {
textBox.Alpha = 1; textBox.Alpha = 1;
background.Colour = Current.Disabled ? colourProvider.Background4 : colourProvider.Background5; background.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Background4 : colourProvider.Background5;
caption.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content2; caption.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Foreground1 : colourProvider.Content2;
textBox.Colour = Current.Disabled ? colourProvider.Foreground1 : colourProvider.Content1; textBox.Colour = currentNumberInstantaneous.Disabled ? colourProvider.Foreground1 : colourProvider.Content1;
BorderThickness = IsHovered || textBox.Focused.Value || slider.IsDragging.Value ? 2 : 0; BorderThickness = IsHovered || textBox.Focused.Value || slider.IsDragging.Value ? 2 : 0;
BorderColour = textBox.Focused.Value ? colourProvider.Highlight1 : colourProvider.Light4; BorderColour = textBox.Focused.Value ? colourProvider.Highlight1 : colourProvider.Light4;
@ -253,16 +276,17 @@ namespace osu.Game.Graphics.UserInterfaceV2
background.Colour = colourProvider.Background5; background.Colour = colourProvider.Background5;
} }
private void updateTextBoxFromSlider() private void updateValueDisplay()
{ {
if (updatingFromTextBox) return; if (updatingFromTextBox) return;
textBox.Text = slider.GetDisplayableValue(Current.Value).ToString(); textBox.Text = slider.GetDisplayableValue(currentNumberInstantaneous.Value).ToString();
} }
private partial class Slider : OsuSliderBar<T> private partial class Slider : OsuSliderBar<T>
{ {
public BindableBool IsDragging { get; set; } = new BindableBool(); public BindableBool IsDragging { get; set; } = new BindableBool();
public Action? OnCommit { get; set; }
private Box leftBox = null!; private Box leftBox = null!;
private Box rightBox = null!; private Box rightBox = null!;
@ -369,6 +393,16 @@ namespace osu.Game.Graphics.UserInterfaceV2
{ {
nub.MoveToX(value, 200, Easing.OutPow10); nub.MoveToX(value, 200, Easing.OutPow10);
} }
protected override bool Commit()
{
bool result = base.Commit();
if (result)
OnCommit?.Invoke();
return result;
}
} }
} }
} }

View File

@ -202,6 +202,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
} }
else else
{ {
BorderThickness = 0;
background.Colour = colourProvider.Background4; background.Colour = colourProvider.Background4;
} }
} }

View File

@ -15,7 +15,7 @@ namespace osu.Game.Input
{ {
/// <summary> /// <summary>
/// Connects <see cref="OsuSetting.ConfineMouseMode"/> with <see cref="FrameworkSetting.ConfineMouseMode"/>. /// Connects <see cref="OsuSetting.ConfineMouseMode"/> with <see cref="FrameworkSetting.ConfineMouseMode"/>.
/// If <see cref="OsuGame.LocalUserPlaying"/> is true, we should also confine the mouse cursor if it has been /// If <see cref="ILocalUserPlayInfo.PlayingState"/> is playing, we should also confine the mouse cursor if it has been
/// requested with <see cref="OsuConfineMouseMode.DuringGameplay"/>. /// requested with <see cref="OsuConfineMouseMode.DuringGameplay"/>.
/// </summary> /// </summary>
public partial class ConfineMouseTracker : Component public partial class ConfineMouseTracker : Component
@ -25,7 +25,7 @@ namespace osu.Game.Input
private Bindable<bool> frameworkMinimiseOnFocusLossInFullscreen; private Bindable<bool> frameworkMinimiseOnFocusLossInFullscreen;
private Bindable<OsuConfineMouseMode> osuConfineMode; private Bindable<OsuConfineMouseMode> osuConfineMode;
private IBindable<bool> localUserPlaying; private IBindable<LocalUserPlayingState> localUserPlaying;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ILocalUserPlayInfo localUserInfo, FrameworkConfigManager frameworkConfigManager, OsuConfigManager osuConfigManager) private void load(ILocalUserPlayInfo localUserInfo, FrameworkConfigManager frameworkConfigManager, OsuConfigManager osuConfigManager)
@ -37,7 +37,7 @@ namespace osu.Game.Input
frameworkMinimiseOnFocusLossInFullscreen.BindValueChanged(_ => updateConfineMode()); frameworkMinimiseOnFocusLossInFullscreen.BindValueChanged(_ => updateConfineMode());
osuConfineMode = osuConfigManager.GetBindable<OsuConfineMouseMode>(OsuSetting.ConfineMouseMode); osuConfineMode = osuConfigManager.GetBindable<OsuConfineMouseMode>(OsuSetting.ConfineMouseMode);
localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy(); localUserPlaying = localUserInfo.PlayingState.GetBoundCopy();
osuConfineMode.ValueChanged += _ => updateConfineMode(); osuConfineMode.ValueChanged += _ => updateConfineMode();
localUserPlaying.BindValueChanged(_ => updateConfineMode(), true); localUserPlaying.BindValueChanged(_ => updateConfineMode(), true);
@ -63,7 +63,7 @@ namespace osu.Game.Input
break; break;
case OsuConfineMouseMode.DuringGameplay: case OsuConfineMouseMode.DuringGameplay:
frameworkConfineMode.Value = localUserPlaying.Value ? ConfineMouseMode.Always : ConfineMouseMode.Never; frameworkConfineMode.Value = localUserPlaying.Value == LocalUserPlayingState.Playing ? ConfineMouseMode.Always : ConfineMouseMode.Never;
break; break;
case OsuConfineMouseMode.Always: case OsuConfineMouseMode.Always:

View File

@ -3,15 +3,16 @@
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Game.Screens.Play;
using osuTK.Input; using osuTK.Input;
namespace osu.Game.Input namespace osu.Game.Input
{ {
public partial class OsuUserInputManager : UserInputManager public partial class OsuUserInputManager : UserInputManager
{ {
protected override bool AllowRightClickFromLongTouch => !LocalUserPlaying.Value; protected override bool AllowRightClickFromLongTouch => PlayingState.Value != LocalUserPlayingState.Playing;
public readonly BindableBool LocalUserPlaying = new BindableBool(); public readonly IBindable<LocalUserPlayingState> PlayingState = new Bindable<LocalUserPlayingState>();
internal OsuUserInputManager() internal OsuUserInputManager()
{ {

View File

@ -12,7 +12,12 @@ namespace osu.Game.Localisation
/// <summary> /// <summary>
/// "Caution" /// "Caution"
/// </summary> /// </summary>
public static LocalisableString Caution => new TranslatableString(getKey(@"header_text"), @"Caution"); public static LocalisableString CautionHeaderText => new TranslatableString(getKey(@"header_text"), @"Caution");
/// <summary>
/// "Are you sure you want to delete the following:"
/// </summary>
public static LocalisableString DeletionHeaderText => new TranslatableString(getKey(@"deletion_header_text"), @"Are you sure you want to delete the following:");
/// <summary> /// <summary>
/// "Yes. Go for it." /// "Yes. Go for it."

View File

@ -46,7 +46,7 @@ namespace osu.Game.Online.Chat
{ {
public ExternalLinkDialog(string url, Action openExternalLinkAction, Action copyExternalLinkAction) public ExternalLinkDialog(string url, Action openExternalLinkAction, Action copyExternalLinkAction)
{ {
HeaderText = DialogStrings.Caution; HeaderText = DialogStrings.CautionHeaderText;
BodyText = $"Are you sure you want to open the following link in a web browser?\n\n{url}"; BodyText = $"Are you sure you want to open the following link in a web browser?\n\n{url}";
Icon = FontAwesome.Solid.ExclamationTriangle; Icon = FontAwesome.Solid.ExclamationTriangle;

View File

@ -205,7 +205,6 @@ namespace osu.Game.Online.Chat
protected partial class StandAloneMessage : ChatLine protected partial class StandAloneMessage : ChatLine
{ {
protected override float FontSize => 13;
protected override float Spacing => 5; protected override float Spacing => 5;
protected override float UsernameWidth => 90; protected override float UsernameWidth => 90;

View File

@ -175,14 +175,9 @@ namespace osu.Game
/// </summary> /// </summary>
public readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>(); public readonly IBindable<OverlayActivation> OverlayActivationMode = new Bindable<OverlayActivation>();
/// <summary> IBindable<LocalUserPlayingState> ILocalUserPlayInfo.PlayingState => playingState;
/// Whether the local user is currently interacting with the game in a way that should not be interrupted.
/// </summary> private readonly Bindable<LocalUserPlayingState> playingState = new Bindable<LocalUserPlayingState>();
/// <remarks>
/// This is exclusively managed by <see cref="Player"/>. If other components are mutating this state, a more
/// resilient method should be used to ensure correct state.
/// </remarks>
public Bindable<bool> LocalUserPlaying = new BindableBool();
protected OsuScreenStack ScreenStack; protected OsuScreenStack ScreenStack;
@ -302,7 +297,7 @@ namespace osu.Game
protected override UserInputManager CreateUserInputManager() protected override UserInputManager CreateUserInputManager()
{ {
var userInputManager = base.CreateUserInputManager(); var userInputManager = base.CreateUserInputManager();
(userInputManager as OsuUserInputManager)?.LocalUserPlaying.BindTo(LocalUserPlaying); (userInputManager as OsuUserInputManager)?.PlayingState.BindTo(playingState);
return userInputManager; return userInputManager;
} }
@ -391,11 +386,11 @@ namespace osu.Game
// Transfer any runtime changes back to configuration file. // Transfer any runtime changes back to configuration file.
SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID.ToString(); SkinManager.CurrentSkinInfo.ValueChanged += skin => configSkin.Value = skin.NewValue.ID.ToString();
LocalUserPlaying.BindValueChanged(p => playingState.BindValueChanged(p =>
{ {
BeatmapManager.PauseImports = p.NewValue; BeatmapManager.PauseImports = p.NewValue != LocalUserPlayingState.NotPlaying;
SkinManager.PauseImports = p.NewValue; SkinManager.PauseImports = p.NewValue != LocalUserPlayingState.NotPlaying;
ScoreManager.PauseImports = p.NewValue; ScoreManager.PauseImports = p.NewValue != LocalUserPlayingState.NotPlaying;
}, true); }, true);
IsActive.BindValueChanged(active => updateActiveState(active.NewValue), true); IsActive.BindValueChanged(active => updateActiveState(active.NewValue), true);
@ -1553,6 +1548,16 @@ namespace osu.Game
scope.SetTag(@"screen", newScreen?.GetType().ReadableName() ?? @"none"); scope.SetTag(@"screen", newScreen?.GetType().ReadableName() ?? @"none");
}); });
switch (current)
{
case Player player:
player.PlayingState.UnbindFrom(playingState);
// reset for sanity.
playingState.Value = LocalUserPlayingState.NotPlaying;
break;
}
switch (newScreen) switch (newScreen)
{ {
case IntroScreen intro: case IntroScreen intro:
@ -1565,14 +1570,15 @@ namespace osu.Game
versionManager?.Show(); versionManager?.Show();
break; break;
case Player player:
player.PlayingState.BindTo(playingState);
break;
default: default:
versionManager?.Hide(); versionManager?.Hide();
break; break;
} }
// reset on screen change for sanity.
LocalUserPlaying.Value = false;
if (current is IOsuScreen currentOsuScreen) if (current is IOsuScreen currentOsuScreen)
{ {
OverlayActivationMode.UnbindFrom(currentOsuScreen.OverlayActivationMode); OverlayActivationMode.UnbindFrom(currentOsuScreen.OverlayActivationMode);
@ -1621,7 +1627,5 @@ namespace osu.Game
if (newScreen == null) if (newScreen == null)
Exit(); Exit();
} }
IBindable<bool> ILocalUserPlayInfo.IsPlaying => LocalUserPlaying;
} }
} }

View File

@ -541,7 +541,10 @@ namespace osu.Game
realmBlocker = realm.BlockAllOperations("migration"); realmBlocker = realm.BlockAllOperations("migration");
success = true; success = true;
} }
catch { } catch (Exception ex)
{
Logger.Log($"Attempting to block all operations failed: {ex}", LoggingTarget.Database);
}
readyToRun.Set(); readyToRun.Set();
}, false); }, false);

View File

@ -98,7 +98,7 @@ namespace osu.Game.Overlays
apiUser.BindValueChanged(_ => Schedule(() => apiUser.BindValueChanged(_ => Schedule(() =>
{ {
if (api.IsLoggedIn) if (api.IsLoggedIn)
replaceResultsAreaContent(Drawable.Empty()); replaceResultsAreaContent(Empty());
})); }));
} }

View File

@ -3,8 +3,6 @@
#nullable disable #nullable disable
using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -16,8 +14,6 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapSet; using osu.Game.Overlays.BeatmapSet;
using osu.Game.Overlays.BeatmapSet.Scores; using osu.Game.Overlays.BeatmapSet.Scores;
using osu.Game.Overlays.Comments; using osu.Game.Overlays.Comments;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Select.Details;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -37,14 +33,6 @@ namespace osu.Game.Overlays
private (BeatmapSetLookupType type, int id)? lastLookup; private (BeatmapSetLookupType type, int id)? lastLookup;
/// <remarks>
/// Isolates the beatmap set overlay from the game-wide selected mods bindable
/// to avoid affecting the beatmap details section (i.e. <see cref="AdvancedStats.StatisticRow"/>).
/// </remarks>
[Cached]
[Cached(typeof(IBindable<IReadOnlyList<Mod>>))]
protected readonly Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
public BeatmapSetOverlay() public BeatmapSetOverlay()
: base(OverlayColourScheme.Blue) : base(OverlayColourScheme.Blue)
{ {

View File

@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Chat
public IReadOnlyCollection<Drawable> DrawableContentFlow => drawableContentFlow; public IReadOnlyCollection<Drawable> DrawableContentFlow => drawableContentFlow;
protected virtual float FontSize => 12; private const float font_size = 13;
protected virtual float Spacing => 15; protected virtual float Spacing => 15;
@ -183,13 +183,13 @@ namespace osu.Game.Overlays.Chat
Anchor = Anchor.TopLeft, Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft, Origin = Anchor.TopLeft,
Spacing = new Vector2(-1, 0), Spacing = new Vector2(-1, 0),
Font = OsuFont.GetFont(size: FontSize, weight: FontWeight.SemiBold, fixedWidth: true), Font = OsuFont.GetFont(size: font_size, weight: FontWeight.SemiBold, fixedWidth: true),
AlwaysPresent = true, AlwaysPresent = true,
}, },
drawableUsername = new DrawableChatUsername(message.Sender) drawableUsername = new DrawableChatUsername(message.Sender)
{ {
Width = UsernameWidth, Width = UsernameWidth,
FontSize = FontSize, FontSize = font_size,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Origin = Anchor.TopRight, Origin = Anchor.TopRight,
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
@ -258,7 +258,7 @@ namespace osu.Game.Overlays.Chat
private void styleMessageContent(SpriteText text) private void styleMessageContent(SpriteText text)
{ {
text.Shadow = false; text.Shadow = false;
text.Font = text.Font.With(size: FontSize, italics: Message.IsAction, weight: isMention ? FontWeight.SemiBold : FontWeight.Medium); text.Font = text.Font.With(size: font_size, italics: Message.IsAction, weight: isMention ? FontWeight.SemiBold : FontWeight.Medium);
Color4 messageColour = colourProvider?.Content1 ?? Colour4.White; Color4 messageColour = colourProvider?.Content1 ?? Colour4.White;

View File

@ -75,7 +75,7 @@ namespace osu.Game.Overlays.Chat
Height = LineHeight, Height = LineHeight,
Colour = colourProvider?.Background5 ?? Colour4.White, Colour = colourProvider?.Background5 ?? Colour4.White,
}, },
Drawable.Empty(), Empty(),
new OsuSpriteText new OsuSpriteText
{ {
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
@ -87,7 +87,7 @@ namespace osu.Game.Overlays.Chat
} }
}, },
}, },
Drawable.Empty(), Empty(),
new Circle new Circle
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,

View File

@ -30,9 +30,9 @@ namespace osu.Game.Overlays.Dialog
protected DangerousActionDialog() protected DangerousActionDialog()
{ {
HeaderText = DialogStrings.Caution; HeaderText = DialogStrings.CautionHeaderText;
Icon = FontAwesome.Regular.TrashAlt; Icon = FontAwesome.Solid.ExclamationTriangle;
Buttons = new PopupDialogButton[] Buttons = new PopupDialogButton[]
{ {

View File

@ -0,0 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Sprites;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Dialog
{
/// <summary>
/// A dialog which provides confirmation for deletion of something.
/// </summary>
public abstract partial class DeletionDialog : DangerousActionDialog
{
protected DeletionDialog()
{
HeaderText = DialogStrings.DeletionHeaderText;
Icon = FontAwesome.Solid.Trash;
}
}
}

View File

@ -7,7 +7,7 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
public partial class DeleteModPresetDialog : DangerousActionDialog public partial class DeleteModPresetDialog : DeletionDialog
{ {
public DeleteModPresetDialog(Live<ModPreset> modPreset) public DeleteModPresetDialog(Live<ModPreset> modPreset)
{ {

View File

@ -72,7 +72,7 @@ namespace osu.Game.Overlays
private AudioFilter audioDuckFilter = null!; private AudioFilter audioDuckFilter = null!;
private readonly Bindable<RandomSelectAlgorithm> randomSelectAlgorithm = new Bindable<RandomSelectAlgorithm>(); private readonly Bindable<RandomSelectAlgorithm> randomSelectAlgorithm = new Bindable<RandomSelectAlgorithm>();
private readonly List<BeatmapSetInfo> previousRandomSets = new List<BeatmapSetInfo>(); private readonly List<Live<BeatmapSetInfo>> previousRandomSets = new List<Live<BeatmapSetInfo>>();
private int randomHistoryDirection; private int randomHistoryDirection;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -249,19 +249,19 @@ namespace osu.Game.Overlays
queuedDirection = TrackChangeDirection.Prev; queuedDirection = TrackChangeDirection.Prev;
BeatmapSetInfo? playableSet; Live<BeatmapSetInfo>? playableSet;
if (Shuffle.Value) if (Shuffle.Value)
playableSet = getNextRandom(-1, allowProtectedTracks); playableSet = getNextRandom(-1, allowProtectedTracks);
else else
{ {
playableSet = getBeatmapSets().AsEnumerable().TakeWhile(i => !i.Equals(current?.BeatmapSetInfo)).LastOrDefault(s => !s.Protected || allowProtectedTracks) playableSet = getBeatmapSets().TakeWhile(i => !i.Value.Equals(current?.BeatmapSetInfo)).LastOrDefault(s => !s.Value.Protected || allowProtectedTracks)
?? getBeatmapSets().AsEnumerable().LastOrDefault(s => !s.Protected || allowProtectedTracks); ?? getBeatmapSets().LastOrDefault(s => !s.Value.Protected || allowProtectedTracks);
} }
if (playableSet != null) if (playableSet != null)
{ {
changeBeatmap(beatmaps.GetWorkingBeatmap(playableSet.Beatmaps.First())); changeBeatmap(beatmaps.GetWorkingBeatmap(playableSet.Value.Beatmaps.First()));
restartTrack(); restartTrack();
return PreviousTrackResult.Previous; return PreviousTrackResult.Previous;
} }
@ -345,19 +345,19 @@ namespace osu.Game.Overlays
queuedDirection = TrackChangeDirection.Next; queuedDirection = TrackChangeDirection.Next;
BeatmapSetInfo? playableSet; Live<BeatmapSetInfo>? playableSet;
if (Shuffle.Value) if (Shuffle.Value)
playableSet = getNextRandom(1, allowProtectedTracks); playableSet = getNextRandom(1, allowProtectedTracks);
else else
{ {
playableSet = getBeatmapSets().AsEnumerable().SkipWhile(i => !i.Equals(current?.BeatmapSetInfo)) playableSet = getBeatmapSets().SkipWhile(i => !i.Value.Equals(current?.BeatmapSetInfo))
.Where(i => !i.Protected || allowProtectedTracks) .Where(i => !i.Value.Protected || allowProtectedTracks)
.ElementAtOrDefault(1) .ElementAtOrDefault(1)
?? getBeatmapSets().AsEnumerable().FirstOrDefault(i => !i.Protected || allowProtectedTracks); ?? getBeatmapSets().FirstOrDefault(i => !i.Value.Protected || allowProtectedTracks);
} }
var playableBeatmap = playableSet?.Beatmaps.FirstOrDefault(); var playableBeatmap = playableSet?.Value.Beatmaps.FirstOrDefault();
if (playableBeatmap != null) if (playableBeatmap != null)
{ {
@ -369,11 +369,11 @@ namespace osu.Game.Overlays
return false; return false;
} }
private BeatmapSetInfo? getNextRandom(int direction, bool allowProtectedTracks) private Live<BeatmapSetInfo>? getNextRandom(int direction, bool allowProtectedTracks)
{ {
BeatmapSetInfo result; Live<BeatmapSetInfo> result;
var possibleSets = getBeatmapSets().AsEnumerable().Where(s => !s.Protected || allowProtectedTracks).ToArray(); var possibleSets = getBeatmapSets().Where(s => !s.Value.Protected || allowProtectedTracks).ToArray();
if (possibleSets.Length == 0) if (possibleSets.Length == 0)
return null; return null;
@ -432,7 +432,9 @@ namespace osu.Game.Overlays
private TrackChangeDirection? queuedDirection; private TrackChangeDirection? queuedDirection;
private IQueryable<BeatmapSetInfo> getBeatmapSets() => realm.Realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending); private IEnumerable<Live<BeatmapSetInfo>> getBeatmapSets() => realm.Realm.All<BeatmapSetInfo>().Where(s => !s.DeletePending)
.AsEnumerable()
.Select(s => new RealmLive<BeatmapSetInfo>(s, realm));
private void changeBeatmap(WorkingBeatmap newWorking) private void changeBeatmap(WorkingBeatmap newWorking)
{ {
@ -459,8 +461,8 @@ namespace osu.Game.Overlays
else else
{ {
// figure out the best direction based on order in playlist. // figure out the best direction based on order in playlist.
int last = getBeatmapSets().AsEnumerable().TakeWhile(b => !b.Equals(current.BeatmapSetInfo)).Count(); int last = getBeatmapSets().TakeWhile(b => !b.Value.Equals(current.BeatmapSetInfo)).Count();
int next = getBeatmapSets().AsEnumerable().TakeWhile(b => !b.Equals(newWorking.BeatmapSetInfo)).Count(); int next = getBeatmapSets().TakeWhile(b => !b.Value.Equals(newWorking.BeatmapSetInfo)).Count();
direction = last > next ? TrackChangeDirection.Prev : TrackChangeDirection.Next; direction = last > next ? TrackChangeDirection.Prev : TrackChangeDirection.Next;
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Dialog;
@ -12,6 +13,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
public MassDeleteConfirmationDialog(Action deleteAction, LocalisableString deleteContent) public MassDeleteConfirmationDialog(Action deleteAction, LocalisableString deleteContent)
{ {
BodyText = deleteContent; BodyText = deleteContent;
Icon = FontAwesome.Solid.Trash;
DangerousAction = deleteAction; DangerousAction = deleteAction;
} }
} }

View File

@ -20,13 +20,13 @@ namespace osu.Game.Overlays.Settings
Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS }; Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS };
} }
public LocalisableString TooltipText { get; set; }
public IEnumerable<string> Keywords { get; set; } = Array.Empty<string>(); public IEnumerable<string> Keywords { get; set; } = Array.Empty<string>();
public BindableBool CanBeShown { get; } = new BindableBool(true); public BindableBool CanBeShown { get; } = new BindableBool(true);
IBindable<bool> IConditionalFilterable.CanBeShown => CanBeShown; IBindable<bool> IConditionalFilterable.CanBeShown => CanBeShown;
public LocalisableString TooltipText { get; set; }
public override IEnumerable<LocalisableString> FilterTerms public override IEnumerable<LocalisableString> FilterTerms
{ {
get get

View File

@ -66,7 +66,7 @@ namespace osu.Game.Overlays.Settings
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Size = new Vector2(SettingsSidebar.EXPANDED_WIDTH); Size = new Vector2(EXPANDED_WIDTH);
Padding = new MarginPadding(40); Padding = new MarginPadding(40);

View File

@ -26,6 +26,9 @@ namespace osu.Game.Rulesets.Difficulty
protected const int ATTRIB_ID_FLASHLIGHT = 17; protected const int ATTRIB_ID_FLASHLIGHT = 17;
protected const int ATTRIB_ID_SLIDER_FACTOR = 19; protected const int ATTRIB_ID_SLIDER_FACTOR = 19;
protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21; protected const int ATTRIB_ID_SPEED_NOTE_COUNT = 21;
protected const int ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT = 23;
protected const int ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT = 25;
protected const int ATTRIB_ID_OK_HIT_WINDOW = 27;
/// <summary> /// <summary>
/// The mods which were applied to the beatmap. /// The mods which were applied to the beatmap.

View File

@ -90,6 +90,9 @@ namespace osu.Game.Rulesets.Edit
private Bindable<bool> autoSeekOnPlacement; private Bindable<bool> autoSeekOnPlacement;
private readonly Bindable<bool> composerFocusMode = new Bindable<bool>(); private readonly Bindable<bool> composerFocusMode = new Bindable<bool>();
[CanBeNull]
private RadioButton lastTool;
protected DrawableRuleset<TObject> DrawableRuleset { get; private set; } protected DrawableRuleset<TObject> DrawableRuleset { get; private set; }
protected HitObjectComposer(Ruleset ruleset) protected HitObjectComposer(Ruleset ruleset)
@ -213,8 +216,7 @@ namespace osu.Game.Rulesets.Edit
}, },
}; };
toolboxCollection.Items = CompositionTools toolboxCollection.Items = (CompositionTools.Prepend(new SelectTool()))
.Prepend(new SelectTool())
.Select(t => new HitObjectCompositionToolButton(t, () => toolSelected(t))) .Select(t => new HitObjectCompositionToolButton(t, () => toolSelected(t)))
.ToList(); .ToList();
@ -231,7 +233,7 @@ namespace osu.Game.Rulesets.Edit
sampleBankTogglesCollection.AddRange(BlueprintContainer.SampleBankTernaryStates.Select(b => new DrawableTernaryButton(b))); sampleBankTogglesCollection.AddRange(BlueprintContainer.SampleBankTernaryStates.Select(b => new DrawableTernaryButton(b)));
setSelectTool(); SetSelectTool();
EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged; EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged;
} }
@ -256,7 +258,7 @@ namespace osu.Game.Rulesets.Edit
{ {
// it's important this is performed before the similar code in EditorRadioButton disables the button. // it's important this is performed before the similar code in EditorRadioButton disables the button.
if (!timing.NewValue) if (!timing.NewValue)
setSelectTool(); SetSelectTool();
}); });
EditorBeatmap.HasTiming.BindValueChanged(hasTiming => EditorBeatmap.HasTiming.BindValueChanged(hasTiming =>
@ -460,14 +462,18 @@ namespace osu.Game.Rulesets.Edit
if (EditorBeatmap.SelectedHitObjects.Any()) if (EditorBeatmap.SelectedHitObjects.Any())
{ {
// ensure in selection mode if a selection is made. // ensure in selection mode if a selection is made.
setSelectTool(); SetSelectTool();
} }
} }
private void setSelectTool() => toolboxCollection.Items.First().Select(); public void SetSelectTool() => toolboxCollection.Items.First().Select();
public void SetLastTool() => (lastTool ?? toolboxCollection.Items.First()).Select();
private void toolSelected(CompositionTool tool) private void toolSelected(CompositionTool tool)
{ {
lastTool = toolboxCollection.Items.OfType<HitObjectCompositionToolButton>().FirstOrDefault(i => i.Tool == BlueprintContainer.CurrentTool);
BlueprintContainer.CurrentTool = tool; BlueprintContainer.CurrentTool = tool;
if (!(tool is SelectTool)) if (!(tool is SelectTool))

View File

@ -71,6 +71,11 @@ namespace osu.Game.Rulesets.Edit
PlacementActive = PlacementState.Finished; PlacementActive = PlacementState.Finished;
} }
/// <summary>
/// Determines which objects to snap to for the snap result in <see cref="UpdateTimeAndPosition"/>.
/// </summary>
public virtual SnapType SnapType => SnapType.All;
/// <summary> /// <summary>
/// Updates the time and position of this <see cref="PlacementBlueprint"/> based on the provided snap information. /// Updates the time and position of this <see cref="PlacementBlueprint"/> based on the provided snap information.
/// </summary> /// </summary>

View File

@ -8,6 +8,7 @@ using System.Linq;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
@ -30,6 +31,7 @@ using osu.Game.Screens.Edit.Setup;
using osu.Game.Screens.Ranking.Statistics; using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Users; using osu.Game.Users;
using osuTK;
namespace osu.Game.Rulesets namespace osu.Game.Rulesets
{ {
@ -396,10 +398,28 @@ namespace osu.Game.Rulesets
/// <summary> /// <summary>
/// Can be overridden to add ruleset-specific sections to the editor beatmap setup screen. /// Can be overridden to add ruleset-specific sections to the editor beatmap setup screen.
/// </summary> /// </summary>
public virtual IEnumerable<SetupSection> CreateEditorSetupSections() => public virtual IEnumerable<Drawable> CreateEditorSetupSections() =>
[ [
new MetadataSection(),
new DifficultySection(), new DifficultySection(),
new ColoursSection(), new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(25),
Children = new Drawable[]
{
new ResourcesSection
{
RelativeSizeAxes = Axes.X,
},
new ColoursSection
{
RelativeSizeAxes = Axes.X,
}
}
},
new DesignSection(),
]; ];
/// <summary> /// <summary>

View File

@ -297,7 +297,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void updatePlacementPosition() private void updatePlacementPosition()
{ {
var snapResult = Composer.FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position); var snapResult = Composer.FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position, CurrentPlacement.SnapType);
// if no time was found from positional snapping, we should still quantize to the beat. // if no time was found from positional snapping, we should still quantize to the beat.
snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null); snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null);

View File

@ -150,13 +150,25 @@ namespace osu.Game.Screens.Edit.Compose.Components
switch (e.Key) switch (e.Key)
{ {
case Key.G: case Key.G:
return CanReverse && reverseButton?.TriggerClick() == true; if (!CanReverse || reverseButton == null)
return false;
reverseButton.TriggerAction();
return true;
case Key.Comma: case Key.Comma:
return canRotate.Value && rotateCounterClockwiseButton?.TriggerClick() == true; if (!canRotate.Value || rotateCounterClockwiseButton == null)
return false;
rotateCounterClockwiseButton.TriggerAction();
return true;
case Key.Period: case Key.Period:
return canRotate.Value && rotateClockwiseButton?.TriggerClick() == true; if (!canRotate.Value || rotateClockwiseButton == null)
return false;
rotateClockwiseButton.TriggerAction();
return true;
} }
return base.OnKeyDown(e); return base.OnKeyDown(e);
@ -285,7 +297,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
Action = action Action = action
}; };
button.OperationStarted += freezeButtonPosition; button.Clicked += freezeButtonPosition;
button.HoverLost += unfreezeButtonPosition; button.HoverLost += unfreezeButtonPosition;
button.OperationStarted += operationStarted; button.OperationStarted += operationStarted;

View File

@ -21,6 +21,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
public Action? Action; public Action? Action;
public event Action? Clicked;
public event Action? HoverLost; public event Action? HoverLost;
public SelectionBoxButton(IconUsage iconUsage, string tooltip) public SelectionBoxButton(IconUsage iconUsage, string tooltip)
@ -49,11 +51,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override bool OnClick(ClickEvent e) protected override bool OnClick(ClickEvent e)
{ {
Circle.FlashColour(Colours.GrayF, 300); Clicked?.Invoke();
TriggerAction();
TriggerOperationStarted();
Action?.Invoke();
TriggerOperationEnded();
return true; return true;
} }
@ -71,5 +72,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
} }
public LocalisableString TooltipText { get; } public LocalisableString TooltipText { get; }
public void TriggerAction()
{
Circle.FlashColour(Colours.GrayF, 300);
TriggerOperationStarted();
Action?.Invoke();
TriggerOperationEnded();
}
} }
} }

View File

@ -7,7 +7,7 @@ using osu.Game.Overlays.Dialog;
namespace osu.Game.Screens.Edit namespace osu.Game.Screens.Edit
{ {
public partial class DeleteDifficultyConfirmationDialog : DangerousActionDialog public partial class DeleteDifficultyConfirmationDialog : DeletionDialog
{ {
public DeleteDifficultyConfirmationDialog(BeatmapInfo beatmapInfo, Action deleteAction) public DeleteDifficultyConfirmationDialog(BeatmapInfo beatmapInfo, Action deleteAction)
{ {

View File

@ -721,10 +721,16 @@ namespace osu.Game.Screens.Edit
switch (e.Action) switch (e.Action)
{ {
case GlobalAction.EditorSeekToPreviousHitObject: case GlobalAction.EditorSeekToPreviousHitObject:
if (editorBeatmap.SelectedHitObjects.Any())
return false;
seekHitObject(-1); seekHitObject(-1);
return true; return true;
case GlobalAction.EditorSeekToNextHitObject: case GlobalAction.EditorSeekToNextHitObject:
if (editorBeatmap.SelectedHitObjects.Any())
return false;
seekHitObject(1); seekHitObject(1);
return true; return true;

View File

@ -6,6 +6,7 @@ using osu.Framework.Graphics;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Skinning;
namespace osu.Game.Screens.Edit.Setup namespace osu.Game.Screens.Edit.Setup
{ {
@ -13,23 +14,62 @@ namespace osu.Game.Screens.Edit.Setup
{ {
public override LocalisableString Title => EditorSetupStrings.ColoursHeader; public override LocalisableString Title => EditorSetupStrings.ColoursHeader;
private LabelledColourPalette comboColours = null!; private FormColourPalette comboColours = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
comboColours = new LabelledColourPalette comboColours = new FormColourPalette
{ {
Label = EditorSetupStrings.HitCircleSliderCombos, Caption = EditorSetupStrings.HitCircleSliderCombos,
FixedLabelWidth = LABEL_WIDTH,
ColourNamePrefix = EditorSetupStrings.ComboColourPrefix
} }
}; };
}
private bool syncingColours;
protected override void LoadComplete()
{
if (Beatmap.BeatmapSkin != null) if (Beatmap.BeatmapSkin != null)
comboColours.Colours.BindTo(Beatmap.BeatmapSkin.ComboColours); comboColours.Colours.AddRange(Beatmap.BeatmapSkin.ComboColours);
if (comboColours.Colours.Count == 0)
{
// compare ctor of `EditorBeatmapSkin`
for (int i = 0; i < SkinConfiguration.DefaultComboColours.Count; ++i)
comboColours.Colours.Add(SkinConfiguration.DefaultComboColours[(i + 1) % SkinConfiguration.DefaultComboColours.Count]);
}
comboColours.Colours.BindCollectionChanged((_, _) =>
{
if (Beatmap.BeatmapSkin != null)
{
if (syncingColours)
return;
syncingColours = true;
Beatmap.BeatmapSkin.ComboColours.Clear();
Beatmap.BeatmapSkin.ComboColours.AddRange(comboColours.Colours);
syncingColours = false;
}
});
Beatmap.BeatmapSkin?.ComboColours.BindCollectionChanged((_, _) =>
{
if (syncingColours)
return;
syncingColours = true;
comboColours.Colours.Clear();
comboColours.Colours.AddRange(Beatmap.BeatmapSkin?.ComboColours);
syncingColours = false;
});
} }
} }
} }

View File

@ -15,77 +15,77 @@ using osu.Game.Localisation;
namespace osu.Game.Screens.Edit.Setup namespace osu.Game.Screens.Edit.Setup
{ {
internal partial class DesignSection : SetupSection public partial class DesignSection : SetupSection
{ {
protected LabelledSwitchButton EnableCountdown = null!; protected FormCheckBox EnableCountdown = null!;
protected FillFlowContainer CountdownSettings = null!; protected FillFlowContainer CountdownSettings = null!;
protected LabelledEnumDropdown<CountdownType> CountdownSpeed = null!; protected FormEnumDropdown<CountdownType> CountdownSpeed = null!;
protected LabelledNumberBox CountdownOffset = null!; protected FormNumberBox CountdownOffset = null!;
private LabelledSwitchButton widescreenSupport = null!; private FormCheckBox widescreenSupport = null!;
private LabelledSwitchButton epilepsyWarning = null!; private FormCheckBox epilepsyWarning = null!;
private LabelledSwitchButton letterboxDuringBreaks = null!; private FormCheckBox letterboxDuringBreaks = null!;
private LabelledSwitchButton samplesMatchPlaybackRate = null!; private FormCheckBox samplesMatchPlaybackRate = null!;
public override LocalisableString Title => EditorSetupStrings.DesignHeader; public override LocalisableString Title => EditorSetupStrings.DesignHeader;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Children = new[] Children = new Drawable[]
{ {
EnableCountdown = new LabelledSwitchButton EnableCountdown = new FormCheckBox
{ {
Label = EditorSetupStrings.EnableCountdown, Caption = EditorSetupStrings.EnableCountdown,
HintText = EditorSetupStrings.CountdownDescription,
Current = { Value = Beatmap.BeatmapInfo.Countdown != CountdownType.None }, Current = { Value = Beatmap.BeatmapInfo.Countdown != CountdownType.None },
Description = EditorSetupStrings.CountdownDescription
}, },
CountdownSettings = new FillFlowContainer CountdownSettings = new FillFlowContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Spacing = new Vector2(10), Spacing = new Vector2(5),
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Children = new Drawable[] Children = new Drawable[]
{ {
CountdownSpeed = new LabelledEnumDropdown<CountdownType> CountdownSpeed = new FormEnumDropdown<CountdownType>
{ {
Label = EditorSetupStrings.CountdownSpeed, Caption = EditorSetupStrings.CountdownSpeed,
Current = { Value = Beatmap.BeatmapInfo.Countdown != CountdownType.None ? Beatmap.BeatmapInfo.Countdown : CountdownType.Normal }, Current = { Value = Beatmap.BeatmapInfo.Countdown != CountdownType.None ? Beatmap.BeatmapInfo.Countdown : CountdownType.Normal },
Items = Enum.GetValues<CountdownType>().Where(type => type != CountdownType.None) Items = Enum.GetValues<CountdownType>().Where(type => type != CountdownType.None)
}, },
CountdownOffset = new LabelledNumberBox CountdownOffset = new FormNumberBox
{ {
Label = EditorSetupStrings.CountdownOffset, Caption = EditorSetupStrings.CountdownOffset,
HintText = EditorSetupStrings.CountdownOffsetDescription,
Current = { Value = Beatmap.BeatmapInfo.CountdownOffset.ToString() }, Current = { Value = Beatmap.BeatmapInfo.CountdownOffset.ToString() },
Description = EditorSetupStrings.CountdownOffsetDescription, TabbableContentContainer = this,
} }
} }
}, },
Empty(), widescreenSupport = new FormCheckBox
widescreenSupport = new LabelledSwitchButton
{ {
Label = EditorSetupStrings.WidescreenSupport, Caption = EditorSetupStrings.WidescreenSupport,
Description = EditorSetupStrings.WidescreenSupportDescription, HintText = EditorSetupStrings.WidescreenSupportDescription,
Current = { Value = Beatmap.BeatmapInfo.WidescreenStoryboard } Current = { Value = Beatmap.BeatmapInfo.WidescreenStoryboard }
}, },
epilepsyWarning = new LabelledSwitchButton epilepsyWarning = new FormCheckBox
{ {
Label = EditorSetupStrings.EpilepsyWarning, Caption = EditorSetupStrings.EpilepsyWarning,
Description = EditorSetupStrings.EpilepsyWarningDescription, HintText = EditorSetupStrings.EpilepsyWarningDescription,
Current = { Value = Beatmap.BeatmapInfo.EpilepsyWarning } Current = { Value = Beatmap.BeatmapInfo.EpilepsyWarning }
}, },
letterboxDuringBreaks = new LabelledSwitchButton letterboxDuringBreaks = new FormCheckBox
{ {
Label = EditorSetupStrings.LetterboxDuringBreaks, Caption = EditorSetupStrings.LetterboxDuringBreaks,
Description = EditorSetupStrings.LetterboxDuringBreaksDescription, HintText = EditorSetupStrings.LetterboxDuringBreaksDescription,
Current = { Value = Beatmap.BeatmapInfo.LetterboxInBreaks } Current = { Value = Beatmap.BeatmapInfo.LetterboxInBreaks }
}, },
samplesMatchPlaybackRate = new LabelledSwitchButton samplesMatchPlaybackRate = new FormCheckBox
{ {
Label = EditorSetupStrings.SamplesMatchPlaybackRate, Caption = EditorSetupStrings.SamplesMatchPlaybackRate,
Description = EditorSetupStrings.SamplesMatchPlaybackRateDescription, HintText = EditorSetupStrings.SamplesMatchPlaybackRateDescription,
Current = { Value = Beatmap.BeatmapInfo.SamplesMatchPlaybackRate } Current = { Value = Beatmap.BeatmapInfo.SamplesMatchPlaybackRate }
} }
}; };

View File

@ -15,12 +15,12 @@ namespace osu.Game.Screens.Edit.Setup
{ {
public partial class DifficultySection : SetupSection public partial class DifficultySection : SetupSection
{ {
private LabelledSliderBar<float> circleSizeSlider { get; set; } = null!; private FormSliderBar<float> circleSizeSlider { get; set; } = null!;
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!; private FormSliderBar<float> healthDrainSlider { get; set; } = null!;
private LabelledSliderBar<float> approachRateSlider { get; set; } = null!; private FormSliderBar<float> approachRateSlider { get; set; } = null!;
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!; private FormSliderBar<float> overallDifficultySlider { get; set; } = null!;
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!; private FormSliderBar<double> baseVelocitySlider { get; set; } = null!;
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!; private FormSliderBar<double> tickRateSlider { get; set; } = null!;
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader; public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
@ -29,90 +29,96 @@ namespace osu.Game.Screens.Edit.Setup
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
circleSizeSlider = new LabelledSliderBar<float> circleSizeSlider = new FormSliderBar<float>
{ {
Label = BeatmapsetsStrings.ShowStatsCs, Caption = BeatmapsetsStrings.ShowStatsCs,
FixedLabelWidth = LABEL_WIDTH, HintText = EditorSetupStrings.CircleSizeDescription,
Description = EditorSetupStrings.CircleSizeDescription,
Current = new BindableFloat(Beatmap.Difficulty.CircleSize) Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
{ {
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0, MinValue = 0,
MaxValue = 10, MaxValue = 10,
Precision = 0.1f, Precision = 0.1f,
} },
TransferValueOnCommit = true,
TabbableContentContainer = this,
}, },
healthDrainSlider = new LabelledSliderBar<float> healthDrainSlider = new FormSliderBar<float>
{ {
Label = BeatmapsetsStrings.ShowStatsDrain, Caption = BeatmapsetsStrings.ShowStatsDrain,
FixedLabelWidth = LABEL_WIDTH, HintText = EditorSetupStrings.DrainRateDescription,
Description = EditorSetupStrings.DrainRateDescription,
Current = new BindableFloat(Beatmap.Difficulty.DrainRate) Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
{ {
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0, MinValue = 0,
MaxValue = 10, MaxValue = 10,
Precision = 0.1f, Precision = 0.1f,
} },
TransferValueOnCommit = true,
TabbableContentContainer = this,
}, },
approachRateSlider = new LabelledSliderBar<float> approachRateSlider = new FormSliderBar<float>
{ {
Label = BeatmapsetsStrings.ShowStatsAr, Caption = BeatmapsetsStrings.ShowStatsAr,
FixedLabelWidth = LABEL_WIDTH, HintText = EditorSetupStrings.ApproachRateDescription,
Description = EditorSetupStrings.ApproachRateDescription,
Current = new BindableFloat(Beatmap.Difficulty.ApproachRate) Current = new BindableFloat(Beatmap.Difficulty.ApproachRate)
{ {
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0, MinValue = 0,
MaxValue = 10, MaxValue = 10,
Precision = 0.1f, Precision = 0.1f,
} },
TransferValueOnCommit = true,
TabbableContentContainer = this,
}, },
overallDifficultySlider = new LabelledSliderBar<float> overallDifficultySlider = new FormSliderBar<float>
{ {
Label = BeatmapsetsStrings.ShowStatsAccuracy, Caption = BeatmapsetsStrings.ShowStatsAccuracy,
FixedLabelWidth = LABEL_WIDTH, HintText = EditorSetupStrings.OverallDifficultyDescription,
Description = EditorSetupStrings.OverallDifficultyDescription,
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty) Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
{ {
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
MinValue = 0, MinValue = 0,
MaxValue = 10, MaxValue = 10,
Precision = 0.1f, Precision = 0.1f,
} },
TransferValueOnCommit = true,
TabbableContentContainer = this,
}, },
baseVelocitySlider = new LabelledSliderBar<double> baseVelocitySlider = new FormSliderBar<double>
{ {
Label = EditorSetupStrings.BaseVelocity, Caption = EditorSetupStrings.BaseVelocity,
FixedLabelWidth = LABEL_WIDTH, HintText = EditorSetupStrings.BaseVelocityDescription,
Description = EditorSetupStrings.BaseVelocityDescription,
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier) Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
{ {
Default = 1.4, Default = 1.4,
MinValue = 0.4, MinValue = 0.4,
MaxValue = 3.6, MaxValue = 3.6,
Precision = 0.01f, Precision = 0.01f,
} },
TransferValueOnCommit = true,
TabbableContentContainer = this,
}, },
tickRateSlider = new LabelledSliderBar<double> tickRateSlider = new FormSliderBar<double>
{ {
Label = EditorSetupStrings.TickRate, Caption = EditorSetupStrings.TickRate,
FixedLabelWidth = LABEL_WIDTH, HintText = EditorSetupStrings.TickRateDescription,
Description = EditorSetupStrings.TickRateDescription,
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate) Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
{ {
Default = 1, Default = 1,
MinValue = 1, MinValue = 1,
MaxValue = 4, MaxValue = 4,
Precision = 1, Precision = 1,
} },
TransferValueOnCommit = true,
TabbableContentContainer = this,
}, },
}; };
foreach (var item in Children.OfType<LabelledSliderBar<float>>()) foreach (var item in Children.OfType<FormSliderBar<float>>())
item.Current.ValueChanged += _ => updateValues(); item.Current.ValueChanged += _ => updateValues();
foreach (var item in Children.OfType<LabelledSliderBar<double>>()) foreach (var item in Children.OfType<FormSliderBar<double>>())
item.Current.ValueChanged += _ => updateValues(); item.Current.ValueChanged += _ => updateValues();
} }

View File

@ -1,22 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Graphics.UserInterfaceV2;
namespace osu.Game.Screens.Edit.Setup
{
internal partial class LabelledRomanisedTextBox : LabelledTextBox
{
protected override OsuTextBox CreateTextBox() => new RomanisedTextBox();
private partial class RomanisedTextBox : OsuTextBox
{
protected override bool AllowIme => false;
protected override bool CanAddCharacter(char character)
=> MetadataUtils.IsRomanised(character);
}
}
}

View File

@ -14,16 +14,16 @@ namespace osu.Game.Screens.Edit.Setup
{ {
public partial class MetadataSection : SetupSection public partial class MetadataSection : SetupSection
{ {
protected LabelledTextBox ArtistTextBox = null!; protected FormTextBox ArtistTextBox = null!;
protected LabelledTextBox RomanisedArtistTextBox = null!; protected FormTextBox RomanisedArtistTextBox = null!;
protected LabelledTextBox TitleTextBox = null!; protected FormTextBox TitleTextBox = null!;
protected LabelledTextBox RomanisedTitleTextBox = null!; protected FormTextBox RomanisedTitleTextBox = null!;
private LabelledTextBox creatorTextBox = null!; private FormTextBox creatorTextBox = null!;
private LabelledTextBox difficultyTextBox = null!; private FormTextBox difficultyTextBox = null!;
private LabelledTextBox sourceTextBox = null!; private FormTextBox sourceTextBox = null!;
private LabelledTextBox tagsTextBox = null!; private FormTextBox tagsTextBox = null!;
public override LocalisableString Title => EditorSetupStrings.MetadataHeader; public override LocalisableString Title => EditorSetupStrings.MetadataHeader;
@ -34,33 +34,26 @@ namespace osu.Game.Screens.Edit.Setup
Children = new[] Children = new[]
{ {
ArtistTextBox = createTextBox<LabelledTextBox>(EditorSetupStrings.Artist, ArtistTextBox = createTextBox<FormTextBox>(EditorSetupStrings.Artist,
!string.IsNullOrEmpty(metadata.ArtistUnicode) ? metadata.ArtistUnicode : metadata.Artist), !string.IsNullOrEmpty(metadata.ArtistUnicode) ? metadata.ArtistUnicode : metadata.Artist),
RomanisedArtistTextBox = createTextBox<LabelledRomanisedTextBox>(EditorSetupStrings.RomanisedArtist, RomanisedArtistTextBox = createTextBox<FormRomanisedTextBox>(EditorSetupStrings.RomanisedArtist,
!string.IsNullOrEmpty(metadata.Artist) ? metadata.Artist : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)), !string.IsNullOrEmpty(metadata.Artist) ? metadata.Artist : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)),
TitleTextBox = createTextBox<FormTextBox>(EditorSetupStrings.Title,
Empty(),
TitleTextBox = createTextBox<LabelledTextBox>(EditorSetupStrings.Title,
!string.IsNullOrEmpty(metadata.TitleUnicode) ? metadata.TitleUnicode : metadata.Title), !string.IsNullOrEmpty(metadata.TitleUnicode) ? metadata.TitleUnicode : metadata.Title),
RomanisedTitleTextBox = createTextBox<LabelledRomanisedTextBox>(EditorSetupStrings.RomanisedTitle, RomanisedTitleTextBox = createTextBox<FormRomanisedTextBox>(EditorSetupStrings.RomanisedTitle,
!string.IsNullOrEmpty(metadata.Title) ? metadata.Title : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)), !string.IsNullOrEmpty(metadata.Title) ? metadata.Title : MetadataUtils.StripNonRomanisedCharacters(metadata.ArtistUnicode)),
creatorTextBox = createTextBox<FormTextBox>(EditorSetupStrings.Creator, metadata.Author.Username),
Empty(), difficultyTextBox = createTextBox<FormTextBox>(EditorSetupStrings.DifficultyName, Beatmap.BeatmapInfo.DifficultyName),
sourceTextBox = createTextBox<FormTextBox>(BeatmapsetsStrings.ShowInfoSource, metadata.Source),
creatorTextBox = createTextBox<LabelledTextBox>(EditorSetupStrings.Creator, metadata.Author.Username), tagsTextBox = createTextBox<FormTextBox>(BeatmapsetsStrings.ShowInfoTags, metadata.Tags)
difficultyTextBox = createTextBox<LabelledTextBox>(EditorSetupStrings.DifficultyName, Beatmap.BeatmapInfo.DifficultyName),
sourceTextBox = createTextBox<LabelledTextBox>(BeatmapsetsStrings.ShowInfoSource, metadata.Source),
tagsTextBox = createTextBox<LabelledTextBox>(BeatmapsetsStrings.ShowInfoTags, metadata.Tags)
}; };
} }
private TTextBox createTextBox<TTextBox>(LocalisableString label, string initialValue) private TTextBox createTextBox<TTextBox>(LocalisableString label, string initialValue)
where TTextBox : LabelledTextBox, new() where TTextBox : FormTextBox, new()
=> new TTextBox => new TTextBox
{ {
Label = label, Caption = label,
FixedLabelWidth = LABEL_WIDTH,
Current = { Value = initialValue }, Current = { Value = initialValue },
TabbableContentContainer = this TabbableContentContainer = this
}; };
@ -75,13 +68,13 @@ namespace osu.Game.Screens.Edit.Setup
ArtistTextBox.Current.BindValueChanged(artist => transferIfRomanised(artist.NewValue, RomanisedArtistTextBox)); ArtistTextBox.Current.BindValueChanged(artist => transferIfRomanised(artist.NewValue, RomanisedArtistTextBox));
TitleTextBox.Current.BindValueChanged(title => transferIfRomanised(title.NewValue, RomanisedTitleTextBox)); TitleTextBox.Current.BindValueChanged(title => transferIfRomanised(title.NewValue, RomanisedTitleTextBox));
foreach (var item in Children.OfType<LabelledTextBox>()) foreach (var item in Children.OfType<FormTextBox>())
item.OnCommit += onCommit; item.OnCommit += onCommit;
updateReadOnlyState(); updateReadOnlyState();
} }
private void transferIfRomanised(string value, LabelledTextBox target) private void transferIfRomanised(string value, FormTextBox target)
{ {
if (MetadataUtils.IsRomanised(value)) if (MetadataUtils.IsRomanised(value))
target.Current.Value = value; target.Current.Value = value;
@ -119,5 +112,18 @@ namespace osu.Game.Screens.Edit.Setup
Beatmap.SaveState(); Beatmap.SaveState();
} }
private partial class FormRomanisedTextBox : FormTextBox
{
internal override InnerTextBox CreateTextBox() => new RomanisedTextBox();
private partial class RomanisedTextBox : InnerTextBox
{
protected override bool AllowIme => false;
protected override bool CanAddCharacter(char character)
=> MetadataUtils.IsRomanised(character);
}
}
} }
} }

View File

@ -7,15 +7,16 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Localisation; using osu.Game.Localisation;
namespace osu.Game.Screens.Edit.Setup namespace osu.Game.Screens.Edit.Setup
{ {
internal partial class ResourcesSection : SetupSection public partial class ResourcesSection : SetupSection
{ {
private LabelledFileChooser audioTrackChooser = null!; private FormFileSelector audioTrackChooser = null!;
private LabelledFileChooser backgroundChooser = null!; private FormFileSelector backgroundChooser = null!;
public override LocalisableString Title => EditorSetupStrings.ResourcesHeader; public override LocalisableString Title => EditorSetupStrings.ResourcesHeader;
@ -34,28 +35,33 @@ namespace osu.Game.Screens.Edit.Setup
[Resolved] [Resolved]
private Editor? editor { get; set; } private Editor? editor { get; set; }
[Resolved] private SetupScreenHeaderBackground headerBackground = null!;
private SetupScreenHeader header { get; set; } = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
headerBackground = new SetupScreenHeaderBackground
{
RelativeSizeAxes = Axes.X,
Height = 110,
};
Children = new Drawable[] Children = new Drawable[]
{ {
backgroundChooser = new LabelledFileChooser(".jpg", ".jpeg", ".png") backgroundChooser = new FormFileSelector(".jpg", ".jpeg", ".png")
{ {
Label = GameplaySettingsStrings.BackgroundHeader, Caption = GameplaySettingsStrings.BackgroundHeader,
FixedLabelWidth = LABEL_WIDTH, PlaceholderText = EditorSetupStrings.ClickToSelectBackground,
TabbableContentContainer = this
}, },
audioTrackChooser = new LabelledFileChooser(".mp3", ".ogg") audioTrackChooser = new FormFileSelector(".mp3", ".ogg")
{ {
Label = EditorSetupStrings.AudioTrack, Caption = EditorSetupStrings.AudioTrack,
FixedLabelWidth = LABEL_WIDTH, PlaceholderText = EditorSetupStrings.ClickToSelectTrack,
TabbableContentContainer = this
}, },
}; };
backgroundChooser.PreviewContainer.Add(headerBackground);
if (!string.IsNullOrEmpty(working.Value.Metadata.BackgroundFile)) if (!string.IsNullOrEmpty(working.Value.Metadata.BackgroundFile))
backgroundChooser.Current.Value = new FileInfo(working.Value.Metadata.BackgroundFile); backgroundChooser.Current.Value = new FileInfo(working.Value.Metadata.BackgroundFile);
@ -64,8 +70,6 @@ namespace osu.Game.Screens.Edit.Setup
backgroundChooser.Current.BindValueChanged(backgroundChanged); backgroundChooser.Current.BindValueChanged(backgroundChanged);
audioTrackChooser.Current.BindValueChanged(audioTrackChanged); audioTrackChooser.Current.BindValueChanged(audioTrackChanged);
updatePlaceholderText();
} }
public bool ChangeBackgroundImage(FileInfo source) public bool ChangeBackgroundImage(FileInfo source)
@ -92,7 +96,7 @@ namespace osu.Game.Screens.Edit.Setup
editorBeatmap.SaveState(); editorBeatmap.SaveState();
working.Value.Metadata.BackgroundFile = destination.Name; working.Value.Metadata.BackgroundFile = destination.Name;
header.Background.UpdateBackground(); headerBackground.UpdateBackground();
editor?.ApplyToBackground(bg => bg.RefreshBackground()); editor?.ApplyToBackground(bg => bg.RefreshBackground());
@ -132,22 +136,12 @@ namespace osu.Game.Screens.Edit.Setup
{ {
if (file.NewValue == null || !ChangeBackgroundImage(file.NewValue)) if (file.NewValue == null || !ChangeBackgroundImage(file.NewValue))
backgroundChooser.Current.Value = file.OldValue; backgroundChooser.Current.Value = file.OldValue;
updatePlaceholderText();
} }
private void audioTrackChanged(ValueChangedEvent<FileInfo?> file) private void audioTrackChanged(ValueChangedEvent<FileInfo?> file)
{ {
if (file.NewValue == null || !ChangeAudioTrack(file.NewValue)) if (file.NewValue == null || !ChangeAudioTrack(file.NewValue))
audioTrackChooser.Current.Value = file.OldValue; audioTrackChooser.Current.Value = file.OldValue;
updatePlaceholderText();
}
private void updatePlaceholderText()
{
audioTrackChooser.Text = audioTrackChooser.Current.Value?.Name ?? EditorSetupStrings.ClickToSelectTrack;
backgroundChooser.Text = backgroundChooser.Current.Value?.Name ?? EditorSetupStrings.ClickToSelectBackground;
} }
} }
} }

View File

@ -1,55 +1,81 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Overlays; using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Screens.Edit.Setup namespace osu.Game.Screens.Edit.Setup
{ {
public partial class SetupScreen : EditorScreen public partial class SetupScreen : EditorScreen
{ {
[Cached] public const float COLUMN_WIDTH = 450;
private SectionsContainer<SetupSection> sections { get; } = new SetupScreenSectionsContainer(); public const float SPACING = 28;
public const float MAX_WIDTH = 2 * COLUMN_WIDTH + SPACING;
[Cached]
private SetupScreenHeader header = new SetupScreenHeader();
public SetupScreen() public SetupScreen()
: base(EditorScreenMode.SongSetup) : base(EditorScreenMode.SongSetup)
{ {
} }
private OsuScrollContainer scroll = null!;
private FillFlowContainer flow = null!;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(EditorBeatmap beatmap, OverlayColourProvider colourProvider) private void load(EditorBeatmap beatmap, OverlayColourProvider colourProvider)
{ {
var ruleset = beatmap.BeatmapInfo.Ruleset.CreateInstance(); var ruleset = beatmap.BeatmapInfo.Ruleset.CreateInstance();
List<SetupSection> sectionsEnumerable = Children = new Drawable[]
[
new ResourcesSection(),
new MetadataSection()
];
sectionsEnumerable.AddRange(ruleset.CreateEditorSetupSections());
sectionsEnumerable.Add(new DesignSection());
Add(new Box
{ {
Colour = colourProvider.Background3, new Box
RelativeSizeAxes = Axes.Both, {
}); RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background3,
},
scroll = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(15),
Child = flow = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Full,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Spacing = new Vector2(25),
ChildrenEnumerable = ruleset.CreateEditorSetupSections().Select(section => section.With(s =>
{
s.Width = 450;
s.Anchor = Anchor.TopCentre;
s.Origin = Anchor.TopCentre;
})),
}
}
};
}
Add(sections.With(s => protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
if (scroll.DrawWidth > MAX_WIDTH)
{ {
s.RelativeSizeAxes = Axes.Both; flow.RelativeSizeAxes = Axes.None;
s.ChildrenEnumerable = sectionsEnumerable; flow.Width = MAX_WIDTH;
s.FixedHeader = header; }
})); else
{
flow.RelativeSizeAxes = Axes.X;
flow.Width = 1;
}
} }
public override void OnExiting(ScreenExitEvent e) public override void OnExiting(ScreenExitEvent e)
@ -62,19 +88,5 @@ namespace osu.Game.Screens.Edit.Setup
// (and potentially block the exit procedure for save). // (and potentially block the exit procedure for save).
GetContainingFocusManager()?.TriggerFocusContention(this); GetContainingFocusManager()?.TriggerFocusContention(this);
} }
private partial class SetupScreenSectionsContainer : SectionsContainer<SetupSection>
{
protected override UserTrackingScrollContainer CreateScrollContainer()
{
var scrollContainer = base.CreateScrollContainer();
// Workaround for masking issues (see https://github.com/ppy/osu-framework/issues/1675#issuecomment-910023157)
// Note that this actually causes the full scroll range to be reduced by 2px at the bottom, but it's not really noticeable.
scrollContainer.Margin = new MarginPadding { Top = 2 };
return scrollContainer;
}
}
} }
} }

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