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:
commit
5db1f05953
5
.github/workflows/ci.yml
vendored
5
.github/workflows/ci.yml
vendored
@ -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
|
||||||
|
33
.github/workflows/diffcalc.yml
vendored
33
.github/workflows/diffcalc.yml
vendored
@ -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 }}
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
|
@ -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();
|
||||||
|
@ -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", () =>
|
||||||
{
|
{
|
||||||
|
@ -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)
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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());
|
||||||
|
|
||||||
|
@ -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}";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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))));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
126
osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs
Normal file
126
osu.Game.Rulesets.Osu/Edit/Blueprints/GridPlacementBlueprint.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
{
|
{
|
||||||
|
29
osu.Game.Rulesets.Osu/Edit/GridFromPointsTool.cs
Normal file
29
osu.Game.Rulesets.Osu/Edit/GridFromPointsTool.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
@ -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();
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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"/>
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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())
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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();
|
||||||
|
@ -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", () =>
|
||||||
{
|
{
|
||||||
|
@ -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);
|
||||||
|
123
osu.Game.Tests/Visual/Editing/TestSceneColoursSection.cs
Normal file
123
osu.Game.Tests/Visual/Editing/TestSceneColoursSection.cs
Normal 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
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
{
|
{
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
{
|
{
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
|
@ -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,
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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);
|
||||||
|
@ -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.
|
||||||
|
@ -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);
|
||||||
|
@ -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>();
|
||||||
|
|
@ -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();
|
||||||
|
@ -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)
|
||||||
|
238
osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs
Normal file
238
osu.Game/Graphics/UserInterfaceV2/FormColourPalette.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -202,6 +202,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
BorderThickness = 0;
|
||||||
background.Colour = colourProvider.Background4;
|
background.Colour = colourProvider.Background4;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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:
|
||||||
|
@ -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()
|
||||||
{
|
{
|
||||||
|
@ -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."
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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());
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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[]
|
||||||
{
|
{
|
||||||
|
20
osu.Game/Overlays/Dialog/DeletionDialog.cs
Normal file
20
osu.Game/Overlays/Dialog/DeletionDialog.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
@ -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))
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
{
|
{
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
Loading…
Reference in New Issue
Block a user