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

Merge branch 'master' into taiko-beat-snap-grid

This commit is contained in:
Dean Herbert 2023-10-19 23:53:01 +09:00
commit 52c2eb93de
No known key found for this signature in database
106 changed files with 943 additions and 377 deletions

View File

@ -14,8 +14,8 @@
#
# The workflow can be run in two ways:
# 1. Via workflow dispatch.
# 2. By an owner of the repository posting a pull request or issue comment containing `!diffcalc`.
# For pull requests, the workflow will assume the pull request as the target to compare against (i.e. the `OSU_B` variable).
# 2. By an owner of the repository posting a pull request or issue comment containing `!diffcalc`.
# For pull requests, the workflow will assume the pull request as the target to compare against (i.e. the `OSU_B` variable).
# Any lines in the comment of the form `KEY=VALUE` are treated as variables for the generator.
#
# ## Google Service Account
@ -104,24 +104,25 @@ env:
COMMENT_TAG: execution-${{ github.run_id }}-${{ github.run_number }}-${{ github.run_attempt }}
jobs:
wait-for-queue:
name: "Wait for previous workflows"
check-permissions:
name: Check permissions
runs-on: ubuntu-latest
if: ${{ !cancelled() && (github.event_name == 'workflow_dispatch' || contains(github.event.comment.body, '!diffcalc') && github.event.comment.author_association == 'OWNER') }}
timeout-minutes: 50400 # 35 days, the maximum for jobs.
if: ${{ github.event_name == 'workflow_dispatch' || contains(github.event.comment.body, '!diffcalc') }}
steps:
- uses: ahmadnassri/action-workflow-queue@v1
- name: Check permissions
if: ${{ github.event_name != 'workflow_dispatch' }}
uses: actions-cool/check-user-permission@a0668c9aec87f3875fc56170b6452a453e9dd819 # v2.2.0
with:
timeout: 2147483647 # Around 24 days, maximum supported.
delay: 120000 # Poll every 2 minutes. API seems fairly low on this one.
require: 'write'
create-comment:
name: Create PR comment
needs: check-permissions
runs-on: ubuntu-latest
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '!diffcalc') && github.event.comment.author_association == 'OWNER' }}
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
steps:
- name: Create comment
uses: thollander/actions-comment-pull-request@v2
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
with:
comment_tag: ${{ env.COMMENT_TAG }}
message: |
@ -129,11 +130,21 @@ jobs:
*This comment will update on completion*
wait-for-queue:
name: "Wait for previous workflows"
needs: check-permissions
runs-on: self-hosted
timeout-minutes: 50400 # 35 days, the maximum for jobs on self-hosted runners.
steps:
- uses: ahmadnassri/action-workflow-queue@f547ac848c16a9bb1b2ed4a850e0cc5098af2cf8 # v1.1.5
with:
timeout: 2147483647 # Around 24 days - the maximum supported by JS setTimeout().
delay: 120000 # Poll every 2 minutes - the API limit seems fairly low on this one.
directory:
name: Prepare directory
needs: wait-for-queue
runs-on: self-hosted
if: ${{ !cancelled() && (github.event_name == 'workflow_dispatch' || contains(github.event.comment.body, '!diffcalc') && github.event.comment.author_association == 'OWNER') }}
outputs:
GENERATOR_DIR: ${{ steps.set-outputs.outputs.GENERATOR_DIR }}
GENERATOR_ENV: ${{ steps.set-outputs.outputs.GENERATOR_ENV }}
@ -159,7 +170,6 @@ jobs:
name: Setup environment
needs: directory
runs-on: self-hosted
if: ${{ !cancelled() && needs.directory.result == 'success' }}
env:
VARS_JSON: ${{ toJSON(vars) }}
steps:
@ -185,7 +195,7 @@ jobs:
- name: Add pull-request environment
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
run: |
sed -i "s;^OSU_B=.*$;OSU_B=${{ github.event.issue.pull_request.url }};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
sed -i "s;^OSU_B=.*$;OSU_B=${{ github.event.issue.pull_request.html_url }};" "${{ needs.directory.outputs.GENERATOR_ENV }}"
- name: Add comment environment
if: ${{ github.event_name == 'issue_comment' }}
@ -239,7 +249,6 @@ jobs:
name: Setup scores
needs: [ directory, environment ]
runs-on: self-hosted
if: ${{ !cancelled() && needs.environment.result == 'success' }}
steps:
- name: Query latest data
id: query
@ -252,7 +261,7 @@ jobs:
- name: Restore cache
id: restore-cache
uses: maxnowack/local-cache@v1
uses: maxnowack/local-cache@038cc090b52e4f205fbc468bf5b0756df6f68775 # v1
with:
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
key: ${{ steps.query.outputs.DATA_NAME }}
@ -272,7 +281,6 @@ jobs:
name: Setup beatmaps
needs: directory
runs-on: self-hosted
if: ${{ !cancelled() && needs.directory.result == 'success' }}
steps:
- name: Query latest data
id: query
@ -284,7 +292,7 @@ jobs:
- name: Restore cache
id: restore-cache
uses: maxnowack/local-cache@v1
uses: maxnowack/local-cache@038cc090b52e4f205fbc468bf5b0756df6f68775 # v1
with:
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
key: ${{ steps.query.outputs.DATA_NAME }}
@ -305,7 +313,6 @@ jobs:
needs: [ directory, environment, scores, beatmaps ]
runs-on: self-hosted
timeout-minutes: 720
if: ${{ !cancelled() && needs.scores.result == 'success' && needs.beatmaps.result == 'success' }}
outputs:
TARGET: ${{ steps.run.outputs.TARGET }}
SPREADSHEET_LINK: ${{ steps.run.outputs.SPREADSHEET_LINK }}
@ -331,21 +338,25 @@ jobs:
cd "${{ needs.directory.outputs.GENERATOR_DIR }}"
docker-compose down
output-cli:
name: Output info
needs: generator
runs-on: ubuntu-latest
steps:
- name: Output info
if: ${{ success() }}
run: |
echo "Target: ${{ steps.run.outputs.TARGET }}"
echo "Spreadsheet: ${{ steps.run.outputs.SPREADSHEET_LINK }}"
echo "Target: ${{ needs.generator.outputs.TARGET }}"
echo "Spreadsheet: ${{ needs.generator.outputs.SPREADSHEET_LINK }}"
update-comment:
name: Update PR comment
needs: [ create-comment, generator ]
runs-on: ubuntu-latest
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request && contains(github.event.comment.body, '!diffcalc') && github.event.comment.author_association == 'OWNER' }}
if: ${{ always() && needs.create-comment.result == 'success' }}
steps:
- name: Update comment on success
if: ${{ needs.generator.result == 'success' }}
uses: thollander/actions-comment-pull-request@v2
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
with:
comment_tag: ${{ env.COMMENT_TAG }}
mode: upsert
@ -356,10 +367,18 @@ jobs:
- name: Update comment on failure
if: ${{ needs.generator.result == 'failure' }}
uses: thollander/actions-comment-pull-request@v2
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
with:
comment_tag: ${{ env.COMMENT_TAG }}
mode: upsert
create_if_not_exists: false
message: |
Difficulty calculation failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
- name: Update comment on cancellation
if: ${{ needs.generator.result == 'cancelled' }}
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
with:
comment_tag: ${{ env.COMMENT_TAG }}
mode: delete
message: '.' # Appears to be required by this action for non-error status code.

View File

@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Catch.Tests
private float getCaughtObjectPosition(Fruit fruit)
{
var caughtObject = catcher.ChildrenOfType<CaughtObject>().Single(c => c.HitObject == fruit);
return caughtObject.Parent.ToSpaceOfOtherDrawable(caughtObject.Position, catcher).X;
return caughtObject.Parent!.ToSpaceOfOtherDrawable(caughtObject.Position, catcher).X;
}
private void catchFruit(Fruit fruit, float x)

View File

@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Catch
new KeyBinding(InputKey.X, CatchAction.MoveRight),
new KeyBinding(InputKey.Right, CatchAction.MoveRight),
new KeyBinding(InputKey.Shift, CatchAction.Dash),
new KeyBinding(InputKey.Shift, CatchAction.Dash),
new KeyBinding(InputKey.MouseLeft, CatchAction.Dash),
};
public override IEnumerable<Mod> ConvertFromLegacyMods(LegacyMods mods)

View File

@ -0,0 +1,26 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Catch.Edit
{
public partial class CatchDistanceSnapProvider : ComposerDistanceSnapProvider
{
protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
{
// osu!catch's distance snap implementation is limited, in that a custom spacing cannot be specified.
// Therefore this functionality is not currently used.
//
// The implementation below is probably correct but should be checked if/when exposed via controls.
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
float actualDistance = Math.Abs(((CatchHitObject)before).EffectiveX - ((CatchHitObject)after).EffectiveX);
return actualDistance / expectedDistance;
}
}
}

View File

@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Edit
{
base.Update();
Scale = new Vector2(Math.Min(Parent.ChildSize.X / CatchPlayfield.WIDTH, Parent.ChildSize.Y / CatchPlayfield.HEIGHT));
Scale = new Vector2(Math.Min(Parent!.ChildSize.X / CatchPlayfield.WIDTH, Parent!.ChildSize.Y / CatchPlayfield.HEIGHT));
Height = 1 / Scale.Y;
}
}

View File

@ -1,13 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.EnumExtensions;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
@ -19,13 +19,13 @@ using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Components.TernaryButtons;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
namespace osu.Game.Rulesets.Catch.Edit
{
// we're also a ScrollingHitObjectComposer candidate, but can't be everything can we?
public partial class CatchHitObjectComposer : DistancedHitObjectComposer<CatchHitObject>
public partial class CatchHitObjectComposer : ScrollingHitObjectComposer<CatchHitObject>, IKeyBindingHandler<GlobalAction>
{
private const float distance_snap_radius = 50;
@ -37,6 +37,11 @@ namespace osu.Game.Rulesets.Catch.Edit
MaxValue = 10,
};
[Cached(typeof(IDistanceSnapProvider))]
protected readonly CatchDistanceSnapProvider DistanceSnapProvider = new CatchDistanceSnapProvider();
private BeatSnapGrid beatSnapGrid = null!;
public CatchHitObjectComposer(CatchRuleset ruleset)
: base(ruleset)
{
@ -45,8 +50,11 @@ namespace osu.Game.Rulesets.Catch.Edit
[BackgroundDependencyLoader]
private void load()
{
AddInternal(DistanceSnapProvider);
DistanceSnapProvider.AttachToToolbox(RightToolbox);
// todo: enable distance spacing once catch supports applying it to its existing distance snap grid implementation.
DistanceSpacingMultiplier.Disabled = true;
DistanceSnapProvider.DistanceSpacingMultiplier.Disabled = true;
LayerBelowRuleset.Add(new PlayfieldBorder
{
@ -63,19 +71,35 @@ namespace osu.Game.Rulesets.Catch.Edit
Catcher.BASE_DASH_SPEED, -Catcher.BASE_DASH_SPEED,
Catcher.BASE_WALK_SPEED, -Catcher.BASE_WALK_SPEED,
}));
AddInternal(beatSnapGrid = new CatchBeatSnapGrid());
}
protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
=> base.CreateTernaryButtons()
.Concat(DistanceSnapProvider.CreateTernaryButtons());
protected override void UpdateAfterChildren()
{
// osu!catch's distance snap implementation is limited, in that a custom spacing cannot be specified.
// Therefore this functionality is not currently used.
//
// The implementation below is probably correct but should be checked if/when exposed via controls.
base.UpdateAfterChildren();
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
float actualDistance = Math.Abs(((CatchHitObject)before).EffectiveX - ((CatchHitObject)after).EffectiveX);
return actualDistance / expectedDistance;
if (BlueprintContainer.CurrentTool is SelectTool)
{
if (EditorBeatmap.SelectedHitObjects.Any())
{
beatSnapGrid.SelectionTimeRange = (EditorBeatmap.SelectedHitObjects.Min(h => h.StartTime), EditorBeatmap.SelectedHitObjects.Max(h => h.GetEndTime()));
}
else
beatSnapGrid.SelectionTimeRange = null;
}
else
{
var result = FindSnappedPositionAndTime(InputManager.CurrentState.Mouse.Position);
if (result.Time is double time)
beatSnapGrid.SelectionTimeRange = (time, time);
else
beatSnapGrid.SelectionTimeRange = null;
}
}
protected override void Update()
@ -85,7 +109,7 @@ namespace osu.Game.Rulesets.Catch.Edit
updateDistanceSnapGrid();
}
public override bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
switch (e.Action)
{
@ -94,14 +118,18 @@ namespace osu.Game.Rulesets.Catch.Edit
// May be worth considering standardising "zoom" behaviour with what the timeline uses (ie. alt-wheel) but that may cause new conflicts.
case GlobalAction.IncreaseScrollSpeed:
this.TransformBindableTo(timeRangeMultiplier, timeRangeMultiplier.Value - 1, 200, Easing.OutQuint);
break;
return true;
case GlobalAction.DecreaseScrollSpeed:
this.TransformBindableTo(timeRangeMultiplier, timeRangeMultiplier.Value + 1, 200, Easing.OutQuint);
break;
return true;
}
return base.OnPressed(e);
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
protected override DrawableRuleset<CatchHitObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods) =>
@ -189,7 +217,7 @@ namespace osu.Game.Rulesets.Catch.Edit
private void updateDistanceSnapGrid()
{
if (DistanceSnapToggle.Value != TernaryState.True)
if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True)
{
distanceSnapGrid.Hide();
return;

View File

@ -44,8 +44,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
if (Column != null)
{
headPiece.Y = Parent.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.StartTime)).Y;
tailPiece.Y = Parent.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.EndTime)).Y;
headPiece.Y = Parent!.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.StartTime)).Y;
tailPiece.Y = Parent!.ToLocalSpace(Column.ScreenSpacePositionAtTime(HitObject.EndTime)).Y;
switch (scrollingInfo.Direction.Value)
{

View File

@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints
foreach (var child in InternalChildren)
child.Anchor = child.Origin = anchor;
Position = Parent.ToLocalSpace(HitObjectContainer.ScreenSpacePositionAtTime(HitObject.StartTime)) - AnchorPosition;
Position = Parent!.ToLocalSpace(HitObjectContainer.ScreenSpacePositionAtTime(HitObject.StartTime)) - AnchorPosition;
Width = HitObjectContainer.DrawWidth;
}
}

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mania.Mods
foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns))
{
HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer;
Container hocParent = (Container)hoc.Parent;
Container hocParent = (Container)hoc.Parent!;
hocParent.Remove(hoc, false);
hocParent.Add(new PlayfieldCoveringWrapper(hoc).With(c =>

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
});
moveMouseToHitObject(1);
AddAssert("merge option available", () => selectionHandler.ContextMenuItems.Any(o => o.Text.Value == "Merge selection"));
AddAssert("merge option available", () => selectionHandler.ContextMenuItems?.Any(o => o.Text.Value == "Merge selection") == true);
mergeSelection();
@ -198,7 +198,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
});
moveMouseToHitObject(1);
AddAssert("merge option not available", () => selectionHandler.ContextMenuItems.Length > 0 && selectionHandler.ContextMenuItems.All(o => o.Text.Value != "Merge selection"));
AddAssert("merge option not available", () => selectionHandler.ContextMenuItems?.Length > 0 && selectionHandler.ContextMenuItems.All(o => o.Text.Value != "Merge selection"));
mergeSelection();
AddAssert("circles not merged", () => circle1 is not null && circle2 is not null
&& EditorBeatmap.HitObjects.Contains(circle1) && EditorBeatmap.HitObjects.Contains(circle2));
@ -222,7 +222,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
});
moveMouseToHitObject(1);
AddAssert("merge option available", () => selectionHandler.ContextMenuItems.Any(o => o.Text.Value == "Merge selection"));
AddAssert("merge option available", () => selectionHandler.ContextMenuItems?.Any(o => o.Text.Value == "Merge selection") == true);
mergeSelection();

View File

@ -47,8 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[Cached]
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
[Cached(typeof(IDistanceSnapProvider))]
private readonly OsuHitObjectComposer snapProvider = new OsuHitObjectComposer(new OsuRuleset())
private readonly TestHitObjectComposer composer = new TestHitObjectComposer
{
// Just used for the snap implementation, so let's hide from vision.
AlwaysPresent = true,
@ -71,11 +70,18 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
base.Content.Children = new Drawable[]
{
editorClock = new EditorClock(editorBeatmap),
new PopoverContainer { Child = snapProvider },
new PopoverContainer { Child = composer },
Content
};
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.CacheAs(composer.DistanceSnapProvider);
return dependencies;
}
protected override Container<Drawable> Content { get; } = new PopoverContainer { RelativeSizeAxes = Axes.Both };
[SetUp]
@ -84,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
editorBeatmap.Difficulty.SliderMultiplier = 1;
editorBeatmap.ControlPointInfo.Clear();
editorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = beat_length });
snapProvider.DistanceSpacingMultiplier.Value = 1;
composer.DistanceSnapProvider.DistanceSpacingMultiplier.Value = 1;
Children = new Drawable[]
{
@ -116,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[TestCase(0.5f)]
public void TestDistanceSpacing(float multiplier)
{
AddStep($"set distance spacing = {multiplier}", () => snapProvider.DistanceSpacingMultiplier.Value = multiplier);
AddStep($"set distance spacing = {multiplier}", () => composer.DistanceSnapProvider.DistanceSpacingMultiplier.Value = multiplier);
}
[Test]
@ -153,7 +159,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[TestCase(2f, beat_length * 2)]
public void TestDistanceSpacingAdjust(float multiplier, float expectedDistance)
{
AddStep($"Set distance spacing to {multiplier}", () => snapProvider.DistanceSpacingMultiplier.Value = multiplier);
AddStep($"Set distance spacing to {multiplier}", () => composer.DistanceSnapProvider.DistanceSpacingMultiplier.Value = multiplier);
AddStep("move mouse to point", () => InputManager.MoveMouseTo(grid.ToScreenSpace(grid_position + new Vector2(beat_length, 0) * 2)));
assertSnappedDistance(expectedDistance);
@ -266,5 +272,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
cursor.Position = LastSnappedPosition = GetSnapPosition.Invoke(inputManager.CurrentState.Mouse.Position);
}
}
private partial class TestHitObjectComposer : OsuHitObjectComposer
{
public new IDistanceSnapProvider DistanceSnapProvider => base.DistanceSnapProvider;
public TestHitObjectComposer()
: base(new OsuRuleset())
{
}
}
}
}

View File

@ -169,7 +169,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep($"move mouse to control point {index}", () =>
{
Vector2 position = slider.Path.ControlPoints[index].Position;
InputManager.MoveMouseTo(visualiser.Pieces[0].Parent.ToScreenSpace(position));
InputManager.MoveMouseTo(visualiser.Pieces[0].Parent!.ToScreenSpace(position));
});
}

View File

@ -169,7 +169,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep($"move mouse to {relativePosition}", () =>
{
Vector2 position = slider.Position + relativePosition;
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position));
});
[Test]
@ -331,7 +331,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep($"move mouse to {relativePosition}", () =>
{
Vector2 position = slider.Position + relativePosition;
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position));
});
}
@ -340,7 +340,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep($"move mouse to control point {index}", () =>
{
Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position;
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position));
});
}

View File

@ -187,7 +187,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep($"move mouse to control point {index}", () =>
{
Vector2 position = slider.Position + slider.Path.ControlPoints[index].Position;
InputManager.MoveMouseTo(drawableObject.Parent.ToScreenSpace(position));
InputManager.MoveMouseTo(drawableObject.Parent!.ToScreenSpace(position));
});
}

View File

@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
var firstPiece = this.ChildrenOfType<PathControlPointPiece<Slider>>().Single(piece => piece.ControlPoint == slider.Path.ControlPoints[0]);
var pos = slider.Path.PositionAt(0.25d) + slider.Position;
InputManager.MoveMouseTo(firstPiece.Parent.ToScreenSpace(pos));
InputManager.MoveMouseTo(firstPiece.Parent!.ToScreenSpace(pos));
});
AddStep("move slider end", () =>
{

View File

@ -231,7 +231,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
if (slider is null || visualiser is null) return;
Vector2 position = slider.Path.ControlPoints[index].Position + slider.Position;
InputManager.MoveMouseTo(visualiser.Pieces[0].Parent.ToScreenSpace(position));
InputManager.MoveMouseTo(visualiser.Pieces[0].Parent!.ToScreenSpace(position));
});
}
@ -241,7 +241,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
if (visualiser is null) return;
MenuItem? item = visualiser.ContextMenuItems.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
MenuItem? item = visualiser.ContextMenuItems?.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);
item?.Action.Value?.Invoke();
});

View File

@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Tests
pool = pools[poolIndex];
// We need to make sure neither the pool nor the judgement get disposed when new content is set, and they both share the same parent.
((Container)pool.Parent).Clear(false);
((Container)pool.Parent!).Clear(false);
}
var container = new Container

View File

@ -27,6 +27,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Tests
{
@ -50,6 +51,8 @@ namespace osu.Game.Rulesets.Osu.Tests
snakingOut.Value = !v;
});
AddToggleStep("toggle hidden", hiddenActive => SelectedMods.Value = hiddenActive ? new[] { new OsuModHidden() } : Array.Empty<Mod>());
AddSliderStep("hit at", 0f, 1f, 0f, v =>
{
progressToHit = v;

View File

@ -47,7 +47,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
public Action<List<PathControlPoint>> SplitControlPointsRequested;
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider snapProvider { get; set; }
private IPositionSnapProvider positionSnapProvider { get; set; }
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider distanceSnapProvider { get; set; }
public PathControlPointVisualiser(T hitObject, bool allowSelection)
{
@ -288,10 +291,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
if (selectedControlPoints.Contains(hitObject.Path.ControlPoints[0]))
{
// Special handling for selections containing head control point - the position of the hit object changes which means the snapped position and time have to be taken into account
Vector2 newHeadPosition = Parent.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex]));
var result = snapProvider?.FindSnappedPositionAndTime(newHeadPosition);
Vector2 newHeadPosition = Parent!.ToScreenSpace(e.MousePosition + (dragStartPositions[0] - dragStartPositions[draggedControlPointIndex]));
var result = positionSnapProvider?.FindSnappedPositionAndTime(newHeadPosition);
Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position;
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? newHeadPosition) - hitObject.Position;
hitObject.Position += movementDelta;
hitObject.StartTime = result?.Time ?? hitObject.StartTime;
@ -309,9 +312,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
else
{
var result = snapProvider?.FindSnappedPositionAndTime(Parent.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids);
var result = positionSnapProvider?.FindSnappedPositionAndTime(Parent!.ToScreenSpace(e.MousePosition), SnapType.GlobalGrids);
Vector2 movementDelta = Parent.ToLocalSpace(result?.ScreenSpacePosition ?? Parent.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;
Vector2 movementDelta = Parent!.ToLocalSpace(result?.ScreenSpacePosition ?? Parent!.ToScreenSpace(e.MousePosition)) - dragStartPositions[draggedControlPointIndex] - hitObject.Position;
for (int i = 0; i < controlPoints.Count; ++i)
{
@ -322,7 +325,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
}
// Snap the path to the current beat divisor before checking length validity.
hitObject.SnapTo(snapProvider);
hitObject.SnapTo(distanceSnapProvider);
if (!hitObject.Path.HasValidLength)
{
@ -332,7 +335,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
hitObject.Position = oldPosition;
hitObject.StartTime = oldStartTime;
// Snap the path length again to undo the invalid length.
hitObject.SnapTo(snapProvider);
hitObject.SnapTo(distanceSnapProvider);
return;
}

View File

@ -39,7 +39,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private int currentSegmentLength;
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider snapProvider { get; set; }
private IPositionSnapProvider positionSnapProvider { get; set; }
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider distanceSnapProvider { get; set; }
protected override bool IsValidForPlacement => HitObject.Path.HasValidLength;
@ -198,7 +201,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
// Update the cursor position.
var result = snapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.Body ? SnapType.GlobalGrids : SnapType.All);
var result = positionSnapProvider?.FindSnappedPositionAndTime(inputManager.CurrentState.Mouse.Position, state == SliderPlacementState.Body ? SnapType.GlobalGrids : SnapType.All);
cursor.Position = ToLocalSpace(result?.ScreenSpacePosition ?? inputManager.CurrentState.Mouse.Position) - HitObject.Position;
}
else if (cursor != null)
@ -230,7 +233,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updateSlider()
{
HitObject.Path.ExpectedDistance.Value = snapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
HitObject.Path.ExpectedDistance.Value = distanceSnapProvider?.FindSnappedDistance(HitObject, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
bodyPiece.UpdateFrom(HitObject);
headCirclePiece.UpdateFrom(HitObject.HeadCircle);

View File

@ -40,7 +40,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
protected PathControlPointVisualiser<Slider> ControlPointVisualiser { get; private set; }
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider snapProvider { get; set; }
private IPositionSnapProvider positionSnapProvider { get; set; }
[Resolved(CanBeNull = true)]
private IDistanceSnapProvider distanceSnapProvider { get; set; }
[Resolved(CanBeNull = true)]
private IPlacementHandler placementHandler { get; set; }
@ -194,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
if (placementControlPoint != null)
{
var result = snapProvider?.FindSnappedPositionAndTime(ToScreenSpace(e.MousePosition));
var result = positionSnapProvider?.FindSnappedPositionAndTime(ToScreenSpace(e.MousePosition));
placementControlPoint.Position = ToLocalSpace(result?.ScreenSpacePosition ?? ToScreenSpace(e.MousePosition)) - HitObject.Position;
}
}
@ -245,7 +248,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
// Move the control points from the insertion index onwards to make room for the insertion
controlPoints.Insert(insertionIndex, pathControlPoint);
HitObject.SnapTo(snapProvider);
HitObject.SnapTo(distanceSnapProvider);
return pathControlPoint;
}
@ -267,7 +270,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
}
// Snap the slider to the current beat divisor before checking length validity.
HitObject.SnapTo(snapProvider);
HitObject.SnapTo(distanceSnapProvider);
// If there are 0 or 1 remaining control points, or the slider has an invalid length, it is in a degenerate form and should be deleted
if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLength)

View File

@ -0,0 +1,31 @@
// 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.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class OsuDistanceSnapProvider : ComposerDistanceSnapProvider
{
protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
{
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position);
return actualDistance / expectedDistance;
}
protected override bool AdjustDistanceSpacing(GlobalAction action, float amount)
{
// To allow better visualisation, ensure that the spacing grid is visible before adjusting.
DistanceSnapToggle.Value = TernaryState.True;
return base.AdjustDistanceSpacing(action, amount);
}
}
}

View File

@ -17,7 +17,6 @@ using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
@ -30,7 +29,7 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Edit
{
public partial class OsuHitObjectComposer : DistancedHitObjectComposer<OsuHitObject>
public partial class OsuHitObjectComposer : HitObjectComposer<OsuHitObject>
{
public OsuHitObjectComposer(Ruleset ruleset)
: base(ruleset)
@ -49,18 +48,27 @@ namespace osu.Game.Rulesets.Osu.Edit
private readonly Bindable<TernaryState> rectangularGridSnapToggle = new Bindable<TernaryState>();
protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
{
new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Th })
});
protected override IEnumerable<TernaryButton> CreateTernaryButtons()
=> base.CreateTernaryButtons()
.Concat(DistanceSnapProvider.CreateTernaryButtons())
.Concat(new[]
{
new TernaryButton(rectangularGridSnapToggle, "Grid Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Th })
});
private BindableList<HitObject> selectedHitObjects;
private Bindable<HitObject> placementObject;
[Cached(typeof(IDistanceSnapProvider))]
protected readonly OsuDistanceSnapProvider DistanceSnapProvider = new OsuDistanceSnapProvider();
[BackgroundDependencyLoader]
private void load()
{
AddInternal(DistanceSnapProvider);
DistanceSnapProvider.AttachToToolbox(RightToolbox);
// Give a bit of breathing room around the playfield content.
PlayfieldContentContainer.Padding = new MarginPadding(10);
@ -81,7 +89,7 @@ namespace osu.Game.Rulesets.Osu.Edit
placementObject = EditorBeatmap.PlacementObject.GetBoundCopy();
placementObject.ValueChanged += _ => updateDistanceSnapGrid();
DistanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid();
DistanceSnapProvider.DistanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid();
// we may be entering the screen with a selection already active
updateDistanceSnapGrid();
@ -106,14 +114,6 @@ namespace osu.Game.Rulesets.Osu.Edit
private RectangularPositionSnapGrid rectangularPositionSnapGrid;
protected override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
{
float expectedDistance = DurationToDistance(before, after.StartTime - before.GetEndTime());
float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position);
return actualDistance / expectedDistance;
}
protected override void Update()
{
base.Update();
@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Edit
// We want to ensure that in this particular case, the time-snapping component of distance snap is still applied.
// The easiest way to ensure this is to attempt application of distance snap after a nearby object is found, and copy over
// the time value if the proposed positions are roughly the same.
if (snapType.HasFlagFast(SnapType.RelativeGrids) && DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
if (snapType.HasFlagFast(SnapType.RelativeGrids) && DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{
(Vector2 distanceSnappedPosition, double distanceSnappedTime) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(snapResult.ScreenSpacePosition));
if (Precision.AlmostEquals(distanceSnapGrid.ToScreenSpace(distanceSnappedPosition), snapResult.ScreenSpacePosition, 1))
@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Osu.Edit
if (snapType.HasFlagFast(SnapType.RelativeGrids))
{
if (DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
if (DistanceSnapProvider.DistanceSnapToggle.Value == TernaryState.True && distanceSnapGrid != null)
{
(Vector2 pos, double time) = distanceSnapGrid.GetSnappedPosition(distanceSnapGrid.ToLocalSpace(screenSpacePosition));
@ -220,7 +220,7 @@ namespace osu.Game.Rulesets.Osu.Edit
distanceSnapGridCache.Invalidate();
distanceSnapGrid = null;
if (DistanceSnapToggle.Value != TernaryState.True)
if (DistanceSnapProvider.DistanceSnapToggle.Value != TernaryState.True)
return;
switch (BlueprintContainer.CurrentTool)
@ -262,14 +262,6 @@ namespace osu.Game.Rulesets.Osu.Edit
base.OnKeyUp(e);
}
protected override bool AdjustDistanceSpacing(GlobalAction action, float amount)
{
// To allow better visualisation, ensure that the spacing grid is visible before adjusting.
DistanceSnapToggle.Value = TernaryState.True;
return base.AdjustDistanceSpacing(action, amount);
}
private bool gridSnapMomentary;
private void handleToggleViaKey(KeyboardEvent key)

View File

@ -139,12 +139,12 @@ namespace osu.Game.Rulesets.Osu.Mods
if (Precision.AlmostEquals(restrictTo.Rotation, 0))
{
start = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopLeft).X;
end = Parent.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopRight).X;
start = Parent!.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopLeft).X;
end = Parent!.ToLocalSpace(restrictTo.ScreenSpaceDrawQuad.TopRight).X;
}
else
{
float center = restrictTo.ToSpaceOfOtherDrawable(restrictTo.OriginPosition, Parent).X;
float center = restrictTo.ToSpaceOfOtherDrawable(restrictTo.OriginPosition, Parent!).X;
float halfDiagonal = (restrictTo.DrawSize / 2).LengthFast;
start = center - halfDiagonal;

View File

@ -98,6 +98,9 @@ namespace osu.Game.Rulesets.Osu.Mods
// only apply to circle piece reverse arrow is not affected by hidden.
sliderRepeat.CirclePiece.FadeOut(fadeDuration);
using (drawableObject.BeginAbsoluteSequence(drawableObject.HitStateUpdateTime))
sliderRepeat.FadeOut();
break;
case DrawableHitCircle circle:

View File

@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
/// </summary>
public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult);
private RectangleF parentScreenSpaceRectangle => ((DrawableOsuHitObject)ParentHitObject)?.parentScreenSpaceRectangle ?? Parent.ScreenSpaceDrawQuad.AABBFloat;
private RectangleF parentScreenSpaceRectangle => ((DrawableOsuHitObject)ParentHitObject)?.parentScreenSpaceRectangle ?? Parent!.ScreenSpaceDrawQuad.AABBFloat;
/// <summary>
/// Calculates the position of the given <paramref name="drawable"/> relative to the playfield area.

View File

@ -118,11 +118,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
case ArmedState.Hit:
this.FadeOut(animDuration, Easing.Out);
const float final_scale = 1.5f;
Arrow.ScaleTo(Scale * final_scale, animDuration, Easing.Out);
CirclePiece.ScaleTo(Scale * final_scale, animDuration, Easing.Out);
break;
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@ -12,6 +13,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK;
using osuTK.Graphics;
@ -19,8 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
public partial class ArgonReverseArrow : CompositeDrawable
{
[Resolved]
private DrawableHitObject drawableObject { get; set; } = null!;
private DrawableSliderRepeat drawableRepeat { get; set; } = null!;
private Bindable<Color4> accentColour = null!;
@ -29,8 +30,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
private Sprite side = null!;
[BackgroundDependencyLoader]
private void load(TextureStore textures)
private void load(DrawableHitObject drawableObject, TextureStore textures)
{
drawableRepeat = (DrawableSliderRepeat)drawableObject;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
@ -70,10 +73,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
}
};
accentColour = drawableObject.AccentColour.GetBoundCopy();
accentColour = drawableRepeat.AccentColour.GetBoundCopy();
accentColour.BindValueChanged(accent => icon.Colour = accent.NewValue.Darken(4), true);
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
drawableRepeat.ApplyCustomUpdateState += updateStateTransforms;
}
private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state)
@ -96,6 +99,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
.MoveToX(0, move_in_duration, Easing.Out)
.Loop(total - (move_in_duration + move_out_duration));
break;
case ArmedState.Hit:
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
this.ScaleTo(1.5f, animDuration, Easing.Out);
break;
}
}
@ -103,8 +111,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
{
base.Dispose(isDisposing);
if (drawableObject.IsNotNull())
drawableObject.ApplyCustomUpdateState -= updateStateTransforms;
if (drawableRepeat.IsNotNull())
drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms;
}
}
}

View File

@ -109,7 +109,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Argon
base.Update();
//undo rotation on layers which should not be rotated.
float appliedRotation = Parent.Rotation;
float appliedRotation = Parent!.Rotation;
fill.Rotation = -appliedRotation;
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
@ -8,14 +9,14 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public partial class DefaultReverseArrow : CompositeDrawable
{
[Resolved]
private DrawableHitObject drawableObject { get; set; } = null!;
private DrawableSliderRepeat drawableRepeat { get; set; } = null!;
public DefaultReverseArrow()
{
@ -36,9 +37,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
}
[BackgroundDependencyLoader]
private void load()
private void load(DrawableHitObject drawableObject)
{
drawableObject.ApplyCustomUpdateState += updateStateTransforms;
drawableRepeat = (DrawableSliderRepeat)drawableObject;
drawableRepeat.ApplyCustomUpdateState += updateStateTransforms;
}
private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state)
@ -55,6 +57,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
.ScaleTo(1f, move_in_duration, Easing.Out)
.Loop(total - (move_in_duration + move_out_duration));
break;
case ArmedState.Hit:
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
InternalChild.ScaleTo(1.5f, animDuration, Easing.Out);
break;
}
}
@ -62,8 +69,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
{
base.Dispose(isDisposing);
if (drawableObject.IsNotNull())
drawableObject.ApplyCustomUpdateState -= updateStateTransforms;
if (drawableRepeat.IsNotNull())
drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms;
}
}
}

View File

@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
protected override bool OnMouseMove(MouseMoveEvent e)
{
mousePosition = Parent.ToLocalSpace(e.ScreenSpaceMousePosition);
mousePosition = Parent!.ToLocalSpace(e.ScreenSpaceMousePosition);
return base.OnMouseMove(e);
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -17,8 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
public partial class LegacyReverseArrow : CompositeDrawable
{
[Resolved]
private DrawableHitObject drawableObject { get; set; } = null!;
private DrawableSliderRepeat drawableRepeat { get; set; } = null!;
private Drawable proxy = null!;
@ -31,8 +31,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
private bool shouldRotate;
[BackgroundDependencyLoader]
private void load(ISkinSource skinSource)
private void load(DrawableHitObject drawableObject, ISkinSource skinSource)
{
drawableRepeat = (DrawableSliderRepeat)drawableObject;
AutoSizeAxes = Axes.Both;
string lookupName = new OsuSkinComponentLookup(OsuSkinComponents.ReverseArrow).LookupName;
@ -58,10 +60,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
proxy = CreateProxy();
drawableObject.HitObjectApplied += onHitObjectApplied;
onHitObjectApplied(drawableObject);
drawableRepeat.HitObjectApplied += onHitObjectApplied;
onHitObjectApplied(drawableRepeat);
accentColour = drawableObject.AccentColour.GetBoundCopy();
accentColour = drawableRepeat.AccentColour.GetBoundCopy();
accentColour.BindValueChanged(c =>
{
arrow.Colour = textureIsDefaultSkin && c.NewValue.R + c.NewValue.G + c.NewValue.B > (600 / 255f) ? Color4.Black : Color4.White;
@ -73,8 +75,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Debug.Assert(proxy.Parent == null);
// see logic in LegacySliderHeadHitCircle.
(drawableObject as DrawableSliderRepeat)?.DrawableSlider
.OverlayElementContainer.Add(proxy);
drawableRepeat.DrawableSlider.OverlayElementContainer.Add(proxy);
}
private void updateStateTransforms(DrawableHitObject hitObject, ArmedState state)
@ -102,6 +103,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
}
break;
case ArmedState.Hit:
double animDuration = Math.Min(300, drawableRepeat.HitObject.SpanDuration);
InternalChild.ScaleTo(1.4f, animDuration, Easing.Out);
break;
}
}
@ -109,10 +115,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
base.Dispose(isDisposing);
if (drawableObject.IsNotNull())
if (drawableRepeat.IsNotNull())
{
drawableObject.HitObjectApplied -= onHitObjectApplied;
drawableObject.ApplyCustomUpdateState -= updateStateTransforms;
drawableRepeat.HitObjectApplied -= onHitObjectApplied;
drawableRepeat.ApplyCustomUpdateState -= updateStateTransforms;
}
}
}

View File

@ -88,7 +88,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
base.UpdateAfterChildren();
//undo rotation on layers which should not be rotated.
float appliedRotation = Parent.Rotation;
float appliedRotation = Parent!.Rotation;
layerNd.Rotation = -appliedRotation;
layerSpec.Rotation = -appliedRotation;

View File

@ -60,12 +60,12 @@ namespace osu.Game.Rulesets.Osu.UI
// game_size = DrawSizePreservingFillContainer.TargetSize = new Vector2(1024, 768)
//
// Parent is a 4:3 aspect enforced, using height as the constricting dimension
// Parent.ChildSize.X = min(game_size.X, game_size.Y * (4 / 3)) * playfield_size_adjust
// Parent.ChildSize.X = 819.2
// Parent!.ChildSize.X = min(game_size.X, game_size.Y * (4 / 3)) * playfield_size_adjust
// Parent!.ChildSize.X = 819.2
//
// Scale = 819.2 / 512
// Scale = 1.6
Scale = new Vector2(Parent.ChildSize.X / OsuPlayfield.BASE_SIZE.X);
Scale = new Vector2(Parent!.ChildSize.X / OsuPlayfield.BASE_SIZE.X);
Position = new Vector2(0, (PlayfieldShift ? 8f : 0f) * Scale.X);
// Size = 0.625
Size = Vector2.Divide(Vector2.One, Scale);

View File

@ -30,8 +30,8 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints
var topLeft = new Vector2(float.MaxValue, float.MaxValue);
var bottomRight = new Vector2(float.MinValue, float.MinValue);
topLeft = Vector2.ComponentMin(topLeft, Parent.ToLocalSpace(DrawableObject.ScreenSpaceDrawQuad.TopLeft));
bottomRight = Vector2.ComponentMax(bottomRight, Parent.ToLocalSpace(DrawableObject.ScreenSpaceDrawQuad.BottomRight));
topLeft = Vector2.ComponentMin(topLeft, Parent!.ToLocalSpace(DrawableObject.ScreenSpaceDrawQuad.TopLeft));
bottomRight = Vector2.ComponentMax(bottomRight, Parent!.ToLocalSpace(DrawableObject.ScreenSpaceDrawQuad.BottomRight));
Size = bottomRight - topLeft;
Position = topLeft;

View File

@ -261,7 +261,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
base.Update();
Size = BaseSize * Parent.RelativeChildSize;
Size = BaseSize * Parent!.RelativeChildSize;
// Make the swell stop at the hit target
X = Math.Max(0, X);

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Argon
protected override void Update()
{
base.Update();
Width = Parent.DrawSize.X + DrawHeight;
Width = Parent!.DrawSize.X + DrawHeight;
}
}
}

View File

@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
protected override void Update()
{
base.Update();
Width = Parent.DrawSize.X + DrawHeight;
Width = Parent!.DrawSize.X + DrawHeight;
}
}
}

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.UI
// This is still a bit weird, because readability changes with window size, but it is what it is.
if (LockPlayfieldAspectRange.Value)
{
float currentAspect = Parent.ChildSize.X / Parent.ChildSize.Y;
float currentAspect = Parent!.ChildSize.X / Parent!.ChildSize.Y;
if (currentAspect > MAXIMUM_ASPECT)
height *= currentAspect / MAXIMUM_ASPECT;

View File

@ -3,7 +3,6 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@ -230,25 +229,25 @@ namespace osu.Game.Tests.Editing
}
private void assertSnapDistance(float expectedDistance, HitObject? referenceObject, bool includeSliderVelocity)
=> AddAssert($"distance is {expectedDistance}", () => composer.GetBeatSnapDistanceAt(referenceObject ?? new HitObject(), includeSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
=> AddAssert($"distance is {expectedDistance}", () => composer.DistanceSnapProvider.GetBeatSnapDistanceAt(referenceObject ?? new HitObject(), includeSliderVelocity), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
private void assertDurationToDistance(double duration, float expectedDistance, HitObject? referenceObject = null)
=> AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DurationToDistance(referenceObject ?? new HitObject(), duration), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
=> AddAssert($"duration = {duration} -> distance = {expectedDistance}", () => composer.DistanceSnapProvider.DurationToDistance(referenceObject ?? new HitObject(), duration), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
private void assertDistanceToDuration(float distance, double expectedDuration, HitObject? referenceObject = null)
=> AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceToDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON));
=> AddAssert($"distance = {distance} -> duration = {expectedDuration}", () => composer.DistanceSnapProvider.DistanceToDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON));
private void assertSnappedDuration(float distance, double expectedDuration, HitObject? referenceObject = null)
=> AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.FindSnappedDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON));
=> AddAssert($"distance = {distance} -> duration = {expectedDuration} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDuration(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDuration).Within(Precision.FLOAT_EPSILON));
private void assertSnappedDistance(float distance, float expectedDistance, HitObject? referenceObject = null)
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.FindSnappedDistance(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
=> AddAssert($"distance = {distance} -> distance = {expectedDistance} (snapped)", () => composer.DistanceSnapProvider.FindSnappedDistance(referenceObject ?? new HitObject(), distance), () => Is.EqualTo(expectedDistance).Within(Precision.FLOAT_EPSILON));
private partial class TestHitObjectComposer : OsuHitObjectComposer
{
public new EditorBeatmap EditorBeatmap => base.EditorBeatmap;
public new Bindable<double> DistanceSpacingMultiplier => base.DistanceSpacingMultiplier;
public new IDistanceSnapProvider DistanceSnapProvider => base.DistanceSnapProvider;
public TestHitObjectComposer()
: base(new OsuRuleset())

View File

@ -0,0 +1,117 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Input.Bindings;
using osu.Game.Input;
using osu.Game.Input.Bindings;
using osuTK.Input;
namespace osu.Game.Tests.Input
{
[TestFixture]
public class RealmKeyBindingStoreTest
{
[Test]
public void TestBindingsWithoutDuplicatesAreNotModified()
{
var bindings = new List<RealmKeyBinding>
{
new RealmKeyBinding(GlobalAction.Back, KeyCombination.FromKey(Key.Escape)),
new RealmKeyBinding(GlobalAction.Back, KeyCombination.FromMouseButton(MouseButton.Button1)),
new RealmKeyBinding(GlobalAction.MusicPrev, KeyCombination.FromKey(Key.F1)),
new RealmKeyBinding(GlobalAction.MusicNext, KeyCombination.FromKey(Key.F5))
};
int countCleared = RealmKeyBindingStore.ClearDuplicateBindings(bindings);
Assert.Multiple(() =>
{
Assert.That(countCleared, Is.Zero);
Assert.That(bindings[0].Action, Is.EqualTo((int)GlobalAction.Back));
Assert.That(bindings[0].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.Escape)));
Assert.That(bindings[1].Action, Is.EqualTo((int)GlobalAction.Back));
Assert.That(bindings[1].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.ExtraMouseButton1)));
Assert.That(bindings[2].Action, Is.EqualTo((int)GlobalAction.MusicPrev));
Assert.That(bindings[2].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.F1)));
Assert.That(bindings[3].Action, Is.EqualTo((int)GlobalAction.MusicNext));
Assert.That(bindings[3].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.F5)));
});
}
[Test]
public void TestDuplicateBindingsAreCleared()
{
var bindings = new List<RealmKeyBinding>
{
new RealmKeyBinding(GlobalAction.Back, KeyCombination.FromKey(Key.Escape)),
new RealmKeyBinding(GlobalAction.Back, KeyCombination.FromMouseButton(MouseButton.Button1)),
new RealmKeyBinding(GlobalAction.MusicPrev, KeyCombination.FromKey(Key.F1)),
new RealmKeyBinding(GlobalAction.IncreaseVolume, KeyCombination.FromKey(Key.Escape)),
new RealmKeyBinding(GlobalAction.MusicNext, KeyCombination.FromKey(Key.F5)),
new RealmKeyBinding(GlobalAction.ExportReplay, KeyCombination.FromKey(Key.F1)),
new RealmKeyBinding(GlobalAction.TakeScreenshot, KeyCombination.FromKey(Key.PrintScreen)),
};
int countCleared = RealmKeyBindingStore.ClearDuplicateBindings(bindings);
Assert.Multiple(() =>
{
Assert.That(countCleared, Is.EqualTo(4));
Assert.That(bindings[0].Action, Is.EqualTo((int)GlobalAction.Back));
Assert.That(bindings[0].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.None)));
Assert.That(bindings[1].Action, Is.EqualTo((int)GlobalAction.Back));
Assert.That(bindings[1].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.ExtraMouseButton1)));
Assert.That(bindings[2].Action, Is.EqualTo((int)GlobalAction.MusicPrev));
Assert.That(bindings[2].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.None)));
Assert.That(bindings[3].Action, Is.EqualTo((int)GlobalAction.IncreaseVolume));
Assert.That(bindings[3].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.None)));
Assert.That(bindings[4].Action, Is.EqualTo((int)GlobalAction.MusicNext));
Assert.That(bindings[4].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.F5)));
Assert.That(bindings[5].Action, Is.EqualTo((int)GlobalAction.ExportReplay));
Assert.That(bindings[5].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.None)));
Assert.That(bindings[6].Action, Is.EqualTo((int)GlobalAction.TakeScreenshot));
Assert.That(bindings[6].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.PrintScreen)));
});
}
[Test]
public void TestDuplicateBindingsAllowedIfBoundToSameAction()
{
var bindings = new List<RealmKeyBinding>
{
new RealmKeyBinding(GlobalAction.Back, KeyCombination.FromKey(Key.Escape)),
new RealmKeyBinding(GlobalAction.Back, KeyCombination.FromKey(Key.Escape)),
new RealmKeyBinding(GlobalAction.MusicPrev, KeyCombination.FromKey(Key.F1)),
};
int countCleared = RealmKeyBindingStore.ClearDuplicateBindings(bindings);
Assert.Multiple(() =>
{
Assert.That(countCleared, Is.EqualTo(0));
Assert.That(bindings[0].Action, Is.EqualTo((int)GlobalAction.Back));
Assert.That(bindings[0].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.Escape)));
Assert.That(bindings[1].Action, Is.EqualTo((int)GlobalAction.Back));
Assert.That(bindings[1].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.Escape)));
Assert.That(bindings[2].Action, Is.EqualTo((int)GlobalAction.MusicPrev));
Assert.That(bindings[2].KeyCombination, Is.EqualTo(new KeyCombination(InputKey.F1)));
});
}
}
}

View File

@ -187,11 +187,9 @@ namespace osu.Game.Tests.Visual.Editing
private class SnapProvider : IDistanceSnapProvider
{
public SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.AllGrids) => new SnapResult(screenSpacePosition, 0);
public Bindable<double> DistanceSpacingMultiplier { get; } = new BindableDouble(1);
IBindable<double> IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier;
Bindable<double> IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier;
public float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true) => beat_snap_distance;

View File

@ -182,7 +182,7 @@ namespace osu.Game.Tests.Visual.Editing
if (sameRuleset)
{
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog);
AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog.PerformOkAction());
AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction());
}
AddUntilStep("wait for created", () =>
@ -269,7 +269,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo));
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog);
AddStep("confirm creation as a copy", () => DialogOverlay.CurrentDialog.Buttons.ElementAt(1).TriggerClick());
AddStep("confirm creation as a copy", () => DialogOverlay.CurrentDialog!.Buttons.ElementAt(1).TriggerClick());
AddUntilStep("wait for created", () =>
{
@ -342,7 +342,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo));
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog);
AddStep("confirm creation as a copy", () => DialogOverlay.CurrentDialog.Buttons.ElementAt(1).TriggerClick());
AddStep("confirm creation as a copy", () => DialogOverlay.CurrentDialog!.Buttons.ElementAt(1).TriggerClick());
AddUntilStep("wait for created", () =>
{
@ -380,7 +380,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("try to create new difficulty", () => Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo));
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog);
AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog.PerformOkAction());
AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction());
AddUntilStep("wait for created", () =>
{
@ -415,7 +415,7 @@ namespace osu.Game.Tests.Visual.Editing
if (sameRuleset)
{
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog);
AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog.PerformOkAction());
AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction());
}
AddUntilStep("wait for created", () =>
@ -476,7 +476,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("exit", () => Editor.Exit());
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is PromptForSaveDialog);
AddStep("attempt to save", () => DialogOverlay.CurrentDialog.PerformOkAction());
AddStep("attempt to save", () => DialogOverlay.CurrentDialog!.PerformOkAction());
AddAssert("editor is still current", () => Editor.IsCurrentScreen());
}

View File

@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.Editing
public void TestWidthUpdatesOnDrawSizeChanges()
{
AddStep("Shrink scroll container", () => scrollContainer.Width = 0.5f);
AddAssert("Scroll container width shrunk", () => scrollContainer.DrawWidth == scrollContainer.Parent.DrawWidth / 2);
AddAssert("Scroll container width shrunk", () => scrollContainer.DrawWidth == scrollContainer.Parent!.DrawWidth / 2);
AddAssert("Inner container width matches scroll container", () => innerBox.DrawWidth == scrollContainer.DrawWidth);
}

View File

@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Gameplay
var expectedComponentsAdjustmentContainer = new DependencyProvidingContainer
{
Position = actualComponentsContainer.Parent.ToSpaceOfOtherDrawable(actualComponentsContainer.DrawPosition, Content),
Position = actualComponentsContainer.Parent!.ToSpaceOfOtherDrawable(actualComponentsContainer.DrawPosition, Content),
Size = actualComponentsContainer.DrawSize,
Child = expectedComponentsContainer,
// proxy the same required dependencies that `actualComponentsContainer` is using.

View File

@ -220,7 +220,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public partial class TestInputConsumer : CompositeDrawable, IKeyBindingHandler<TestAction>
{
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent.ReceivePositionalInputAt(screenSpacePos);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent!.ReceivePositionalInputAt(screenSpacePos);
private readonly Box box;

View File

@ -259,7 +259,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public partial class TestInputConsumer : CompositeDrawable, IKeyBindingHandler<TestAction>
{
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent.ReceivePositionalInputAt(screenSpacePos);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent!.ReceivePositionalInputAt(screenSpacePos);
private readonly Box box;

View File

@ -188,7 +188,7 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep("placeholder shown", () =>
{
var notFoundDrawable = overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault();
return notFoundDrawable != null && notFoundDrawable.IsPresent && notFoundDrawable.Parent.DrawHeight > 0;
return notFoundDrawable != null && notFoundDrawable.IsPresent && notFoundDrawable.Parent!.DrawHeight > 0;
});
}

View File

@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.Playlists
});
});
AddUntilStep("Progress details are hidden", () => match.ChildrenOfType<RoomLocalUserInfo>().FirstOrDefault()?.Parent.Alpha == 0);
AddUntilStep("Progress details are hidden", () => match.ChildrenOfType<RoomLocalUserInfo>().FirstOrDefault()?.Parent!.Alpha == 0);
AddUntilStep("Leaderboard shows two aggregate scores", () => match.ChildrenOfType<MatchLeaderboardScore>().Count(s => s.ScoreText.Text != "0") == 2);
@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual.Playlists
});
});
AddUntilStep("Progress details are visible", () => match.ChildrenOfType<RoomLocalUserInfo>().FirstOrDefault()?.Parent.Alpha == 1);
AddUntilStep("Progress details are visible", () => match.ChildrenOfType<RoomLocalUserInfo>().FirstOrDefault()?.Parent!.Alpha == 1);
}
[Test]

View File

@ -66,20 +66,20 @@ namespace osu.Game.Tests.Visual.Settings
});
AddStep("set non-default value", () => revertToDefaultButton.Current.Value = "non-default");
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(revertToDefaultButton.Parent.DrawHeight, control.DrawHeight, 1));
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(revertToDefaultButton.Parent!.DrawHeight, control.DrawHeight, 1));
AddStep("set label", () => textBox.LabelText = "label text");
AddAssert("default value button centre aligned to label size", () =>
{
var label = textBox.ChildrenOfType<OsuSpriteText>().Single(spriteText => spriteText.Text == "label text");
return Precision.AlmostEquals(revertToDefaultButton.Parent.DrawHeight, label.DrawHeight, 1);
return Precision.AlmostEquals(revertToDefaultButton.Parent!.DrawHeight, label.DrawHeight, 1);
});
AddStep("clear label", () => textBox.LabelText = default);
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(revertToDefaultButton.Parent.DrawHeight, control.DrawHeight, 1));
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(revertToDefaultButton.Parent!.DrawHeight, control.DrawHeight, 1));
AddStep("set warning text", () => textBox.SetNoticeText("This is some very important warning text! Hopefully it doesn't break the alignment of the default value indicator...", true));
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(revertToDefaultButton.Parent.DrawHeight, control.DrawHeight, 1));
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(revertToDefaultButton.Parent!.DrawHeight, control.DrawHeight, 1));
}
/// <summary>

View File

@ -269,8 +269,8 @@ namespace osu.Game.Tests.Visual.UserInterface
if (!logoFacade.Transforms.Any() && !transferContainer.Transforms.Any())
{
Random random = new Random();
trackingContainer.Delay(500).MoveTo(new Vector2(random.Next(0, (int)logo.Parent.DrawWidth), random.Next(0, (int)logo.Parent.DrawHeight)), 300);
transferContainer.Delay(500).MoveTo(new Vector2(random.Next(0, (int)logo.Parent.DrawWidth), random.Next(0, (int)logo.Parent.DrawHeight)), 300);
trackingContainer.Delay(500).MoveTo(new Vector2(random.Next(0, (int)logo.Parent!.DrawWidth), random.Next(0, (int)logo.Parent!.DrawHeight)), 300);
transferContainer.Delay(500).MoveTo(new Vector2(random.Next(0, (int)logo.Parent!.DrawWidth), random.Next(0, (int)logo.Parent!.DrawHeight)), 300);
}
if (randomPositions)

View File

@ -0,0 +1,59 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneRoundedSliderBar : OsuManualInputManagerTestScene
{
[Cached]
private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Purple);
private readonly BindableDouble current = new BindableDouble(5)
{
Precision = 0.1f,
MinValue = 0,
MaxValue = 15
};
private RoundedSliderBar<double> slider = null!;
[BackgroundDependencyLoader]
private void load()
{
Child = slider = new RoundedSliderBar<double>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Current = current,
RelativeSizeAxes = Axes.X,
Width = 0.4f
};
}
[Test]
public void TestNubDoubleClickRevertToDefault()
{
AddStep("set slider to 1", () => slider.Current.Value = 1);
AddStep("move mouse to nub", () => InputManager.MoveMouseTo(slider.ChildrenOfType<Nub>().Single()));
AddStep("double click nub", () =>
{
InputManager.Click(MouseButton.Left);
InputManager.Click(MouseButton.Left);
});
AddAssert("slider is default", () => slider.Current.IsDefault);
}
}
}

View File

@ -1,15 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK.Input;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneShearedSliderBar : OsuTestScene
public partial class TestSceneShearedSliderBar : OsuManualInputManagerTestScene
{
[Cached]
private OverlayColourProvider colourProvider { get; set; } = new OverlayColourProvider(OverlayColourScheme.Purple);
@ -21,10 +25,12 @@ namespace osu.Game.Tests.Visual.UserInterface
MaxValue = 15
};
private ShearedSliderBar<double> slider = null!;
[BackgroundDependencyLoader]
private void load()
{
Child = new ShearedSliderBar<double>
Child = slider = new ShearedSliderBar<double>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -33,5 +39,21 @@ namespace osu.Game.Tests.Visual.UserInterface
Width = 0.4f
};
}
[Test]
public void TestNubDoubleClickRevertToDefault()
{
AddStep("set slider to 1", () => slider.Current.Value = 1);
AddStep("move mouse to nub", () => InputManager.MoveMouseTo(slider.ChildrenOfType<ShearedNub>().Single()));
AddStep("double click nub", () =>
{
InputManager.Click(MouseButton.Left);
InputManager.Click(MouseButton.Left);
});
AddAssert("slider is default", () => slider.Current.IsDefault);
}
}
}

View File

@ -15,7 +15,7 @@ namespace osu.Game.Tournament.Components
public new Bindable<DateTimeOffset>? Current
{
get => current;
set => current.Current = value;
set => current.Current = value!;
}
public DateTextBox()

View File

@ -61,8 +61,8 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
var topLeft = new Vector2(minX, minY);
OriginPosition = new Vector2(PathRadius);
Position = Parent.ToLocalSpace(topLeft);
Vertices = points.Select(p => Parent.ToLocalSpace(p) - Parent.ToLocalSpace(topLeft)).ToList();
Position = Parent!.ToLocalSpace(topLeft);
Vertices = points.Select(p => Parent!.ToLocalSpace(p) - Parent!.ToLocalSpace(topLeft)).ToList();
}
}
}

View File

@ -70,9 +70,9 @@ namespace osu.Game.Beatmaps
throw new NotImplementedException();
}
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new DummyBeatmapConverter { Beatmap = beatmap };
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new DummyBeatmapConverter(beatmap);
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => null;
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new NotImplementedException();
public override string Description => "dummy";
@ -80,9 +80,15 @@ namespace osu.Game.Beatmaps
private class DummyBeatmapConverter : IBeatmapConverter
{
public event Action<HitObject, IEnumerable<HitObject>> ObjectConverted;
public IBeatmap Beatmap { get; }
public IBeatmap Beatmap { get; set; }
public DummyBeatmapConverter(IBeatmap beatmap)
{
Beatmap = beatmap;
}
[CanBeNull]
public event Action<HitObject, IEnumerable<HitObject>> ObjectConverted;
public bool CanConvert() => true;

View File

@ -86,7 +86,7 @@ namespace osu.Game.Beatmaps
public event Action<WorkingBeatmap> OnInvalidated;
public virtual WorkingBeatmap GetWorkingBeatmap(BeatmapInfo beatmapInfo)
public virtual WorkingBeatmap GetWorkingBeatmap([CanBeNull] BeatmapInfo beatmapInfo)
{
if (beatmapInfo?.BeatmapSet == null)
return DefaultBeatmap;

View File

@ -184,6 +184,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.EditorShowHitMarkers, true);
SetDefault(OsuSetting.EditorAutoSeekOnPlacement, true);
SetDefault(OsuSetting.EditorLimitedDistanceSnap, false);
SetDefault(OsuSetting.EditorShowSpeedChanges, false);
SetDefault(OsuSetting.LastProcessedMetadataId, -1);
@ -407,5 +408,6 @@ namespace osu.Game.Configuration
EditorLimitedDistanceSnap,
ReplaySettingsOverlay,
AutomaticallyDownloadMissingBeatmaps,
EditorShowSpeedChanges
}
}

View File

@ -25,6 +25,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Input;
using osu.Game.Input.Bindings;
using osu.Game.Models;
using osu.Game.Online.API;
@ -34,6 +35,7 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Skinning;
using osuTK.Input;
using Realms;
using Realms.Exceptions;
@ -84,8 +86,9 @@ namespace osu.Game.Database
/// 32 2023-07-09 Populate legacy scores with the ScoreV2 mod (and restore TotalScore to the legacy total for such scores) using replay files.
/// 33 2023-08-16 Reset default chat toggle key binding to avoid conflict with newly added leaderboard toggle key binding.
/// 34 2023-08-21 Add BackgroundReprocessingFailed flag to ScoreInfo to track upgrade failures.
/// 35 2023-10-16 Clear key combinations of keybindings that are assigned to more than one action in a given settings section.
/// </summary>
private const int schema_version = 34;
private const int schema_version = 35;
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
@ -1031,6 +1034,47 @@ namespace osu.Game.Database
break;
}
case 35:
{
// catch used `Shift` twice as a default key combination for dash, which generally was bothersome and causes issues elsewhere.
// the duplicate binding logic below had to account for it, it could also break keybinding conflict resolution on revert-to-default.
// as such, detect this situation and fix it before proceeding further.
var catchDashBindings = migration.NewRealm.All<RealmKeyBinding>()
.Where(kb => kb.RulesetName == @"fruits" && kb.ActionInt == 2)
.ToList();
if (catchDashBindings.All(kb => kb.KeyCombination.Equals(new KeyCombination(InputKey.Shift))))
{
Debug.Assert(catchDashBindings.Count == 2);
catchDashBindings.Last().KeyCombination = KeyCombination.FromMouseButton(MouseButton.Left);
}
// with the catch case dealt with, de-duplicate the remaining bindings.
int countCleared = 0;
var globalBindings = migration.NewRealm.All<RealmKeyBinding>().Where(kb => kb.RulesetName == null).ToList();
foreach (var category in Enum.GetValues<GlobalActionCategory>())
{
var categoryActions = GlobalActionContainer.GetGlobalActionsFor(category).Cast<int>().ToHashSet();
var categoryBindings = globalBindings.Where(kb => categoryActions.Contains(kb.ActionInt));
countCleared += RealmKeyBindingStore.ClearDuplicateBindings(categoryBindings);
}
var rulesetBindings = migration.NewRealm.All<RealmKeyBinding>().Where(kb => kb.RulesetName != null).ToList();
foreach (var variantGroup in rulesetBindings.GroupBy(kb => (kb.RulesetName, kb.Variant)))
countCleared += RealmKeyBindingStore.ClearDuplicateBindings(variantGroup);
if (countCleared > 0)
{
Logger.Log($"{countCleared} of your keybinding(s) have been cleared due to being bound to multiple actions. "
+ "Please choose new unique ones in the settings panel.", level: LogLevel.Important);
}
break;
}
}
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");

View File

@ -43,7 +43,7 @@ namespace osu.Game.Extensions
/// <param name="delta">A delta in screen-space coordinates.</param>
/// <returns>The delta vector in Parent's coordinates.</returns>
public static Vector2 ScreenSpaceDeltaToParentSpace(this Drawable drawable, Vector2 delta) =>
drawable.Parent.ToLocalSpace(drawable.Parent.ToScreenSpace(Vector2.Zero) + delta);
drawable.Parent!.ToLocalSpace(drawable.Parent!.ToScreenSpace(Vector2.Zero) + delta);
/// <summary>
/// Some elements don't handle rewind correctly and fixing them is non-trivial.

View File

@ -22,7 +22,7 @@ namespace osu.Game.Graphics.Backgrounds
Sprite.Texture = skin.GetTexture("menu-background") ?? Sprite.Texture;
}
public override bool Equals(Background other)
public override bool Equals(Background? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;

View File

@ -76,10 +76,10 @@ namespace osu.Game.Graphics.Containers
/// <remarks>Will only be correct if the logo's <see cref="Drawable.RelativePositionAxes"/> are set to Axes.Both</remarks>
protected Vector2 ComputeLogoTrackingPosition()
{
var absolutePos = Logo.Parent.ToLocalSpace(LogoFacade.ScreenSpaceDrawQuad.Centre);
var absolutePos = Logo.Parent!.ToLocalSpace(LogoFacade.ScreenSpaceDrawQuad.Centre);
return new Vector2(absolutePos.X / Logo.Parent.RelativeToAbsoluteFactor.X,
absolutePos.Y / Logo.Parent.RelativeToAbsoluteFactor.Y);
return new Vector2(absolutePos.X / Logo.Parent!.RelativeToAbsoluteFactor.X,
absolutePos.Y / Logo.Parent!.RelativeToAbsoluteFactor.Y);
}
protected override void Update()

View File

@ -119,11 +119,11 @@ namespace osu.Game.Graphics.Containers
headerBackgroundContainer.Clear();
headerBackground = value;
if (value == null) return;
headerBackgroundContainer.Add(headerBackground);
lastKnownScroll = null;
if (headerBackground != null)
{
headerBackgroundContainer.Add(headerBackground);
lastKnownScroll = null;
}
}
}

View File

@ -48,7 +48,7 @@ namespace osu.Game.Graphics.Containers
private void keepUprightAndUnstretched()
{
// Decomposes the inverse of the parent DrawInfo.Matrix into rotation, shear and scale.
var parentMatrix = Parent.DrawInfo.Matrix;
var parentMatrix = Parent!.DrawInfo.Matrix;
// Remove Translation.>
parentMatrix.M31 = 0.0f;

View File

@ -178,7 +178,7 @@ namespace osu.Game.Graphics.Containers
// We can not use RelativeSizeAxes for Height, because the height
// of our parent diminishes as the content moves up.
Height = Parent.Parent.DrawSize.Y * 1.5f;
Height = Parent!.Parent!.DrawSize.Y * 1.5f;
}
protected override void PopIn() => Schedule(() => this.MoveToY(FinalPosition, APPEAR_DURATION, easing_show));
@ -188,7 +188,7 @@ namespace osu.Game.Graphics.Containers
double duration = IsLoaded ? DISAPPEAR_DURATION : 0;
// scheduling is required as parent may not be present at the time this is called.
Schedule(() => this.MoveToY(Parent.Parent.DrawSize.Y, duration, easing_hide));
Schedule(() => this.MoveToY(Parent!.Parent!.DrawSize.Y, duration, easing_hide));
}
}
}

View File

@ -62,7 +62,7 @@ namespace osu.Game.Graphics.UserInterface
if (!PlaySamplesOnAdjust)
return;
if (Clock == null || Clock.CurrentTime - lastSampleTime <= 30)
if (Clock.CurrentTime - lastSampleTime <= 30)
return;
if (value.Equals(lastSampleValue))

View File

@ -93,11 +93,12 @@ namespace osu.Game.Graphics.UserInterface
nubContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Child = Nub = new Nub
Child = Nub = new SliderNub
{
Origin = Anchor.TopCentre,
RelativePositionAxes = Axes.X,
Current = { Value = true }
Current = { Value = true },
OnDoubleClicked = () => Current.SetDefault(),
},
},
hoverClickSounds = new HoverClickSounds()
@ -166,5 +167,18 @@ namespace osu.Game.Graphics.UserInterface
{
Nub.MoveToX(value, 250, Easing.OutQuint);
}
public partial class SliderNub : Nub
{
public Action? OnDoubleClicked { get; init; }
protected override bool OnClick(ClickEvent e) => true;
protected override bool OnDoubleClick(DoubleClickEvent e)
{
OnDoubleClicked?.Invoke();
return true;
}
}
}
}

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
@ -18,6 +19,8 @@ namespace osu.Game.Graphics.UserInterface
{
public partial class ShearedNub : Container, IHasCurrentValue<bool>, IHasAccentColour
{
public Action? OnDoubleClicked { get; init; }
protected const float BORDER_WIDTH = 3;
public const int HEIGHT = 30;
@ -179,5 +182,13 @@ namespace osu.Game.Graphics.UserInterface
main.TransformTo(nameof(BorderThickness), BORDER_WIDTH, duration, Easing.OutQuint);
}
}
protected override bool OnClick(ClickEvent e) => true;
protected override bool OnDoubleClick(DoubleClickEvent e)
{
OnDoubleClicked?.Invoke();
return true;
}
}
}

View File

@ -100,7 +100,8 @@ namespace osu.Game.Graphics.UserInterface
X = -SHEAR.X * HEIGHT / 2f,
Origin = Anchor.TopCentre,
RelativePositionAxes = Axes.X,
Current = { Value = true }
Current = { Value = true },
OnDoubleClicked = () => Current.SetDefault(),
},
},
hoverClickSounds = new HoverClickSounds()

View File

@ -13,7 +13,7 @@ namespace osu.Game.IO
public bool IsManaged => ID > 0;
public string Hash { get; set; }
public string Hash { get; set; } = string.Empty;
public int ReferenceCount { get; set; }
}

View File

@ -130,5 +130,31 @@ namespace osu.Game.Input
return true;
}
/// <summary>
/// Clears all <see cref="RealmKeyBinding.KeyCombination"/>s from the provided <paramref name="keyBindings"/>
/// which are assigned to more than one binding.
/// </summary>
/// <param name="keyBindings">The <see cref="RealmKeyBinding"/>s to de-duplicate.</param>
/// <returns>Number of bindings cleared.</returns>
public static int ClearDuplicateBindings(IEnumerable<IKeyBinding> keyBindings)
{
int countRemoved = 0;
var lookup = keyBindings.ToLookup(kb => kb.KeyCombination);
foreach (var group in lookup)
{
if (group.Select(kb => kb.Action).Distinct().Count() <= 1)
continue;
foreach (var binding in group)
binding.KeyCombination = new KeyCombination(InputKey.None);
countRemoved += group.Count();
}
return countRemoved;
}
}
}

View File

@ -29,7 +29,7 @@ namespace osu.Game.Online.API.Requests.Responses
public DateTimeOffset JoinDate;
[JsonProperty(@"username")]
public string Username { get; set; }
public string Username { get; set; } = string.Empty;
[JsonProperty(@"previous_usernames")]
public string[] PreviousUsernames;

View File

@ -22,6 +22,6 @@ namespace osu.Game.Online.Placeholders
AddText(this.message = message);
}
public override bool Equals(Placeholder other) => (other as MessagePlaceholder)?.message == message;
public override bool Equals(Placeholder? other) => (other as MessagePlaceholder)?.message == message;
}
}

View File

@ -267,7 +267,7 @@ namespace osu.Game.Overlays
if (!isDraggingTopBar)
return;
float targetChatHeight = dragStartChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent.DrawSize.Y;
float targetChatHeight = dragStartChatHeight - (e.MousePosition.Y - e.MouseDownPosition.Y) / Parent!.DrawSize.Y;
chatHeight.Value = targetChatHeight;
}

View File

@ -17,7 +17,7 @@ namespace osu.Game.Overlays
public abstract partial class FullscreenOverlay<T> : WaveOverlayContainer, INamedOverlayComponent
where T : OverlayHeader
{
public virtual string IconTexture => Header.Title.IconTexture ?? string.Empty;
public virtual string IconTexture => Header.Title.IconTexture;
public virtual LocalisableString Title => Header.Title.Title;
public virtual LocalisableString Description => Header.Title.Description;

View File

@ -604,7 +604,7 @@ namespace osu.Game.Overlays.Mods
if (columnNumber > 5 && !column.Active.Value) return;
// use X position of the column on screen as a basis for panning the sample
float balance = column.Parent.BoundingBox.Centre.X / RelativeToAbsoluteFactor.X;
float balance = column.Parent!.BoundingBox.Centre.X / RelativeToAbsoluteFactor.X;
// dip frequency and ramp volume of sample over the first 5 displayed columns
float progress = Math.Min(1, columnNumber / 5f);

View File

@ -248,7 +248,7 @@ namespace osu.Game.Overlays
{
base.UpdateAfterChildren();
playlistContainer.Height = MathF.Min(Parent.DrawHeight - margin * 3 - player_height, PlaylistOverlay.PLAYLIST_HEIGHT);
playlistContainer.Height = MathF.Min(Parent!.DrawHeight - margin * 3 - player_height, PlaylistOverlay.PLAYLIST_HEIGHT);
float height = player_height;

View File

@ -241,7 +241,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
protected override void OnDrag(DragEvent e)
{
var newPos = Position + e.Delta;
this.MoveTo(Vector2.Clamp(newPos, Vector2.Zero, Parent.Size));
this.MoveTo(Vector2.Clamp(newPos, Vector2.Zero, Parent!.Size));
}
protected override void OnDragEnd(DragEndEvent e)

View File

@ -202,7 +202,7 @@ namespace osu.Game.Overlays.SkinEditor
if (drawable.Parent == null)
return;
var newAnchor = drawable.Parent.ToSpaceOfOtherDrawable(drawable.AnchorPosition, this);
var newAnchor = drawable.Parent!.ToSpaceOfOtherDrawable(drawable.AnchorPosition, this);
anchorPosition = tweenPosition(anchorPosition ?? newAnchor, newAnchor);
anchorBox.Position = anchorPosition.Value;

View File

@ -135,7 +135,7 @@ namespace osu.Game.Overlays.SkinEditor
{
var drawableItem = (Drawable)b.Item;
var flippedPosition = GeometryUtils.GetFlippedPosition(direction, flipOverOrigin ? drawableItem.Parent.ScreenSpaceDrawQuad : selectionQuad, b.ScreenSpaceSelectionPoint);
var flippedPosition = GeometryUtils.GetFlippedPosition(direction, flipOverOrigin ? drawableItem.Parent!.ScreenSpaceDrawQuad : selectionQuad, b.ScreenSpaceSelectionPoint);
updateDrawablePosition(drawableItem, flippedPosition);
@ -242,7 +242,7 @@ namespace osu.Game.Overlays.SkinEditor
private static void updateDrawablePosition(Drawable drawable, Vector2 screenSpacePosition)
{
drawable.Position =
drawable.Parent.ToLocalSpace(screenSpacePosition) - drawable.AnchorPosition;
drawable.Parent!.ToLocalSpace(screenSpacePosition) - drawable.AnchorPosition;
}
private void applyOrigins(Anchor origin)

View File

@ -63,7 +63,7 @@ namespace osu.Game.Overlays.Toolbar
protected override bool OnClick(ClickEvent e)
{
Parent.TriggerClick();
Parent!.TriggerClick();
return base.OnClick(e);
}
}

View File

@ -81,10 +81,10 @@ namespace osu.Game.Overlays.Wiki.Markdown
{
base.Update();
if (Width > Parent.DrawWidth)
if (Width > Parent!.DrawWidth)
{
float ratio = Height / Width;
Width = Parent.DrawWidth;
Width = Parent!.DrawWidth;
Height = ratio * Width;
}
}

View File

@ -75,7 +75,7 @@ namespace osu.Game.Overlays.Wiki
protected override void Update()
{
base.Update();
Height = Math.Max(panelContainer.Height, Parent.DrawHeight);
Height = Math.Max(panelContainer.Height, Parent!.DrawHeight);
}
private partial class WikiPanelMarkdownContainer : WikiMarkdownContainer

View File

@ -1,8 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@ -24,16 +23,13 @@ using osu.Game.Overlays.OSD;
using osu.Game.Overlays.Settings.Sections;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.TernaryButtons;
namespace osu.Game.Rulesets.Edit
{
/// <summary>
/// Represents a <see cref="HitObjectComposer{TObject}"/> for rulesets with the concept of distances between objects.
/// </summary>
/// <typeparam name="TObject">The base type of supported objects.</typeparam>
public abstract partial class DistancedHitObjectComposer<TObject> : HitObjectComposer<TObject>, IDistanceSnapProvider, IScrollBindingHandler<GlobalAction>
where TObject : HitObject
public abstract partial class ComposerDistanceSnapProvider : Component, IDistanceSnapProvider, IScrollBindingHandler<GlobalAction>
{
private const float adjust_step = 0.1f;
@ -44,27 +40,38 @@ namespace osu.Game.Rulesets.Edit
Precision = 0.01,
};
IBindable<double> IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier;
Bindable<double> IDistanceSnapProvider.DistanceSpacingMultiplier => DistanceSpacingMultiplier;
private ExpandableSlider<double, SizeSlider<double>> distanceSpacingSlider;
private ExpandableButton currentDistanceSpacingButton;
private ExpandableSlider<double, SizeSlider<double>> distanceSpacingSlider = null!;
private ExpandableButton currentDistanceSpacingButton = null!;
[Resolved(canBeNull: true)]
private OnScreenDisplay onScreenDisplay { get; set; }
[Resolved]
private Playfield playfield { get; set; } = null!;
protected readonly Bindable<TernaryState> DistanceSnapToggle = new Bindable<TernaryState>();
[Resolved]
private EditorClock editorClock { get; set; } = null!;
[Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!;
[Resolved]
private IBeatSnapProvider beatSnapProvider { get; set; } = null!;
[Resolved]
private OnScreenDisplay? onScreenDisplay { get; set; }
public readonly Bindable<TernaryState> DistanceSnapToggle = new Bindable<TernaryState>();
private bool distanceSnapMomentary;
protected DistancedHitObjectComposer(Ruleset ruleset)
: base(ruleset)
{
}
private EditorToolboxGroup? toolboxGroup;
[BackgroundDependencyLoader]
private void load()
public void AttachToToolbox(ExpandingToolboxContainer toolboxContainer)
{
RightToolbox.Add(new EditorToolboxGroup("snapping")
if (toolboxGroup != null)
throw new InvalidOperationException($"{nameof(AttachToToolbox)} may be called only once for a single {nameof(ComposerDistanceSnapProvider)} instance.");
toolboxContainer.Add(toolboxGroup = new EditorToolboxGroup("snapping")
{
Alpha = DistanceSpacingMultiplier.Disabled ? 0 : 1,
Children = new Drawable[]
@ -90,16 +97,42 @@ namespace osu.Game.Rulesets.Edit
}
}
});
if (DistanceSpacingMultiplier.Disabled)
{
distanceSpacingSlider.Hide();
return;
}
DistanceSpacingMultiplier.Value = editorBeatmap.BeatmapInfo.DistanceSpacing;
DistanceSpacingMultiplier.BindValueChanged(multiplier =>
{
distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})";
distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({multiplier.NewValue:0.##x})";
if (multiplier.NewValue != multiplier.OldValue)
onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier));
editorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue;
}, true);
// Manual binding to handle enabling distance spacing when the slider is interacted with.
distanceSpacingSlider.Current.BindValueChanged(spacing =>
{
DistanceSpacingMultiplier.Value = spacing.NewValue;
DistanceSnapToggle.Value = TernaryState.True;
});
DistanceSpacingMultiplier.BindValueChanged(spacing => distanceSpacingSlider.Current.Value = spacing.NewValue);
}
private (HitObject before, HitObject after)? getObjectsOnEitherSideOfCurrentTime()
{
HitObject lastBefore = Playfield.HitObjectContainer.AliveObjects.LastOrDefault(h => h.HitObject.StartTime < EditorClock.CurrentTime)?.HitObject;
HitObject? lastBefore = playfield.HitObjectContainer.AliveObjects.LastOrDefault(h => h.HitObject.StartTime < editorClock.CurrentTime)?.HitObject;
if (lastBefore == null)
return null;
HitObject firstAfter = Playfield.HitObjectContainer.AliveObjects.FirstOrDefault(h => h.HitObject.StartTime >= EditorClock.CurrentTime)?.HitObject;
HitObject? firstAfter = playfield.HitObjectContainer.AliveObjects.FirstOrDefault(h => h.HitObject.StartTime >= editorClock.CurrentTime)?.HitObject;
if (firstAfter == null)
return null;
@ -138,41 +171,10 @@ namespace osu.Game.Rulesets.Edit
}
}
protected override void LoadComplete()
{
base.LoadComplete();
if (DistanceSpacingMultiplier.Disabled)
{
distanceSpacingSlider.Hide();
return;
}
DistanceSpacingMultiplier.Value = EditorBeatmap.BeatmapInfo.DistanceSpacing;
DistanceSpacingMultiplier.BindValueChanged(multiplier =>
{
distanceSpacingSlider.ContractedLabelText = $"D. S. ({multiplier.NewValue:0.##x})";
distanceSpacingSlider.ExpandedLabelText = $"Distance Spacing ({multiplier.NewValue:0.##x})";
if (multiplier.NewValue != multiplier.OldValue)
onScreenDisplay?.Display(new DistanceSpacingToast(multiplier.NewValue.ToLocalisableString(@"0.##x"), multiplier));
EditorBeatmap.BeatmapInfo.DistanceSpacing = multiplier.NewValue;
}, true);
// Manual binding to handle enabling distance spacing when the slider is interacted with.
distanceSpacingSlider.Current.BindValueChanged(spacing =>
{
DistanceSpacingMultiplier.Value = spacing.NewValue;
DistanceSnapToggle.Value = TernaryState.True;
});
DistanceSpacingMultiplier.BindValueChanged(spacing => distanceSpacingSlider.Current.Value = spacing.NewValue);
}
protected override IEnumerable<TernaryButton> CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[]
public IEnumerable<TernaryButton> CreateTernaryButtons() => new[]
{
new TernaryButton(DistanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler })
});
};
protected override bool OnKeyDown(KeyDownEvent e)
{
@ -242,26 +244,28 @@ namespace osu.Game.Rulesets.Edit
return true;
}
#region IDistanceSnapProvider
public virtual float GetBeatSnapDistanceAt(HitObject referenceObject, bool useReferenceSliderVelocity = true)
{
return (float)(100 * (useReferenceSliderVelocity && referenceObject is IHasSliderVelocity hasSliderVelocity ? hasSliderVelocity.SliderVelocityMultiplier : 1) * EditorBeatmap.Difficulty.SliderMultiplier * 1
/ BeatSnapProvider.BeatDivisor);
return (float)(100 * (useReferenceSliderVelocity && referenceObject is IHasSliderVelocity hasSliderVelocity ? hasSliderVelocity.SliderVelocityMultiplier : 1) * editorBeatmap.Difficulty.SliderMultiplier * 1
/ beatSnapProvider.BeatDivisor);
}
public virtual float DurationToDistance(HitObject referenceObject, double duration)
{
double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime);
double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime);
return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceObject));
}
public virtual double DistanceToDuration(HitObject referenceObject, float distance)
{
double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime);
double beatLength = beatSnapProvider.GetBeatLengthAtTime(referenceObject.StartTime);
return distance / GetBeatSnapDistanceAt(referenceObject) * beatLength;
}
public virtual double FindSnappedDuration(HitObject referenceObject, float distance)
=> BeatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime;
=> beatSnapProvider.SnapTime(referenceObject.StartTime + DistanceToDuration(referenceObject, distance), referenceObject.StartTime) - referenceObject.StartTime;
public virtual float FindSnappedDistance(HitObject referenceObject, float distance)
{
@ -269,9 +273,9 @@ namespace osu.Game.Rulesets.Edit
double actualDuration = startTime + DistanceToDuration(referenceObject, distance);
double snappedEndTime = BeatSnapProvider.SnapTime(actualDuration, startTime);
double snappedEndTime = beatSnapProvider.SnapTime(actualDuration, startTime);
double beatLength = BeatSnapProvider.GetBeatLengthAtTime(startTime);
double beatLength = beatSnapProvider.GetBeatLengthAtTime(startTime);
// we don't want to exceed the actual duration and snap to a point in the future.
// as we are snapping to beat length via SnapTime (which will round-to-nearest), check for snapping in the forward direction and reverse it.
@ -281,6 +285,8 @@ namespace osu.Game.Rulesets.Edit
return DurationToDistance(referenceObject, snappedEndTime - startTime);
}
#endregion
private partial class DistanceSpacingToast : Toast
{
private readonly ValueChangedEvent<double> change;

View File

@ -12,14 +12,14 @@ namespace osu.Game.Rulesets.Edit
/// A snap provider which given a reference hit object and proposed distance from it, offers a more correct duration or distance value.
/// </summary>
[Cached]
public interface IDistanceSnapProvider : IPositionSnapProvider
public interface IDistanceSnapProvider
{
/// <summary>
/// A multiplier which changes the ratio of distance travelled per time unit.
/// Importantly, this is provided for manual usage, and not multiplied into any of the methods exposed by this interface.
/// </summary>
/// <seealso cref="BeatmapInfo.DistanceSpacing"/>
IBindable<double> DistanceSpacingMultiplier { get; }
Bindable<double> DistanceSpacingMultiplier { get; }
/// <summary>
/// Retrieves the distance between two points within a timing point that are one beat length apart.

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI.Scrolling;
@ -18,6 +19,7 @@ namespace osu.Game.Rulesets.Edit
where TObject : HitObject
{
private readonly Bindable<TernaryState> showSpeedChanges = new Bindable<TernaryState>();
private Bindable<bool> configShowSpeedChanges = null!;
protected ScrollingHitObjectComposer(Ruleset ruleset)
: base(ruleset)
@ -25,7 +27,7 @@ namespace osu.Game.Rulesets.Edit
}
[BackgroundDependencyLoader]
private void load()
private void load(OsuConfigManager config)
{
if (DrawableRuleset is ISupportConstantAlgorithmToggle toggleRuleset)
{
@ -44,7 +46,16 @@ namespace osu.Game.Rulesets.Edit
},
});
showSpeedChanges.BindValueChanged(state => toggleRuleset.ShowSpeedChanges.Value = state.NewValue == TernaryState.True, true);
configShowSpeedChanges = config.GetBindable<bool>(OsuSetting.EditorShowSpeedChanges);
configShowSpeedChanges.BindValueChanged(enabled => showSpeedChanges.Value = enabled.NewValue ? TernaryState.True : TernaryState.False, true);
showSpeedChanges.BindValueChanged(state =>
{
bool enabled = state.NewValue == TernaryState.True;
toggleRuleset.ShowSpeedChanges.Value = enabled;
configShowSpeedChanges.Value = enabled;
}, true);
}
}
}

View File

@ -71,7 +71,6 @@ namespace osu.Game.Rulesets.Mods
// Apply a fixed rate change when missing, allowing the player to catch up when the rate is too fast.
private const double rate_change_on_miss = 0.95d;
private IAdjustableAudioComponent? track;
private double targetRate = 1d;
/// <summary>
@ -123,24 +122,27 @@ namespace osu.Game.Rulesets.Mods
/// </summary>
private readonly Dictionary<HitObject, double> ratesForRewinding = new Dictionary<HitObject, double>();
private readonly RateAdjustModHelper rateAdjustHelper;
public ModAdaptiveSpeed()
{
rateAdjustHelper = new RateAdjustModHelper(SpeedChange);
rateAdjustHelper.HandleAudioAdjustments(AdjustPitch);
InitialRate.BindValueChanged(val =>
{
SpeedChange.Value = val.NewValue;
targetRate = val.NewValue;
});
AdjustPitch.BindValueChanged(adjustPitchChanged);
}
public void ApplyToTrack(IAdjustableAudioComponent track)
{
this.track = track;
InitialRate.TriggerChange();
AdjustPitch.TriggerChange();
recentRates.Clear();
recentRates.AddRange(Enumerable.Repeat(InitialRate.Value, recent_rate_count));
rateAdjustHelper.ApplyToTrack(track);
}
public void ApplyToSample(IAdjustableAudioComponent sample)
@ -199,15 +201,6 @@ namespace osu.Game.Rulesets.Mods
}
}
private void adjustPitchChanged(ValueChangedEvent<bool> adjustPitchSetting)
{
track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange);
track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange);
}
private AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue)
=> adjustPitchSettingValue ? AdjustableProperty.Frequency : AdjustableProperty.Tempo;
private IEnumerable<HitObject> getAllApplicableHitObjects(IEnumerable<HitObject> hitObjects)
{
foreach (var hitObject in hitObjects)

View File

@ -5,21 +5,36 @@ using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
namespace osu.Game.Rulesets.Mods
{
public abstract class ModDaycore : ModHalfTime
public abstract class ModDaycore : ModRateAdjust
{
public override string Name => "Daycore";
public override string Acronym => "DC";
public override IconUsage? Icon => null;
public override ModType Type => ModType.DifficultyReduction;
public override LocalisableString Description => "Whoaaaaa...";
[SettingSource("Speed decrease", "The actual decrease to apply")]
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(0.75)
{
MinValue = 0.5,
MaxValue = 0.99,
Precision = 0.01,
};
private readonly BindableNumber<double> tempoAdjust = new BindableDouble(1);
private readonly BindableNumber<double> freqAdjust = new BindableDouble(1);
private readonly RateAdjustModHelper rateAdjustHelper;
protected ModDaycore()
{
rateAdjustHelper = new RateAdjustModHelper(SpeedChange);
// intentionally not deferring the speed change handling to `RateAdjustModHelper`
// as the expected result of operation is not the same (daycore should preserve constant pitch).
SpeedChange.BindValueChanged(val =>
{
freqAdjust.Value = SpeedChange.Default;
@ -29,9 +44,10 @@ namespace osu.Game.Rulesets.Mods
public override void ApplyToTrack(IAdjustableAudioComponent track)
{
// base.ApplyToTrack() intentionally not called (different tempo adjustment is applied)
track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust);
track.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust);
}
public override double ScoreMultiplier => rateAdjustHelper.ScoreMultiplier;
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
@ -26,21 +27,22 @@ namespace osu.Game.Rulesets.Mods
Precision = 0.01,
};
public override double ScoreMultiplier
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
public virtual BindableBool AdjustPitch { get; } = new BindableBool();
private readonly RateAdjustModHelper rateAdjustHelper;
protected ModDoubleTime()
{
get
{
// Round to the nearest multiple of 0.1.
double value = (int)(SpeedChange.Value * 10) / 10.0;
// Offset back to 0.
value -= 1;
// Each 0.1 multiple changes score multiplier by 0.02.
value /= 5;
return 1 + value;
}
rateAdjustHelper = new RateAdjustModHelper(SpeedChange);
rateAdjustHelper.HandleAudioAdjustments(AdjustPitch);
}
public override void ApplyToTrack(IAdjustableAudioComponent track)
{
rateAdjustHelper.ApplyToTrack(track);
}
public override double ScoreMultiplier => rateAdjustHelper.ScoreMultiplier;
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
@ -26,18 +27,22 @@ namespace osu.Game.Rulesets.Mods
Precision = 0.01,
};
public override double ScoreMultiplier
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
public virtual BindableBool AdjustPitch { get; } = new BindableBool();
private readonly RateAdjustModHelper rateAdjustHelper;
protected ModHalfTime()
{
get
{
// Round to the nearest multiple of 0.1.
double value = (int)(SpeedChange.Value * 10) / 10.0;
// Offset back to 0.
value -= 1;
return 1 + value;
}
rateAdjustHelper = new RateAdjustModHelper(SpeedChange);
rateAdjustHelper.HandleAudioAdjustments(AdjustPitch);
}
public override void ApplyToTrack(IAdjustableAudioComponent track)
{
rateAdjustHelper.ApplyToTrack(track);
}
public override double ScoreMultiplier => rateAdjustHelper.ScoreMultiplier;
}
}

View File

@ -11,6 +11,7 @@ using osu.Framework.Localisation;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Objects;
@ -19,22 +20,33 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Mods
{
public abstract class ModNightcore : ModDoubleTime
public abstract class ModNightcore : ModRateAdjust
{
public override string Name => "Nightcore";
public override string Acronym => "NC";
public override IconUsage? Icon => OsuIcon.ModNightcore;
public override ModType Type => ModType.DifficultyIncrease;
public override LocalisableString Description => "Uguuuuuuuu...";
}
public abstract partial class ModNightcore<TObject> : ModNightcore, IApplicableToDrawableRuleset<TObject>
where TObject : HitObject
{
[SettingSource("Speed increase", "The actual increase to apply")]
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(1.5)
{
MinValue = 1.01,
MaxValue = 2,
Precision = 0.01,
};
private readonly BindableNumber<double> tempoAdjust = new BindableDouble(1);
private readonly BindableNumber<double> freqAdjust = new BindableDouble(1);
private readonly RateAdjustModHelper rateAdjustHelper;
protected ModNightcore()
{
rateAdjustHelper = new RateAdjustModHelper(SpeedChange);
// intentionally not deferring the speed change handling to `RateAdjustModHelper`
// as the expected result of operation is not the same (nightcore should preserve constant pitch).
SpeedChange.BindValueChanged(val =>
{
freqAdjust.Value = SpeedChange.Default;
@ -44,11 +56,16 @@ namespace osu.Game.Rulesets.Mods
public override void ApplyToTrack(IAdjustableAudioComponent track)
{
// base.ApplyToTrack() intentionally not called (different tempo adjustment is applied)
track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust);
track.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust);
}
public override double ScoreMultiplier => rateAdjustHelper.ScoreMultiplier;
}
public abstract partial class ModNightcore<TObject> : ModNightcore, IApplicableToDrawableRuleset<TObject>
where TObject : HitObject
{
public void ApplyToDrawableRuleset(DrawableRuleset<TObject> drawableRuleset)
{
drawableRuleset.Overlays.Add(new NightcoreBeatContainer());

View File

@ -13,10 +13,7 @@ namespace osu.Game.Rulesets.Mods
public abstract BindableNumber<double> SpeedChange { get; }
public virtual void ApplyToTrack(IAdjustableAudioComponent track)
{
track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange);
}
public abstract void ApplyToTrack(IAdjustableAudioComponent track);
public virtual void ApplyToSample(IAdjustableAudioComponent sample)
{

View File

@ -44,21 +44,21 @@ namespace osu.Game.Rulesets.Mods
Precision = 0.01,
};
private IAdjustableAudioComponent? track;
private readonly RateAdjustModHelper rateAdjustHelper;
protected ModTimeRamp()
{
rateAdjustHelper = new RateAdjustModHelper(SpeedChange);
rateAdjustHelper.HandleAudioAdjustments(AdjustPitch);
// for preview purpose at song select. eventually we'll want to be able to update every frame.
FinalRate.BindValueChanged(_ => applyRateAdjustment(double.PositiveInfinity), true);
AdjustPitch.BindValueChanged(applyPitchAdjustment);
}
public void ApplyToTrack(IAdjustableAudioComponent track)
{
this.track = track;
rateAdjustHelper.ApplyToTrack(track);
FinalRate.TriggerChange();
AdjustPitch.TriggerChange();
}
public void ApplyToSample(IAdjustableAudioComponent sample)
@ -95,16 +95,5 @@ namespace osu.Game.Rulesets.Mods
/// Adjust the rate along the specified ramp.
/// </summary>
private void applyRateAdjustment(double time) => SpeedChange.Value = ApplyToRate(time);
private void applyPitchAdjustment(ValueChangedEvent<bool> adjustPitchSetting)
{
// remove existing old adjustment
track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange);
track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange);
}
private AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue)
=> adjustPitchSettingValue ? AdjustableProperty.Frequency : AdjustableProperty.Tempo;
}
}

View File

@ -0,0 +1,84 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Audio;
using osu.Framework.Bindables;
namespace osu.Game.Rulesets.Mods
{
/// <summary>
/// Provides common functionality shared across various rate adjust mods.
/// </summary>
public class RateAdjustModHelper : IApplicableToTrack
{
public readonly IBindableNumber<double> SpeedChange;
private IAdjustableAudioComponent? track;
private BindableBool? adjustPitch;
/// <summary>
/// The score multiplier for the current <see cref="SpeedChange"/>.
/// </summary>
public double ScoreMultiplier
{
get
{
// Round to the nearest multiple of 0.1.
double value = (int)(SpeedChange.Value * 10) / 10.0;
// Offset back to 0.
value -= 1;
if (SpeedChange.Value >= 1)
value /= 5;
return 1 + value;
}
}
/// <summary>
/// Construct a new <see cref="RateAdjustModHelper"/>.
/// </summary>
/// <param name="speedChange">The main speed adjust parameter which is exposed to the user.</param>
public RateAdjustModHelper(IBindableNumber<double> speedChange)
{
SpeedChange = speedChange;
}
/// <summary>
/// Setup audio track adjustments for a rate adjust mod.
/// Importantly, <see cref="ApplyToTrack"/> must be called when a track is obtained/changed for this to work.
/// </summary>
/// <param name="adjustPitch">The "adjust pitch" setting as exposed to the user.</param>
public void HandleAudioAdjustments(BindableBool adjustPitch)
{
this.adjustPitch = adjustPitch;
// When switching between pitch adjust, we need to update adjustments to time-shift or frequency-scale.
adjustPitch.BindValueChanged(adjustPitchSetting =>
{
track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange);
track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange);
AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue)
=> adjustPitchSettingValue ? AdjustableProperty.Frequency : AdjustableProperty.Tempo;
});
}
/// <summary>
/// Should be invoked when a track is obtained / changed.
/// </summary>
/// <param name="track">The new track.</param>
/// <exception cref="InvalidOperationException">If this method is called before <see cref="HandleAudioAdjustments"/>.</exception>
public void ApplyToTrack(IAdjustableAudioComponent track)
{
if (adjustPitch == null)
throw new InvalidOperationException($"Must call {nameof(HandleAudioAdjustments)} first");
this.track = track;
adjustPitch.TriggerChange();
}
}
}

View File

@ -23,11 +23,12 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// <summary>
/// <see cref="ConvertSlider"/>s don't need a curve since they're converted to ruleset-specific hitobjects.
/// </summary>
public SliderPath Path { get; set; }
public SliderPath Path { get; set; } = null!;
public double Distance => Path.Distance;
public IList<IList<HitSampleInfo>> NodeSamples { get; set; }
public IList<IList<HitSampleInfo>> NodeSamples { get; set; } = null!;
public int RepeatCount { get; set; }
[JsonIgnore]

View File

@ -186,7 +186,7 @@ namespace osu.Game.Rulesets.UI
this.fallback = fallback;
}
public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT)
public override Texture? Get(string name, WrapMode wrapModeS, WrapMode wrapModeT)
=> primary.Get(name, wrapModeS, wrapModeT) ?? fallback.Get(name, wrapModeS, wrapModeT);
protected override void Dispose(bool disposing)

View File

@ -218,6 +218,7 @@ namespace osu.Game.Rulesets.UI
base.ReloadMappings(realmKeyBindings);
KeyBindings = KeyBindings.Where(b => RealmKeyBindingStore.CheckValidForGameplay(b.KeyCombination)).ToList();
RealmKeyBindingStore.ClearDuplicateBindings(KeyBindings);
}
}
}

View File

@ -15,7 +15,7 @@ namespace osu.Game.Screens.Backgrounds
AddInternal(new Background(textureName));
}
public override bool Equals(BackgroundScreen other)
public override bool Equals(BackgroundScreen? other)
{
if (other is BackgroundScreenCustom backgroundScreenCustom)
return base.Equals(other) && textureName == backgroundScreenCustom.textureName;

View File

@ -383,7 +383,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
// Shrink the parent quad to give a bit of padding so the buttons don't stick *right* on the border.
// AABBFloat assumes no rotation. one would hope the whole editor is not being rotated.
var parentQuad = Parent.ScreenSpaceDrawQuad.AABBFloat.Shrink(ToLocalSpace(thisQuad.TopLeft + new Vector2(button_padding * 2)));
var parentQuad = Parent!.ScreenSpaceDrawQuad.AABBFloat.Shrink(ToLocalSpace(thisQuad.TopLeft + new Vector2(button_padding * 2)));
float topExcess = thisQuad.TopLeft.Y - parentQuad.TopLeft.Y;
float bottomExcess = parentQuad.BottomLeft.Y - thisQuad.BottomLeft.Y;
@ -396,7 +396,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
buttons.Anchor = Anchor.BottomCentre;
buttons.Origin = Anchor.BottomCentre;
buttons.Y = Math.Min(0, ToLocalSpace(Parent.ScreenSpaceDrawQuad.BottomLeft).Y - DrawHeight);
buttons.Y = Math.Min(0, ToLocalSpace(Parent!.ScreenSpaceDrawQuad.BottomLeft).Y - DrawHeight);
}
else if (topExcess > bottomExcess)
{

View File

@ -95,11 +95,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
}
private Vector2 getFinalPosition() =>
Parent.ToLocalSpace(facade.ScreenSpaceDrawQuad.Centre);
Parent!.ToLocalSpace(facade.ScreenSpaceDrawQuad.Centre);
private Vector2 getFinalSize() =>
Parent.ToLocalSpace(facade.ScreenSpaceDrawQuad.BottomRight)
- Parent.ToLocalSpace(facade.ScreenSpaceDrawQuad.TopLeft);
Parent!.ToLocalSpace(facade.ScreenSpaceDrawQuad.BottomRight)
- Parent!.ToLocalSpace(facade.ScreenSpaceDrawQuad.TopLeft);
protected override bool OnClick(ClickEvent e)
{

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